Merge branch 'develop' into fix/dependencies-cleanup

This commit is contained in:
Piotr Dobrowolski 2021-04-13 21:33:56 +02:00 committed by GitHub
commit 603fc0a591
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
412 changed files with 9828 additions and 13648 deletions

2
.dockerignore Normal file
View File

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

View File

@ -5,3 +5,11 @@ JITSI_PRIVATE_MODE=false
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=
# The email address used by Let's encrypt to send renewal warnings (compulsory)
ACME_EMAIL=

View File

@ -102,29 +102,6 @@ jobs:
tags: ${{ env.GITHUB_REF_SLUG }}
add_git_labels: true
build-website:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# Create a slugified value of the branch
- uses: rlespinasse/github-slug-action@3.1.0
- name: "Build and push back image"
uses: docker/build-push-action@v1
with:
dockerfile: website/Dockerfile
path: website/
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: thecodingmachine/workadventure-website
tags: ${{ env.GITHUB_REF_SLUG }}
add_git_labels: true
build-maps:
runs-on: ubuntu-latest
@ -156,7 +133,6 @@ jobs:
- build-pusher
- build-maps
- build-uploader
- build-website
runs-on: ubuntu-latest
steps:
@ -174,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 }}
@ -183,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

@ -39,6 +39,10 @@ jobs:
run: yarn run proto && yarn run copy-to-front
working-directory: "messages"
- name: "Create index.html"
run: ./templater.sh
working-directory: "front"
- name: "Build"
run: yarn run build
env:
@ -53,6 +57,49 @@ jobs:
run: yarn test
working-directory: "front"
continuous-integration-pusher:
name: "Continuous Integration Pusher"
runs-on: "ubuntu-latest"
steps:
- name: "Checkout"
uses: "actions/checkout@v2.0.0"
- name: "Setup NodeJS"
uses: actions/setup-node@v1
with:
node-version: '12.x'
- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
version: '3.x'
- name: "Install dependencies"
run: yarn install
working-directory: "pusher"
- name: "Install messages dependencies"
run: yarn install
working-directory: "messages"
- name: "Build proto messages"
run: yarn run proto && yarn run copy-to-pusher
working-directory: "messages"
- name: "Build"
run: yarn run tsc
working-directory: "pusher"
- name: "Lint"
run: yarn run lint
working-directory: "pusher"
- name: "Jasmine"
run: yarn test
working-directory: "pusher"
continuous-integration-back:
name: "Continuous Integration Back"

67
.github/workflows/push-to-npm.yml vendored Normal file
View File

@ -0,0 +1,67 @@
name: Push @workadventure/iframe-api-typings to NPM
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v2
with:
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- name: Edit tsconfig.json to add declarations
run: "sed -i 's/\"declaration\": false/\"declaration\": true/g' tsconfig.json"
working-directory: "front"
- name: Replace version number
run: 'sed -i "s#VERSION_PLACEHOLDER#${GITHUB_REF/refs\/tags\//}#g" package.json'
working-directory: "front/packages/iframe-api-typings"
- name: Debug package.json
run: cat package.json
working-directory: "front/packages/iframe-api-typings"
- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
version: '3.x'
- name: "Install dependencies"
run: yarn install
working-directory: "front"
- name: "Install messages dependencies"
run: yarn install
working-directory: "messages"
- name: "Build proto messages"
run: yarn run proto && yarn run copy-to-front
working-directory: "messages"
- name: "Create index.html"
run: ./templater.sh
working-directory: "front"
- name: "Build"
run: yarn run build
env:
API_URL: "localhost:8080"
working-directory: "front"
# We build the front to generate the typings of iframe_api, then we copy those typings in a separate package.
- name: Copy typings to package dir
run: cp front/dist/src/iframe_api.d.ts front/packages/iframe-api-typings/iframe_api.d.ts
- name: Install dependencies in package
run: yarn install
working-directory: "front/packages/iframe-api-typings"
- name: Publish package
run: yarn publish
working-directory: "front/packages/iframe-api-typings"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,4 +1,4 @@
![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg)
![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) [![Discord](https://img.shields.io/discord/821338762134290432?label=Discord)](https://discord.gg/YGtngdh9gt)
![WorkAdventure landscape image](README-INTRO.jpg)
@ -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.
@ -27,13 +25,14 @@ docker-compose up
The environment will start.
You should now be able to browse to http://workadventure.localhost/ and see the application.
You should now be able to browse to http://play.workadventure.localhost/ and see the application.
You can view the dashboard at http://workadventure.localhost:8080/
Note: on some OSes, you will need to add this line to your `/etc/hosts` file:
**/etc/hosts**
```
workadventure.localhost 127.0.0.1
127.0.0.1 workadventure.localhost
```
### MacOS developers, your environment with Vagrant
@ -101,5 +100,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,5 +1,4 @@
import {HttpRequest, HttpResponse} from "uWebSockets.js";
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
import {HttpResponse} from "uWebSockets.js";
export class BaseController {

View File

@ -4,7 +4,6 @@ import {HttpRequest, HttpResponse} from "uWebSockets.js";
import { parse } from 'query-string';
import {App} from "../Server/sifrr.server";
import {socketManager} from "../Services/SocketManager";
import {ServerWritableStream} from "grpc";
export class DebugController {
constructor(private App : App) {

View File

@ -1,11 +1,8 @@
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
const URL_ROOM_STARTED = "/Floor0/floor0.json";
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
const ADMIN_API_URL = process.env.ADMIN_API_URL || '';
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken';
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600;
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
const JITSI_ISS = process.env.JITSI_ISS || '';
@ -13,16 +10,14 @@ 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,
URL_ROOM_STARTED,
MINIMUM_DISTANCE,
ADMIN_API_URL,
ADMIN_API_TOKEN,
HTTP_PORT,
GRPC_PORT,
MAX_USERS_PER_ROOM,
GROUP_RADIUS,
ALLOW_ARTILLERY,
CPU_OVERHEAT_THRESHOLD,

View File

@ -1,17 +1,10 @@
import { Group } from "./Group";
import { PointInterface } from "./Websocket/PointInterface";
import {Zone} from "_Model/Zone";
import {Movable} from "_Model/Movable";
import {PositionNotifier} from "_Model/PositionNotifier";
import {ServerDuplexStream} from "grpc";
import {
BatchMessage,
PusherToBackMessage,
ServerToAdminClientMessage,
ServerToClientMessage,
SubMessage
SubMessage, UserJoinedRoomMessage, UserLeftRoomMessage
} from "../Messages/generated/messages_pb";
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
import {AdminSocket} from "../RoomManager";
@ -21,16 +14,26 @@ export class Admin {
) {
}
public sendUserJoin(uuid: string): void {
public sendUserJoin(uuid: string, name: string, ip: string): void {
const serverToAdminClientMessage = new ServerToAdminClientMessage();
serverToAdminClientMessage.setUseruuidjoinedroom(uuid);
const userJoinedRoomMessage = new UserJoinedRoomMessage();
userJoinedRoomMessage.setUuid(uuid);
userJoinedRoomMessage.setName(name);
userJoinedRoomMessage.setIpaddress(ip);
serverToAdminClientMessage.setUserjoinedroom(userJoinedRoomMessage);
this.socket.write(serverToAdminClientMessage);
}
public sendUserLeft(uuid: string): void {
public sendUserLeft(uuid: string/*, name: string, ip: string*/): void {
const serverToAdminClientMessage = new ServerToAdminClientMessage();
serverToAdminClientMessage.setUseruuidleftroom(uuid);
const userLeftRoomMessage = new UserLeftRoomMessage();
userLeftRoomMessage.setUuid(uuid);
serverToAdminClientMessage.setUserleftroom(userLeftRoomMessage);
this.socket.write(serverToAdminClientMessage);
}

View File

@ -7,7 +7,6 @@ import {PositionNotifier} from "./PositionNotifier";
import {Movable} from "_Model/Movable";
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier";
import {arrayIntersect} from "../Services/ArrayHelper";
import {MAX_USERS_PER_ROOM} from "../Enum/EnvironmentVariable";
import {JoinRoomMessage} from "../Messages/generated/messages_pb";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {ZoneSocket} from "src/RoomManager";
@ -39,12 +38,10 @@ export class GameRoom {
private readonly positionNotifier: PositionNotifier;
public readonly roomId: string;
public readonly anonymous: boolean;
public tags: string[];
public policyType: GameRoomPolicyTypes;
public readonly roomSlug: string;
public readonly worldSlug: string = '';
public readonly organizationSlug: string = '';
private versionNumber:number = 1;
private nextUserId: number = 1;
constructor(roomId: string,
@ -57,11 +54,8 @@ export class GameRoom {
onLeaves: LeavesCallback)
{
this.roomId = roomId;
this.anonymous = isRoomAnonymous(roomId);
this.tags = [];
this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY;
if (this.anonymous) {
if (isRoomAnonymous(roomId)) {
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
} else {
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId);
@ -102,17 +96,26 @@ export class GameRoom {
}
const position = ProtobufUtils.toPointInterface(positionMessage);
const user = new User(this.nextUserId, joinRoomMessage.getUseruuid(), position, false, this.positionNotifier, socket, joinRoomMessage.getTagList(), joinRoomMessage.getName(), ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()));
const user = new User(this.nextUserId,
joinRoomMessage.getUseruuid(),
joinRoomMessage.getIpaddress(),
position,
false,
this.positionNotifier,
socket,
joinRoomMessage.getTagList(),
joinRoomMessage.getName(),
ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()),
joinRoomMessage.getCompanion()
);
this.nextUserId++;
this.users.set(user.id, user);
this.usersByUuid.set(user.uuid, user);
// Let's call update position to trigger the join / leave room
//this.updatePosition(socket, userPosition);
this.updateUserGroup(user);
// Notify admins
for (const admin of this.admins) {
admin.sendUserJoin(user.uuid);
admin.sendUserJoin(user.uuid, user.name, user.IPAddress);
}
return user;
@ -135,14 +138,10 @@ export class GameRoom {
// Notify admins
for (const admin of this.admins) {
admin.sendUserLeft(user.uuid);
admin.sendUserLeft(user.uuid/*, user.name, user.IPAddress*/);
}
}
get isFull(): boolean {
return this.users.size >= MAX_USERS_PER_ROOM;
}
public isEmpty(): boolean {
return this.users.size === 0 && this.admins.size === 0;
}
@ -301,10 +300,6 @@ export class GameRoom {
return this.itemsState;
}
public canAccess(userTags: string[]): boolean {
return arrayIntersect(userTags, this.tags);
}
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
return this.positionNotifier.addZoneListener(call, x, y);
}
@ -318,11 +313,16 @@ export class GameRoom {
// Let's send all connected users
for (const user of this.users.values()) {
admin.sendUserJoin(user.uuid);
admin.sendUserJoin(user.uuid, user.name, user.IPAddress);
}
}
public adminLeave(admin: Admin): void {
this.admins.delete(admin);
}
public incrementVersion(): number {
this.versionNumber++
return this.versionNumber;
}
}

View File

@ -4,7 +4,7 @@ import {Zone} from "_Model/Zone";
import {Movable} from "_Model/Movable";
import {PositionNotifier} from "_Model/PositionNotifier";
import {ServerDuplexStream} from "grpc";
import {BatchMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb";
import {BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb";
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
@ -16,13 +16,15 @@ export class User implements Movable {
public constructor(
public id: number,
public readonly uuid: string,
public readonly IPAddress: string,
private position: PointInterface,
public silent: boolean,
private positionNotifier: PositionNotifier,
public readonly socket: UserSocket,
public readonly tags: string[],
public readonly name: string,
public readonly characterLayers: CharacterLayer[]
public readonly characterLayers: CharacterLayer[],
public readonly companion?: CompanionMessage
) {
this.listenedZones = new Set<Zone>();

View File

@ -2,25 +2,23 @@ import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb";
import {
AdminGlobalMessage,
AdminMessage,
AdminPusherToBackMessage, BanMessage,
ClientToServerMessage, EmptyMessage,
AdminPusherToBackMessage,
AdminRoomMessage,
BanMessage,
EmptyMessage,
ItemEventMessage,
JoinRoomMessage,
PlayGlobalMessage,
PusherToBackMessage,
QueryJitsiJwtMessage,
ReportPlayerMessage,
RoomJoinedMessage,
QueryJitsiJwtMessage, RefreshRoomPromptMessage,
ServerToAdminClientMessage,
ServerToClientMessage,
SilentMessage,
UserMovesMessage,
ViewportMessage,
WebRtcSignalToServerMessage,
WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage,
ZoneMessage
} from "./Messages/generated/messages_pb";
import grpc, {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc";
import {Empty} from "google-protobuf/google/protobuf/empty_pb";
import {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc";
import {socketManager} from "./Services/SocketManager";
import {emitError} from "./Services/MessageHelpers";
import {User, UserSocket} from "./Model/User";
@ -45,8 +43,13 @@ const roomManager: IRoomManagerServer = {
if (room === null || user === null) {
if (message.hasJoinroommessage()) {
socketManager.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage).then(({room: gameRoom, user: myUser}) => {
if (call.writable) {
room = gameRoom;
user = myUser;
} else {
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
socketManager.leaveRoom(gameRoom, myUser);
}
});
} else {
throw new Error('The first message sent MUST be of type JoinRoomMessage');
@ -54,12 +57,8 @@ const roomManager: IRoomManagerServer = {
} else {
if (message.hasJoinroommessage()) {
throw new Error('Cannot call JoinRoomMessage twice!');
/*} else if (message.hasViewportmessage()) {
socketManager.handleViewport(client, message.getViewportmessage() as ViewportMessage);*/
} else if (message.hasUsermovesmessage()) {
socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage);
/*} else if (message.hasSetplayerdetailsmessage()) {
socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage);*/
} else if (message.hasSilentmessage()) {
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
} else if (message.hasItemeventmessage()) {
@ -70,10 +69,18 @@ const roomManager: IRoomManagerServer = {
socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage);
} else if (message.hasPlayglobalmessage()) {
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
/*} else if (message.hasReportplayermessage()){
socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);*/
} else if (message.hasQueryjitsijwtmessage()){
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
}else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage();
if(sendUserMessage !== undefined) {
socketManager.handlerSendUserMessage(user, sendUserMessage);
}
}else if (message.hasBanusermessage()) {
const banUserMessage = message.getBanusermessage();
if(banUserMessage !== undefined) {
socketManager.handlerBanUserMessage(room, user, banUserMessage);
}
} else {
throw new Error('Unhandled message type');
}
@ -113,9 +120,6 @@ const roomManager: IRoomManagerServer = {
call.end();
})
/*call.on('finish', () => {
debug('listenZone finish');
})*/
call.on('close', () => {
debug('listenZone connection closed');
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
@ -143,26 +147,6 @@ const roomManager: IRoomManagerServer = {
} else {
throw new Error('The first message sent MUST be of type JoinRoomMessage');
}
} else {
/*if (message.hasJoinroommessage()) {
throw new Error('Cannot call JoinRoomMessage twice!');
} else if (message.hasUsermovesmessage()) {
socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage);
} else if (message.hasSilentmessage()) {
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
} else if (message.hasItemeventmessage()) {
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
} else if (message.hasWebrtcsignaltoservermessage()) {
socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage);
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage);
} else if (message.hasPlayglobalmessage()) {
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
} else if (message.hasQueryjitsijwtmessage()){
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
} else {
throw new Error('Unhandled message type');
}*/
}
} catch (e) {
emitError(call, e);
@ -196,9 +180,21 @@ const roomManager: IRoomManagerServer = {
callback(null, new EmptyMessage());
},
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
// FIXME Work in progress
socketManager.banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage());
socketManager.banUser(call.request.getRoomid(), call.request.getRecipientuuid());
callback(null, new EmptyMessage());
},
sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage());
callback(null, new EmptyMessage());
},
sendWorldFullWarningToRoom(call: ServerUnaryCall<WorldFullWarningToRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
socketManager.dispatchWorlFullWarning(call.request.getRoomid());
callback(null, new EmptyMessage());
},
sendRefreshRoomPrompt(call: ServerUnaryCall<RefreshRoomPromptMessage>, callback: sendUnaryData<EmptyMessage>): void {
socketManager.dispatchRoomRefresh(call.request.getRoomid());
callback(null, new EmptyMessage());
},
};

View File

@ -1,115 +0,0 @@
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
import Axios from "axios";
import {v4} from "uuid";
export interface AdminApiData {
organizationSlug: string
worldSlug: string
roomSlug: string
mapUrlStart: string
tags: string[]
policy_type: number
userUuid: string
messages?: unknown[],
textures: CharacterTexture[]
}
export interface CharacterTexture {
id: number,
level: number,
url: string,
rights: string
}
export interface FetchMemberDataByUuidResponse {
uuid: string;
tags: string[];
textures: CharacterTexture[];
messages: unknown[];
}
class AdminApi {
async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise<AdminApiData> {
if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!');
}
const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = {
organizationSlug,
worldSlug
};
if (roomSlug) {
params.roomSlug = roomSlug;
}
const res = await Axios.get(ADMIN_API_URL + '/api/map',
{
headers: {"Authorization": `${ADMIN_API_TOKEN}`},
params
}
)
return res.data;
}
async fetchMemberDataByUuid(uuid: string): Promise<FetchMemberDataByUuidResponse> {
if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!');
}
try {
const res = await Axios.get(ADMIN_API_URL+'/api/membership/'+uuid,
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
)
return res.data;
} catch (e) {
if (e?.response?.status == 404) {
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
console.warn('Cannot find user with uuid "'+uuid+'". Performing an anonymous login instead.');
return {
uuid: v4(),
tags: [],
textures: [],
messages: [],
}
} else {
throw e;
}
}
}
async fetchMemberDataByToken(organizationMemberToken: string): Promise<AdminApiData> {
if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!');
}
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken,
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
)
return res.data;
}
async fetchCheckUserByToken(organizationMemberToken: string): Promise<AdminApiData> {
if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!');
}
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken,
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
)
return res.data;
}
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) {
return Axios.post(`${ADMIN_API_URL}/api/report`, {
reportedUserUuid,
reportedUserComment,
reporterUserUuid,
},
{
headers: {"Authorization": `${ADMIN_API_TOKEN}`}
});
}
}
export const adminApi = new AdminApi();

View File

@ -1,24 +1,15 @@
import {GameRoom} from "../Model/GameRoom";
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
import {
GroupDeleteMessage,
GroupUpdateMessage,
ItemEventMessage,
ItemStateMessage,
PlayGlobalMessage,
PointMessage,
PositionMessage,
RoomJoinedMessage,
ServerToClientMessage,
SetPlayerDetailsMessage,
SilentMessage,
SubMessage,
ReportPlayerMessage,
UserJoinedMessage,
UserLeftMessage,
UserMovedMessage,
UserMovesMessage,
ViewportMessage,
WebRtcDisconnectMessage,
WebRtcSignalToClientMessage,
WebRtcSignalToServerMessage,
@ -28,47 +19,47 @@ import {
SendUserMessage,
JoinRoomMessage,
Zone as ProtoZone,
BatchMessage,
BatchToPusherMessage,
SubToPusherMessage,
UserJoinedZoneMessage, GroupUpdateZoneMessage, GroupLeftZoneMessage, UserLeftZoneMessage, AdminMessage, BanMessage
UserJoinedZoneMessage,
GroupUpdateZoneMessage,
GroupLeftZoneMessage,
WorldFullWarningMessage,
UserLeftZoneMessage,
BanUserMessage, RefreshRoomMessage,
} from "../Messages/generated/messages_pb";
import {User, UserSocket} from "../Model/User";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {Group} from "../Model/Group";
import {cpuTracker} from "./CpuTracker";
import {ADMIN_API_URL, 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, FetchMemberDataByUuidResponse} from "./AdminApi";
import Jwt from "jsonwebtoken";
import {JITSI_URL} from "../Enum/EnvironmentVariable";
import {clientEventsEmitter} from "./ClientEventsEmitter";
import {gaugeManager} from "./GaugeManager";
import {AdminSocket, ZoneSocket} from "../RoomManager";
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');
interface AdminSocketRoomsList {
[index: string]: number;
}
interface AdminSocketUsersList {
[index: string]: boolean;
}
export interface AdminSocketData {
rooms: AdminSocketRoomsList,
users: AdminSocketUsersList,
}
function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void {
// TODO: should we batch those every 100ms?
const batchMessage = new BatchToPusherMessage();
batchMessage.addPayload(subMessage);
socket.write(batchMessage);
}
@ -84,68 +75,20 @@ export class SocketManager {
});
}
/*getAdminSocketDataFor(roomId:string): AdminSocketData {
const data:AdminSocketData = {
rooms: {},
users: {},
}
const room = this.rooms.get(roomId);
if (room === undefined) {
return data;
}
const users = room.getUsers();
data.rooms[roomId] = users.size;
users.forEach(user => {
data.users[user.uuid] = true
})
return data;
}*/
public async handleJoinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> {
/*const positionMessage = joinRoomMessage.getPositionmessage();
if (positionMessage === undefined) {
// TODO: send error message?
throw new Error('Empty pointMessage found in JoinRoomMessage');
}*/
//const position = ProtobufUtils.toPointInterface(positionMessage);
//const viewport = client.viewport;
//this.sockets.set(client.userId, client); //todo: should this be at the end of the function?
//join new previous room
const {room, user} = await this.joinRoom(socket, joinRoomMessage);
//const things = room.setViewport(client, viewport);
if (!socket.writable) {
console.warn('Socket was aborted');
return {
room,
user
};
}
const roomJoinedMessage = new RoomJoinedMessage();
/*for (const thing of things) {
if (thing instanceof User) {
const player: ExSocketInterface|undefined = this.sockets.get(thing.id);
if (player === undefined) {
console.warn('Something went wrong. The World contains a user "'+thing.id+"' but this user does not exist in the sockets list!");
continue;
}
const userJoinedMessage = new UserJoinedMessage();
userJoinedMessage.setUserid(thing.id);
userJoinedMessage.setName(player.name);
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(player.characterLayers));
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(player.position));
roomJoinedMessage.addUser(userJoinedMessage);
roomJoinedMessage.setTagList(joinRoomMessage.getTagList());
} else if (thing instanceof Group) {
const groupUpdateMessage = new GroupUpdateMessage();
groupUpdateMessage.setGroupid(thing.getId());
groupUpdateMessage.setPosition(ProtobufUtils.toPointMessage(thing.getPosition()));
roomJoinedMessage.addGroup(groupUpdateMessage);
} else {
console.error("Unexpected type for Movable returned by setViewport");
}
}*/
for (const [itemId, item] of room.getItemsState().entries()) {
const itemStateMessage = new ItemStateMessage();
@ -159,9 +102,6 @@ export class SocketManager {
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage);
//user.socket.write(serverToClientMessage);
console.log('SENDING MESSAGE roomJoinedMessage');
socket.write(serverToClientMessage);
return {
@ -169,13 +109,6 @@ export class SocketManager {
user
};
/*const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage);
if (!client.disconnecting) {
client.send(serverToClientMessage.serializeBinary().buffer, true);
}*/
}
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
@ -196,14 +129,6 @@ export class SocketManager {
throw new Error('Viewport not found in message');
}
// sending to all clients in room except sender
/*client.position = {
x: position.x,
y: position.y,
direction,
moving: position.moving,
};
client.viewport = viewport;*/
// update position in the world
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
@ -258,21 +183,6 @@ export class SocketManager {
}
}
// TODO: handle this message in pusher
/*async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) {
try {
const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid());
if (!reportedSocket) {
throw 'reported socket user not found';
}
//TODO report user on admin application
await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid)
} catch (e) {
console.error('An error occurred on "handleReportMessage"');
console.error(e);
}
}*/
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
//send only at user
const remoteUser = room.getUsers().get(data.getReceiverid());
@ -284,6 +194,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);
@ -304,6 +220,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);
@ -324,8 +246,6 @@ export class SocketManager {
debug('Room is empty. Deleting room "%s"', room.roomId);
}
} finally {
//delete Client.roomId;
//this.sockets.delete(Client.userId);
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
console.log('A user left');
}
@ -345,11 +265,6 @@ export class SocketManager {
(thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener),
(thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener)
);
if (!world.anonymous) {
const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug)
world.tags = data.tags
world.policyType = Number(data.policy_type)
}
gaugeManager.incNbRoomGauge();
this.rooms.set(roomId, world);
}
@ -360,20 +275,14 @@ export class SocketManager {
const roomId = joinRoomMessage.getRoomid();
const world = await socketManager.getOrCreateRoom(roomId);
// Dispatch groups position to newly connected user
/*world.getGroups().forEach((group: Group) => {
this.emitCreateUpdateGroupEvent(socket, group);
});*/
const room = await socketManager.getOrCreateRoom(roomId);
//join world
const user = world.join(socket, joinRoomMessage);
const user = room.join(socket, joinRoomMessage);
clientEventsEmitter.emitClientJoin(user.uuid, roomId);
//console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)');
console.log(new Date().toISOString() + ' A user joined');
return {room: world, user};
return {room, user};
}
private onZoneEnter(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) {
@ -387,6 +296,7 @@ export class SocketManager {
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone));
userJoinedZoneMessage.setCompanion(thing.companion);
const subMessage = new SubToPusherMessage();
subMessage.setUserjoinedzonemessage(userJoinedZoneMessage);
@ -481,10 +391,6 @@ export class SocketManager {
}
private joinWebRtcRoom(user: User, group: Group) {
/*const roomId: string = "webrtcroom"+group.getId();
if (user.socket.webRtcRoomId === roomId) {
return;
}*/
for (const otherUser of group.getUsers()) {
if (user === otherUser) {
@ -496,6 +402,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);
@ -509,6 +420,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);
@ -521,6 +437,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
@ -626,31 +561,31 @@ export class SocketManager {
user.socket.write(serverToClientMessage);
}
/**
* Merges the characterLayers received from the front (as an array of string) with the custom textures from the back.
*/
static mergeCharacterLayersAndCustomTextures(characterLayers: string[], memberTextures: CharacterTexture[]): CharacterLayer[] {
const characterLayerObjs: CharacterLayer[] = [];
for (const characterLayer of characterLayers) {
if (characterLayer.startsWith('customCharacterTexture')) {
const customCharacterLayerId: number = +characterLayer.substr(22);
for (const memberTexture of memberTextures) {
if (memberTexture.id == customCharacterLayerId) {
characterLayerObjs.push({
name: characterLayer,
url: memberTexture.url
})
break;
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage){
const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
sendUserMessage.setType(sendUserMessageToSend.getType());
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setSendusermessage(sendUserMessage);
user.socket.write(serverToClientMessage);
}
}
} else {
characterLayerObjs.push({
name: characterLayer,
url: undefined
})
}
}
return characterLayerObjs;
public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage){
const banUserMessage = new BanUserMessage();
banUserMessage.setMessage(banUserMessageToSend.getMessage());
banUserMessage.setType(banUserMessageToSend.getType());
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setSendusermessage(banUserMessage);
user.socket.write(serverToClientMessage);
setTimeout(() => {
// Let's leave the room now.
room.leave(user);
// Let's close the connection when the user is banned.
user.socket.end();
}, 10000);
}
public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void {
@ -671,6 +606,7 @@ export class SocketManager {
userJoinedMessage.setName(thing.name);
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
userJoinedMessage.setCompanion(thing.companion);
const subMessage = new SubToPusherMessage();
subMessage.setUserjoinedzonemessage(userJoinedMessage);
@ -706,11 +642,6 @@ export class SocketManager {
public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise<GameRoom> {
const room = await socketManager.getOrCreateRoom(roomId);
// Dispatch groups position to newly connected user
/*world.getGroups().forEach((group: Group) => {
this.emitCreateUpdateGroupEvent(socket, group);
});*/
room.adminJoin(admin);
return room;
@ -740,15 +671,15 @@ export class SocketManager {
const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(message);
sendUserMessage.setType('ban');
sendUserMessage.setType('ban'); //todo: is the type correct?
const subToPusherMessage = new SubToPusherMessage();
subToPusherMessage.setSendusermessage(sendUserMessage);
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setSendusermessage(sendUserMessage);
recipient.socket.write(subToPusherMessage);
recipient.socket.write(serverToClientMessage);
}
public banUser(roomId: string, recipientUuid: string): void {
public banUser(roomId: string, recipientUuid: string, message: string): void {
const room = this.rooms.get(roomId);
if (!room) {
console.error("In banUser, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
@ -764,17 +695,75 @@ export class SocketManager {
// Let's leave the room now.
room.leave(recipient);
const sendUserMessage = new SendUserMessage();
sendUserMessage.setType('banned');
const banUserMessage = new BanUserMessage();
banUserMessage.setMessage(message);
banUserMessage.setType('banned');
const subToPusherMessage = new SubToPusherMessage();
subToPusherMessage.setSendusermessage(sendUserMessage);
recipient.socket.write(subToPusherMessage);
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setBanusermessage(banUserMessage);
// Let's close the connection when the user is banned.
recipient.socket.write(serverToClientMessage);
recipient.socket.end();
}
sendAdminRoomMessage(roomId: string, message: string) {
const room = this.rooms.get(roomId);
if (!room) {
//todo: this should cause the http call to return a 500
console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
return;
}
room.getUsers().forEach((recipient) => {
const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(message);
sendUserMessage.setType('message');
const clientMessage = new ServerToClientMessage();
clientMessage.setSendusermessage(sendUserMessage);
recipient.socket.write(clientMessage);
});
}
dispatchWorlFullWarning(roomId: string,): void {
const room = this.rooms.get(roomId);
if (!room) {
//todo: this should cause the http call to return a 500
console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
return;
}
room.getUsers().forEach((recipient) => {
const worldFullMessage = new WorldFullWarningMessage();
const clientMessage = new ServerToClientMessage();
clientMessage.setWorldfullwarningmessage(worldFullMessage);
recipient.socket.write(clientMessage);
});
}
dispatchRoomRefresh(roomId: string,): void {
const room = this.rooms.get(roomId);
if (!room) {
return;
}
const versionNumber = room.incrementVersion();
room.getUsers().forEach((recipient) => {
const worldFullMessage = new RefreshRoomMessage();
worldFullMessage.setRoomid(roomId)
worldFullMessage.setVersionnumber(versionNumber)
const clientMessage = new ServerToClientMessage();
clientMessage.setRefreshroommessage(worldFullMessage);
recipient.socket.write(clientMessage);
});
}
}
export const socketManager = new SocketManager();

View File

@ -26,6 +26,7 @@ function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMess
positionMessage.setMoving(false);
const joinRoomMessage = new JoinRoomMessage();
joinRoomMessage.setUseruuid('1');
joinRoomMessage.setIpaddress('10.0.0.2');
joinRoomMessage.setName('foo');
joinRoomMessage.setRoomid('_/global/test.json');
joinRoomMessage.setPositionmessage(positionMessage);

View File

@ -25,14 +25,14 @@ describe("PositionNotifier", () => {
leaveTriggered = true;
});
const user1 = new User(1, 'test', {
const user1 = new User(1, 'test', '10.0.0.2', {
x: 500,
y: 500,
moving: false,
direction: 'down'
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
const user2 = new User(2, 'test', {
const user2 = new User(2, 'test', '10.0.0.2', {
x: -9999,
y: -9999,
moving: false,
@ -100,14 +100,14 @@ describe("PositionNotifier", () => {
leaveTriggered = true;
});
const user1 = new User(1, 'test', {
const user1 = new User(1, 'test', '10.0.0.2', {
x: 500,
y: 500,
moving: false,
direction: 'down'
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
const user2 = new User(2, 'test', {
const user2 = new User(2, 'test', '10.0.0.2', {
x: 0,
y: 0,
moving: false,

View File

@ -2833,9 +2833,9 @@ xtend@^4.0.0:
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
y18n@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
version "3.2.2"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==
yallist@^3.0.0, yallist@^3.0.3:
version "3.1.1"

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=${ACME_EMAIL}
- --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"
PUSHER_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://admin."+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,9 +22,13 @@
"JITSI_ISS": env.JITSI_ISS,
"JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
} + if adminUrl != null then {
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
} + (if adminUrl != null then {
"ADMIN_API_URL": adminUrl,
} else {}
} else {}) + if namespace != "master" then {
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
},
"back2": {
"image": "thecodingmachine/workadventure-back:"+tag,
@ -39,9 +44,13 @@
"JITSI_ISS": env.JITSI_ISS,
"JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
} + if adminUrl != null then {
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
} + (if adminUrl != null then {
"ADMIN_API_URL": adminUrl,
} else {}
} else {}) + if namespace != "master" then {
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
},
"pusher": {
"replicas": 2,
@ -58,9 +67,12 @@
"JITSI_URL": env.JITSI_URL,
"API_URL": "back1:50051,back2:50051",
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
} + if adminUrl != null then {
} + (if adminUrl != null then {
"ADMIN_API_URL": adminUrl,
} else {}
} else {}) + if namespace != "master" then {
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
},
"front": {
"image": "thecodingmachine/workadventure-front:"+tag,
@ -70,15 +82,15 @@
},
"ports": [80],
"env": {
"API_URL": "pusher."+url,
"UPLOADER_URL": "uploader."+url,
"ADMIN_URL": "admin."+url,
"PUSHER_URL": "//pusher."+url,
"UPLOADER_URL": "//uploader."+url,
"ADMIN_URL": "//"+url,
"JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
"TURN_USER": "workadventure",
"TURN_PASSWORD": "WorkAdventure123",
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false"
"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"
}
},
"uploader": {
@ -100,17 +112,6 @@
},
"ports": [80]
},
"website": {
"image": "thecodingmachine/workadventure-website:"+tag,
"host": {
"url": url,
"https": "enable"
},
"ports": [80],
"env": {
"GAME_URL": "https://play."+url
}
}
},
"config": {
"https": {

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

@ -0,0 +1,207 @@
version: "3"
services:
reverse-proxy:
image: traefik:v2.0
command:
- --api.insecure=true
- --providers.docker
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
ports:
- "80:80"
- "443:443"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
depends_on:
- back
- front
volumes:
- /var/run/docker.sock:/var/run/docker.sock
front:
image: thecodingmachine/nodejs:14
environment:
DEBUG_MODE: "$DEBUG_MODE"
JITSI_URL: $JITSI_URL
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
HOST: "0.0.0.0"
NODE_ENV: development
PUSHER_URL: /pusher
UPLOADER_URL: /uploader
ADMIN_URL: /admin
MAPS_URL: /maps
STARTUP_COMMAND_1: yarn install
TURN_SERVER: "turn:localhost:3478,turns: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:
- ./front:/usr/src/app
labels:
- "traefik.http.routers.front.rule=PathPrefix(`/`)"
- "traefik.http.routers.front.entryPoints=web,traefik"
- "traefik.http.services.front.loadbalancer.server.port=8080"
- "traefik.http.routers.front-ssl.rule=PathPrefix(`/`)"
- "traefik.http.routers.front-ssl.entryPoints=websecure"
- "traefik.http.routers.front-ssl.tls=true"
- "traefik.http.routers.front-ssl.service=front"
pusher:
image: thecodingmachine/nodejs:12
command: yarn dev
#command: yarn run prod
#command: yarn run profile
environment:
DEBUG: "*"
STARTUP_COMMAND_1: yarn install
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
API_URL: back:50051
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
volumes:
- ./pusher:/usr/src/app
labels:
- "traefik.http.middlewares.strip-pusher-prefix.stripprefix.prefixes=/pusher"
- "traefik.http.routers.pusher.rule=PathPrefix(`/pusher`)"
- "traefik.http.routers.pusher.middlewares=strip-pusher-prefix@docker"
- "traefik.http.routers.pusher.entryPoints=web"
- "traefik.http.services.pusher.loadbalancer.server.port=8080"
- "traefik.http.routers.pusher-ssl.rule=PathPrefix(`/pusher`)"
- "traefik.http.routers.pusher-ssl.middlewares=strip-pusher-prefix@docker"
- "traefik.http.routers.pusher-ssl.entryPoints=websecure"
- "traefik.http.routers.pusher-ssl.tls=true"
- "traefik.http.routers.pusher-ssl.service=pusher"
maps:
image: thecodingmachine/nodejs:12-apache
environment:
DEBUG_MODE: "$DEBUG_MODE"
HOST: "0.0.0.0"
NODE_ENV: development
#APACHE_DOCUMENT_ROOT: dist/
#APACHE_EXTENSIONS: headers
#APACHE_EXTENSION_HEADERS: 1
STARTUP_COMMAND_0: sudo a2enmod headers
STARTUP_COMMAND_1: yarn install
STARTUP_COMMAND_2: yarn run dev &
volumes:
- ./maps:/var/www/html
labels:
- "traefik.http.middlewares.strip-maps-prefix.stripprefix.prefixes=/maps"
- "traefik.http.routers.maps.rule=PathPrefix(`/maps`)"
- "traefik.http.routers.maps.middlewares=strip-maps-prefix@docker"
- "traefik.http.routers.maps.entryPoints=web,traefik"
- "traefik.http.services.maps.loadbalancer.server.port=80"
- "traefik.http.routers.maps-ssl.rule=PathPrefix(`/maps`)"
- "traefik.http.routers.maps-ssl.middlewares=strip-maps-prefix@docker"
- "traefik.http.routers.maps-ssl.entryPoints=websecure"
- "traefik.http.routers.maps-ssl.tls=true"
- "traefik.http.routers.maps-ssl.service=maps"
back:
image: thecodingmachine/nodejs:12
command: yarn dev
#command: yarn run profile
environment:
DEBUG: "*"
STARTUP_COMMAND_1: yarn install
SECRET_KEY: yourSecretKey
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
ALLOW_ARTILLERY: "true"
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
volumes:
- ./back:/usr/src/app
labels:
- "traefik.http.middlewares.strip-api-prefix.stripprefix.prefixes=/api"
- "traefik.http.routers.back.rule=PathPrefix(`/api`)"
- "traefik.http.routers.back.middlewares=strip-api-prefix@docker"
- "traefik.http.routers.back.entryPoints=web"
- "traefik.http.services.back.loadbalancer.server.port=8080"
- "traefik.http.routers.back-ssl.rule=PathPrefix(`/api`)"
- "traefik.http.routers.back-ssl.middlewares=strip-api-prefix@docker"
- "traefik.http.routers.back-ssl.entryPoints=websecure"
- "traefik.http.routers.back-ssl.tls=true"
- "traefik.http.routers.back-ssl.service=back"
uploader:
image: thecodingmachine/nodejs:12
command: yarn dev
#command: yarn run profile
environment:
DEBUG: "*"
STARTUP_COMMAND_1: yarn install
volumes:
- ./uploader:/usr/src/app
labels:
- "traefik.http.middlewares.strip-uploader-prefix.stripprefix.prefixes=/uploader"
- "traefik.http.routers.uploader.rule=PathPrefix(`/uploader`)"
- "traefik.http.routers.uploader.middlewares=strip-uploader-prefix@docker"
- "traefik.http.routers.uploader.entryPoints=web"
- "traefik.http.services.uploader.loadbalancer.server.port=8080"
- "traefik.http.routers.uploader-ssl.rule=PathPrefix(`/uploader`)"
- "traefik.http.routers.uploader-ssl.middlewares=strip-uploader-prefix@docker"
- "traefik.http.routers.uploader-ssl.entryPoints=websecure"
- "traefik.http.routers.uploader-ssl.tls=true"
- "traefik.http.routers.uploader-ssl.service=uploader"
website:
image: thecodingmachine/nodejs:12-apache
environment:
STARTUP_COMMAND_1: npm install
STARTUP_COMMAND_2: npm run watch &
APACHE_DOCUMENT_ROOT: dist/
volumes:
- ./website:/var/www/html
labels:
- "traefik.http.routers.website.rule=Host(`workadventure.localhost`)"
- "traefik.http.routers.website.entryPoints=web"
- "traefik.http.services.website.loadbalancer.server.port=80"
- "traefik.http.routers.website-ssl.rule=Host(`workadventure.localhost`)"
- "traefik.http.routers.website-ssl.entryPoints=websecure"
- "traefik.http.routers.website-ssl.tls=true"
- "traefik.http.routers.website-ssl.service=website"
messages:
#image: thecodingmachine/nodejs:14
image: thecodingmachine/workadventure-back-base:latest
environment:
#STARTUP_COMMAND_0: sudo apt-get install -y inotify-tools
STARTUP_COMMAND_1: yarn install
STARTUP_COMMAND_2: yarn run proto:watch
volumes:
- ./messages:/usr/src/app
- ./back:/usr/src/back
- ./front:/usr/src/front
- ./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=localhost
# - --server-name=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

View File

@ -26,19 +26,24 @@ services:
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
HOST: "0.0.0.0"
NODE_ENV: development
API_URL: pusher.workadventure.localhost
UPLOADER_URL: uploader.workadventure.localhost
ADMIN_URL: admin.workadventure.localhost
STARTUP_COMMAND_1: yarn install
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
TURN_USER: workadventure
TURN_PASSWORD: WorkAdventure123
PUSHER_URL: //pusher.workadventure.localhost
UPLOADER_URL: //uploader.workadventure.localhost
ADMIN_URL: //workadventure.localhost
STARTUP_COMMAND_1: ./templater.sh
STARTUP_COMMAND_2: yarn install
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:
- ./front:/usr/src/app
labels:
- "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)"
- "traefik.http.routers.front.entryPoints=web,traefik"
- "traefik.http.routers.front.entryPoints=web"
- "traefik.http.services.front.loadbalancer.server.port=8080"
- "traefik.http.routers.front-ssl.rule=Host(`play.workadventure.localhost`)"
- "traefik.http.routers.front-ssl.entryPoints=websecure"
@ -48,10 +53,8 @@ services:
pusher:
image: thecodingmachine/nodejs:12
command: yarn dev
#command: yarn run prod
#command: yarn run profile
environment:
DEBUG: "*"
DEBUG: "socket:*"
STARTUP_COMMAND_1: yarn install
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey
@ -106,6 +109,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:
@ -135,23 +139,6 @@ services:
- "traefik.http.routers.uploader-ssl.tls=true"
- "traefik.http.routers.uploader-ssl.service=uploader"
website:
image: thecodingmachine/nodejs:12-apache
environment:
STARTUP_COMMAND_1: npm install
STARTUP_COMMAND_2: npm run watch &
APACHE_DOCUMENT_ROOT: dist/
volumes:
- ./website:/var/www/html
labels:
- "traefik.http.routers.website.rule=Host(`workadventure.localhost`)"
- "traefik.http.routers.website.entryPoints=web"
- "traefik.http.services.website.loadbalancer.server.port=80"
- "traefik.http.routers.website-ssl.rule=Host(`workadventure.localhost`)"
- "traefik.http.routers.website-ssl.entryPoints=websecure"
- "traefik.http.routers.website-ssl.tls=true"
- "traefik.http.routers.website-ssl.service=website"
messages:
#image: thecodingmachine/nodejs:14
image: thecodingmachine/workadventure-back-base:latest
@ -164,3 +151,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/.gitignore vendored
View File

@ -6,3 +6,4 @@
/dist/webpack.config.js.map
/dist/src
*.sh
!templater.sh

View File

@ -3,13 +3,19 @@ WORKDIR /var/www/messages
COPY --chown=docker:docker messages .
RUN yarn install && yarn proto
# we are rebuilding on each deploy to cope with the API_URL environment URL
# we are rebuilding on each deploy to cope with the PUSHER_URL environment URL
FROM thecodingmachine/nodejs:14-apache
COPY --chown=docker:docker front .
COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated
# Removing the iframe.html file from the final image as this adds a XSS attack.
# iframe.html is only in dev mode to circumvent a limitation
RUN rm dist/iframe.html
RUN yarn install
ENV NODE_ENV=production
ENV STARTUP_COMMAND_0="./templater.sh"
ENV STARTUP_COMMAND_1="yarn run build"
ENV APACHE_DOCUMENT_ROOT=dist/

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

@ -0,0 +1,4 @@
index.html
index.tmpl.html.tmp
/js/
style.*.css

9
front/dist/ga.html.tmpl vendored Normal file
View File

@ -0,0 +1,9 @@
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<!-- TRACKING NUMBER -->"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<!-- TRACKING NUMBER -->');
</script>

17
front/dist/iframe.html vendored Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<script src="/iframe_api.js" ></script>
<script>
// Note: this is a huge XSS flow as we allow anyone to load a Javascript file in our domain.
// This file must ABSOLUTELY be removed from the Docker images/deployments and is only here
// for development purpose (because dynamically generated iframes are not working with
// webpack hot reload due to an issue with rights)
const urlParams = new URLSearchParams(window.location.search);
const scriptUrl = urlParams.get('script');
const script = document.createElement('script');
script.src = scriptUrl;
document.head.append(script);
</script>
</head>
</html>

View File

@ -6,15 +6,8 @@
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-10196481-11"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-10196481-11');
</script>
<!-- TRACK CODE -->
<!-- END TRACK CODE -->
<link rel="apple-touch-icon" sizes="57x57" href="static/images/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="static/images/favicons/apple-icon-60x60.png">
@ -36,7 +29,9 @@
<base href="/">
<link rel="stylesheet" href="/resources/style/style.css">
<link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet">
<link href="https://unpkg.com/nes.css@2.3.0/css/nes.min.css" rel="stylesheet" />
<title>WorkAdventure</title>
</head>
<body id="body" style="margin: 0; background-color: #000">
@ -73,61 +68,39 @@
</div>
</div>
<div id="cowebsite" class="cowebsite hidden">
<button class="close-btn" id="cowebsite-close">
<aside id="cowebsite-aside">
<img src="/static/images/menu.svg" alt="hold to resize"/>
</aside>
<main id="cowebsite-main">
</main>
<button class="top-right-btn" id="cowebsite-fullscreen" alt="fullscreen mode">
<img id="cowebsite-fullscreen-open" src="resources/logos/fullscreen.svg"/>
<img id="cowebsite-fullscreen-close" style="display: none;" src="resources/logos/fullscreen-exit.svg"/>
</button>
<button class="top-right-btn" id="cowebsite-close" alt="close the iframe">
<img src="resources/logos/close.svg"/>
</button>
</div>
<div class="audio-playing">
<img src="/resources/logos/megaphone.svg"/>
</div>
</div>
<div id="audioplayerctrl" class="hidden">
<div class="audioplayer">
<button type="button" id="audioplayer_mute" class="fa fa-volump-up">
<svg
width="1em"
height="1em"
viewBox="0 0 16 16"
class="bi bi-volume-up"
fill="white"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z"
/>
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
<g id="audioplayer_volume_icon_playing">
<path
d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z"
/>
<path
d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z"
/>
<path
d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z"
/>
<path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" />
<path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" />
<path d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z" />
</g>
</svg>
</button>
<div class="audioplayer">
<input
type="range"
id="audioplayer_volume"
min="0"
max="1"
step="0.05"
value="1"
/>
<input type="range" id="audioplayer_volume" min="0" max="1" step="0.025" value="1" />
</div>
</div>
<div class="audioplayer">
<label
id="label-audioplayer_decrease_while_talking"
for="audiooplayer_decrease_while_talking"
title="decrease background volume by 50% when entering conversations"
>
autoreduce
<label id="label-audioplayer_decrease_while_talking" for="audiooplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
reduce in conversations
<input type="checkbox" id="audioplayer_decrease_while_talking" checked />
</label>
<div id="audioplayer" style="visibility: hidden"></div>
@ -137,29 +110,7 @@
<img src="/resources/logos/megaphone.svg" />
</div>
</div>
<!--
<div id="webRtc" class="webrtc">
<div id="activeCam" class="activeCam">
<div id="div-myCamVideo" class="video-container">
<video id="myCamVideo" autoplay muted></video>
</div>
</div>
<div class="btn-cam-action">
<div class="btn-micro">
<img id="microphone" src="resources/logos/microphone.svg">
<img id="microphone-close" src="resources/logos/microphone-close.svg">
</div>
<div class="btn-video">
<img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg">
</div>
<div class="btn-monitor">
<img id="monitor" src="resources/logos/monitor.svg">
<img id="monitor-close" src="resources/logos/monitor-close.svg">
</div>
</div>
</div>
-->
<div id="activeScreenSharing" class="active-screen-sharing active">
</div>
<div id="webRtcSetup" class="webrtcsetup">

View File

@ -1,11 +1,4 @@
<style>
*{
font-family: 'Open Sans', sans-serif;
cursor: url('/resources/logos/cursor_normal.png'), auto;
}
* a, button, select{
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
#gameMenu button {
background-color: black;
color: white;
@ -15,6 +8,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>
@ -29,14 +30,24 @@
<section>
<button id="changeSkinButton">Edit skin</button>
</section>
<section>
<button id="changeCompanionButton">Edit companion</button>
</section>
<section>
<button id="editGameSettingsButton">Settings</button>
</section>
<section>
<button id="toggleFullscreen">Toggle fullscreen</button>
</section>
<section>
<button id="sparkButton">Create map</button>
</section>
<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

@ -1,11 +1,4 @@
<style>
*{
font-family: 'Open Sans', sans-serif;
cursor: url('/resources/logos/cursor_normal.png'), auto;
}
* a, button, select{
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
#menuIcon button {
background-color: black;
color: white;

View File

@ -1,11 +1,4 @@
<style>
*{
font-family: 'Open Sans', sans-serif;
cursor: url('/resources/logos/cursor_normal.png'), auto;
}
* a, button, select{
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
#gameQuality {
background: #eceeee;
border: 1px solid #42464b;

View File

@ -0,0 +1,115 @@
<style>
#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

@ -1,11 +1,4 @@
<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;
}
#gameShare {
background: #eceeee;
border: 1px solid #42464b;
@ -14,9 +7,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,103 @@
<style>
#helpCameraSettings {
background: #eceeee;
border: 1px solid #42464b;
border-radius: 6px;
margin: 10px auto 0;
width: 400px;
height: 370px;
}
#helpCameraSettings 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;
}
#helpCameraSettings input {
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: 30px;
transition: box-shadow 0.3s;
width: 100%;
}
#helpCameraSettings section {
margin: 10px;
}
#helpCameraSettings section.action{
text-align: center;
margin: 0;
}
#helpCameraSettings button {
margin-top: 10px;
background-color: black;
color: white;
border-radius: 7px;
padding-bottom: 4px;
}
#helpCameraSettings button#helpCameraSettingsFormCancel {
background-color: #c7c7c700;
color: #292929;
}
#helpCameraSettings section a{
text-align: center;
font-size: 12px;
margin: 0 6px;
color: black;
}
#helpCameraSettings section h6,
#helpCameraSettings section h5{
margin: 1px;
}
#helpCameraSettings section.text-center{
text-align: center;
}
#helpCameraSettings section p{
font-size: 8px;
margin: 0px 20px;
}
#helpCameraSettings section p.err{
color: #ff0000;
}
#helpCameraSettings section ul{
margin: 6px;
}
#helpCameraSettings section li{
text-align: left;
font-size: 8px;
}
#helpCameraSettings section img {
width: 200px;
margin-top: 10px;
}
</style>
<form id="helpCameraSettings" hidden>
<section class="text-center">
<h5>Camera/Microphone access needed</h5>
<p class="err" id="permissionError">Permission denied</p>
<p class="info">You must allow camera and microphone access in your browser.</p>
<ul>
<li>Please click on the lock or camera symbol on the side of the URL in the address bar. Here you can grant "always allow" access to your input devices.</li>
<li>Please ensure that you have a camera AND microphone plugged into your computer.</li>
</ul>
<p class="info">Once you've followed these steps, please refresh this page.</p>
<p>If you prefer to continue without allowing camera and microphone access, click on Continue</p>
<p id='browserHelpSetting'></p>
</section>
<section class="action">
<button type="submit" id="helpCameraSettingsFormRefresh">Refresh</button>
<button type="submit" id="helpCameraSettingsFormContinue">Continue</button>
</section>
</form>

View File

@ -0,0 +1,18 @@
<style>
#warningMain {
border-radius: 5px;
height: 100px;
width: 300px;
background-color: red;
text-align: center;
}
#warningMain h2 {
padding: 5px;
}
</style>
<main id="warningMain">
<h2>Warning!</h2>
<p>This world is close to its limit!</p>
</main>

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

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg id="i-fullscreen-exit" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M4 12 L12 12 12 4 M20 4 L20 12 28 12 M4 20 L12 20 12 28 M28 20 L20 20 20 28" />
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg id="i-fullscreen" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M4 12 L4 4 12 4 M20 4 L28 4 28 12 M4 20 L4 28 12 28 M28 20 L28 28 20 28" />
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

Before

Width:  |  Height:  |  Size: 16 KiB

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.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 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

@ -0,0 +1,137 @@
/* A potentially shared website could appear in an iframe in the cowebsite space. */
#cowebsite {
position: fixed;
transition: transform 0.5s;
background-color: white;
&.loading {
background-color: gray;
}
main {
iframe {
width: 100%;
height: 100%;
}
}
aside {
background: gray;
align-items: center;
display: flex;
img {
margin: 3px;
pointer-events: none;
height: 20px;
}
}
.top-right-btn{
position: absolute;
background: none;
border: none;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
img {
height: 25px;
padding: 4px;
border-radius: 3px;
}
img:hover {
background-color: rgba(0,0,0,0.4);
}
}
}
@media (min-aspect-ratio: 1/1) {
#cowebsite {
right: 0;
top: 0;
width: 50%;
height: 100vh;
display: flex;
&.loading {
transform: translateX(90%);
}
&.hidden {
transform: translateX(100%);
}
main {
width: 100%;
}
aside {
width: 30px;
cursor: ew-resize;
img {
cursor: ew-resize;
transform: rotate(90deg);
}
}
.top-right-btn{
left: -6px;
&#cowebsite-close {
top: 0px;
}
&#cowebsite-fullscreen {
top: 25px;
}
}
}
}
@media (max-aspect-ratio: 1/1) {
#main-container {
display: flex;
flex-direction: column-reverse;
}
#cowebsite {
left: 0;
top: 0;
width: 100%;
height: 50%;
display: flex;
flex-direction: column-reverse;
&.loading {
transform: translateY(-90%);
}
&.hidden {
transform: translateY(-100%);
}
main {
height: 100%;
}
aside {
height: 30px;
cursor: ns-resize;
flex-direction: column;
img {
cursor: ns-resize;
}
}
.top-right-btn {
&#cowebsite-close {
right: 0px;
}
&#cowebsite-fullscreen {
right: 25px;
}
}
}
}

2
front/dist/resources/style/index.scss vendored Normal file
View File

@ -0,0 +1,2 @@
@import "cowebsite.scss";
@import "style.css";

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{
@ -104,6 +151,7 @@ video#myCamVideo{
.btn-cam-action {
pointer-events: all;
position: absolute;
bottom: 0px;
right: 0px;
@ -139,21 +187,26 @@ video#myCamVideo{
transition: 280ms;
}
.btn-micro{
pointer-events: auto;
transition: all .3s;
right: 44px;
}
.btn-video{
pointer-events: auto;
transition: all .25s;
right: 134px;
}
.btn-monitor{
pointer-events: auto;
transition: all .2s;
right: 224px;
}
/*.btn-call{
transition: all .1s;
left: 0px;
}*/
.btn-copy{
pointer-events: auto;
transition: all .3s;
right: 44px;
opacity: 1;
}
.btn-cam-action div img{
height: 22px;
width: 30px;
@ -281,35 +334,7 @@ body {
max-height: 25%;
}
#cowebsite {
right: 0;
top: 0;
width: 50%;
height: 100vh;
}
#cowebsite.loading {
transform: translateX(90%);
}
#cowebsite.hidden {
transform: translateX(100%);
}
#cowebsite .close-btn{
position: absolute;
top: 10px;
right: -100px;
background: none;
border: none;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
animation: right .2s ease;
}
#cowebsite .close-btn img{
height: 15px;
right: 15px;
}
#cowebsite:hover .close-btn{
right: 10px;
}
}
@media (max-aspect-ratio: 1/1) {
.game-overlay {
@ -328,19 +353,6 @@ body {
.sidebar > div:hover {
max-width: 25%;
}
#cowebsite {
left: 0;
bottom: 0;
width: 100%;
height: 50%;
}
#cowebsite.loading {
transform: translateY(90%);
}
#cowebsite.hidden {
transform: translateY(100%);
}
}
#game {
@ -348,20 +360,6 @@ body {
position: relative; /* Position relative is needed for the game-overlay. */
}
/* A potentially shared website could appear in an iframe in the cowebsite space. */
#cowebsite {
position: fixed;
transition: transform 0.5s;
}
#cowebsite.loading {
background-color: gray;
}
#cowebsite > iframe {
width: 100%;
height: 100%;
}
.audioplayer:first-child {
display: grid;
grid: 2rem / 4rem 10rem;
@ -374,10 +372,14 @@ body {
justify-content: center;
}
.audioplayer > div {
padding-right: 1.2rem;
}
#audioplayerctrl {
position: fixed;
top: 0;
right: 50%;
right: calc(50% - 120px);
padding: 0.3rem 0.5rem;
color: white;
transition: transform 0.5s;
@ -509,6 +511,7 @@ input[type=range]:focus::-ms-fill-upper {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
/* TODO: DO WE NEED FLEX HERE???? WE WANT A SIDEBAR OF EXACTLY 25% (note: flex useful for direction!!!) */
}
@ -544,6 +547,7 @@ input[type=range]:focus::-ms-fill-upper {
.sidebar {
flex: 0 0 25%;
display: flex;
pointer-events: none;
}
.sidebar > div {
@ -567,10 +571,9 @@ input[type=range]:focus::-ms-fill-upper {
}
.chat-mode {
display: flex;
display: grid;
width: 100%;
flex-wrap: wrap;
align-items: flex-start;
padding: 1%;
@ -586,24 +589,20 @@ input[type=range]:focus::-ms-fill-upper {
.chat-mode > div:hover {
margin: 0%;
}
.chat-mode.one-col > div {
flex-basis: 98%;
.chat-mode.one-col {
grid-template-columns: repeat(1, 1fr);
}
.chat-mode.two-col > div {
flex-basis: 48%;
.chat-mode.two-col {
grid-template-columns: repeat(2, 1fr);
}
.chat-mode.three-col > div {
flex-basis: 31.333333%;
.chat-mode.three-col {
grid-template-columns: repeat(3, 1fr);
}
.chat-mode.four-col > div {
flex-basis: 23%;
}
.chat-mode > div:last-child {
flex-grow: 5;
.chat-mode.four-col {
grid-template-columns: repeat(4, 1fr);
}
/*CONSOLE*/
@ -1072,17 +1071,22 @@ div.modal-report-user{
}
.discussion .messages .message p.body{
color: white;
font-size: 16px;
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
}
.discussion .messages .message p a{
color: white;
}
.discussion .send-message{
position: absolute;
bottom: 45px;
width: 220px;
height: 26px;
margin-bottom: 10px;
}
.discussion .send-message input{
@ -1130,6 +1134,31 @@ div.action p.action-body{
margin-left: calc(50% - 75px);
border-radius: 15px;
}
.popUpElement{
font-family: 'Press Start 2P';
text-align: left;
color: white;
}
.popUpElement div {
font-family: 'Press Start 2P';
font-size: 10px;
background-color: #727678;
}
.popUpElement button {
position: relative;
font-size: 10px;
border-image-repeat: revert;
margin-right: 5px;
}
.popUpElement .buttonContainer {
float: right;
background-color: inherit;
}
@keyframes mymove {
0% {bottom: 40px;}
50% {bottom: 30px;}

View File

@ -9,9 +9,13 @@
"@types/quill": "^1.3.7",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"css-loader": "^5.1.3",
"eslint": "^6.8.0",
"html-webpack-plugin": "^4.3.0",
"jasmine": "^3.5.0",
"mini-css-extract-plugin": "^1.3.9",
"sass": "^1.32.8",
"sass-loader": "10.1.1",
"ts-loader": "^6.2.2",
"ts-node": "^8.10.2",
"typescript": "^3.8.3",
@ -26,9 +30,11 @@
"axios": "^0.21.1",
"generic-type-guard": "^3.2.0",
"google-protobuf": "^3.13.0",
"phaser": "^3.22.0",
"phaser": "3.24.1",
"phaser3-rex-plugins": "^1.1.42",
"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

@ -0,0 +1 @@
iframe_api.d.ts

View File

@ -0,0 +1,27 @@
<h1 align="center">WorkAdventure - IFrame API typings for Typescript</h1>
<p align="center">This package contains Typescript typings for <a href="https://workadventu.re/map-building/scripting">WorkAdventure's map scripting API</a></p>
<hr/>
[WorkAdventure](https://workadventu.re) comes with a scripting API. Using this API, you can add some intelligence to your map.
You use this API by loading an external script directly from WorkAdventure (at https://play.workadventu.re/iframe_api.js), or this script is loaded
for you if you are using the "script" property of a map.
This project contains Typescript typings for the `WA` object provided by this script.
## Usage
This package is only useful if you are using Typescript to script your WorkAdventure maps.
## Download & Installation
```shell
$ npm install @workadventure/iframe-api-typings
```
or
```shell
$ yarn add @workadventure/iframe-api-typings
```

View File

@ -0,0 +1 @@
// This file is voluntarily empty.

View File

@ -0,0 +1,13 @@
{
"name": "@workadventure/iframe-api-typings",
"version": "VERSION_PLACEHOLDER",
"description": "Typescript typings for WorkAdventure iFrame API",
"main": "iframe_api.js",
"types": "iframe_api.d.ts",
"repository": "https://github.com/thecodingmachine/workadventure/",
"author": "David Négrier <d.negrier@thecodingmachine.com>",
"license": "MIT",
"publishConfig": {
"access": "public"
}
}

View File

@ -3,6 +3,7 @@ import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {RoomConnection} from "../Connexion/RoomConnection";
import {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {ADMIN_URL} from "../Enum/EnvironmentVariable";
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
export const CLASS_CONSOLE_MESSAGE = 'main-console';
export const INPUT_CONSOLE_MESSAGE = 'input-send-text';
@ -10,13 +11,16 @@ export const UPLOAD_CONSOLE_MESSAGE = 'input-upload-music';
export const INPUT_TYPE_CONSOLE = 'input-type';
export const VIDEO_QUALITY_SELECT = 'select-video-quality';
export const AUDIO_TYPE = 'audio';
export const MESSAGE_TYPE = 'message';
export const AUDIO_TYPE = AdminMessageEventTypes.audio;
export const MESSAGE_TYPE = AdminMessageEventTypes.admin;
interface EventTargetFiles extends EventTarget {
files: Array<File>;
}
/**
* @deprecated
*/
export class ConsoleGlobalMessageManager {
private readonly divMainConsole: HTMLDivElement;
@ -140,7 +144,7 @@ export class ConsoleGlobalMessageManager {
const div = document.createElement('div');
div.id = INPUT_CONSOLE_MESSAGE
const buttonSend = document.createElement('button');
buttonSend.innerText = 'Envoyer';
buttonSend.innerText = 'Send';
buttonSend.classList.add('btn');
buttonSend.addEventListener('click', (event: MouseEvent) => {
this.sendMessage();
@ -242,7 +246,7 @@ export class ConsoleGlobalMessageManager {
div.appendChild(input);
const buttonSend = document.createElement('button');
buttonSend.innerText = 'Envoyer';
buttonSend.innerText = 'Send';
buttonSend.classList.add('btn');
buttonSend.addEventListener('click', (event: MouseEvent) => {
this.sendMessage();
@ -332,7 +336,7 @@ export class ConsoleGlobalMessageManager {
}
active(){
this.userInputManager.clearAllInputKeyboard();
this.userInputManager.disableControls();
this.divMainConsole.style.top = '0';
this.activeConsole = true;
}
@ -372,23 +376,6 @@ export class ConsoleGlobalMessageManager {
this.buttonSendMainConsole.classList.remove('active');
}
/*activeSettingConsole(){
this.activeSetting = true;
if(this.activeMessage){
this.disabledSettingConsole();
}
this.active();
this.divSettingConsole.classList.add('active');
//this.buttonSettingsMainConsole.classList.add('active');
}
disabledSettingConsole(){
this.activeSetting = false;
this.disabled();
this.divSettingConsole.classList.remove('active');
//this.buttonSettingsMainConsole.classList.remove('active');
}*/
private getSectionId(id: string) : string {
return `section-${id}`;
}

View File

@ -1,6 +1,6 @@
import {HtmlUtils} from "./../WebRtc/HtmlUtils";
import {AUDIO_TYPE, MESSAGE_TYPE} from "./ConsoleGlobalMessageManager";
import {API_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "../Connexion/RoomConnection";
import {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";

View File

@ -77,8 +77,10 @@ export class TypeMessageExt implements TypeMessageInterface{
}
}
}
export class Ban extends TypeMessageExt {
}
export class Message extends TypeMessageExt {}
export class Ban extends TypeMessageExt {}
export class Banned extends TypeMessageExt {
showMessage(message: string){

View File

@ -1,28 +1,29 @@
import {RoomConnection} from "../Connexion/RoomConnection";
import * as TypeMessages from "./TypeMessage";
import {Banned} from "./TypeMessage";
import {adminMessagesService} from "../Connexion/AdminMessagesService";
export interface TypeMessageInterface {
showMessage(message: string): void;
}
export class UserMessageManager {
class UserMessageManager {
typeMessages: Map<string, TypeMessageInterface> = new Map<string, TypeMessageInterface>();
receiveBannedMessageListener!: Function;
constructor(private Connection: RoomConnection) {
constructor() {
const valueTypeMessageTab = Object.values(TypeMessages);
Object.keys(TypeMessages).forEach((value: string, index: number) => {
const typeMessageInstance: TypeMessageInterface = (new valueTypeMessageTab[index]() as TypeMessageInterface);
this.typeMessages.set(value.toLowerCase(), typeMessageInstance);
});
this.initialise();
}
initialise() {
//receive signal to show message
this.Connection.receiveUserMessage((type: string, message: string) => {
this.showMessage(type, message);
});
adminMessagesService.messageStream.subscribe((event) => {
const typeMessage = this.showMessage(event.type, event.text);
if(typeMessage instanceof Banned) {
this.receiveBannedMessageListener();
}
})
}
showMessage(type: string, message: string) {
@ -32,5 +33,11 @@ export class UserMessageManager {
return;
}
classTypeMessage.showMessage(message);
return classTypeMessage;
}
setReceiveBanListener(callback: Function){
this.receiveBannedMessageListener = callback;
}
}
export const userMessageManager = new UserMessageManager()

View File

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isButtonClickedEvent =
new tg.IsInterface().withProperties({
popupId: tg.isNumber,
buttonId: tg.isNumber,
}).get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/
export type ButtonClickedEvent = tg.GuardedType<typeof isButtonClickedEvent>;

View File

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isChatEvent =
new tg.IsInterface().withProperties({
message: tg.isString,
author: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;

View File

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isClosePopupEvent =
new tg.IsInterface().withProperties({
popupId: tg.isNumber,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type ClosePopupEvent = tg.GuardedType<typeof isClosePopupEvent>;

View File

@ -0,0 +1,10 @@
import * as tg from "generic-type-guard";
export const isEnterLeaveEvent =
new tg.IsInterface().withProperties({
name: tg.isString,
}).get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/
export type EnterLeaveEvent = tg.GuardedType<typeof isEnterLeaveEvent>;

View File

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isGoToPageEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type GoToPageEvent = tg.GuardedType<typeof isGoToPageEvent>;

View File

@ -0,0 +1,7 @@
export interface IframeEvent {
type: string;
data: unknown;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeEventWrapper = (event: any): event is IframeEvent => typeof event.type === 'string';

View File

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

View File

@ -0,0 +1,20 @@
import * as tg from "generic-type-guard";
const isButtonDescriptor =
new tg.IsInterface().withProperties({
label: tg.isString,
className: tg.isOptional(tg.isString)
}).get();
export const isOpenPopupEvent =
new tg.IsInterface().withProperties({
popupId: tg.isNumber,
targetObject: tg.isString,
message: tg.isString,
buttons: tg.isArray(isButtonDescriptor)
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type OpenPopupEvent = tg.GuardedType<typeof isOpenPopupEvent>;

View File

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isOpenTabEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type OpenTabEvent = tg.GuardedType<typeof isOpenTabEvent>;

View File

@ -0,0 +1,10 @@
import * as tg from "generic-type-guard";
export const isUserInputChatEvent =
new tg.IsInterface().withProperties({
message: tg.isString,
}).get();
/**
* A message sent from the game to the iFrame when a user types a message in the chat.
*/
export type UserInputChatEvent = tg.GuardedType<typeof isUserInputChatEvent>;

View File

@ -0,0 +1,238 @@
import {Subject} from "rxjs";
import {ChatEvent, isChatEvent} from "./Events/ChatEvent";
import {IframeEvent, isIframeEventWrapper} from "./Events/IframeEvent";
import {UserInputChatEvent} from "./Events/UserInputChatEvent";
import * as crypto from "crypto";
import {HtmlUtils} from "../WebRtc/HtmlUtils";
import {EnterLeaveEvent} from "./Events/EnterLeaveEvent";
import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent";
import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent";
import {ButtonClickedEvent} from "./Events/ButtonClickedEvent";
import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent";
import {scriptUtils} from "./ScriptUtils";
import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent";
import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent";
/**
* Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes.
*/
class IframeListener {
private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable();
private readonly _openPopupStream: Subject<OpenPopupEvent> = new Subject();
public readonly openPopupStream = this._openPopupStream.asObservable();
private readonly _openTabStream: Subject<OpenTabEvent> = new Subject();
public readonly openTabStream = this._openTabStream.asObservable();
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
public readonly goToPageStream = this._goToPageStream.asObservable();
private readonly _openCoWebSiteStream: Subject<OpenCoWebSiteEvent> = new Subject();
public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable();
private readonly _closeCoWebSiteStream: Subject<void> = new Subject();
public readonly closeCoWebSiteStream = this._closeCoWebSiteStream.asObservable();
private readonly _disablePlayerControlStream: Subject<void> = new Subject();
public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable();
private readonly _enablePlayerControlStream: Subject<void> = new Subject();
public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable();
private readonly _closePopupStream: Subject<ClosePopupEvent> = new Subject();
public readonly closePopupStream = this._closePopupStream.asObservable();
private readonly _displayBubbleStream: Subject<void> = new Subject();
public readonly displayBubbleStream = this._displayBubbleStream.asObservable();
private readonly _removeBubbleStream: Subject<void> = new Subject();
public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>();
private readonly scripts = new Map<string, HTMLIFrameElement>();
init() {
window.addEventListener("message", (message) => {
// Do we trust the sender of this message?
// Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let found = false;
for (const iframe of this.iframes) {
if (iframe.contentWindow === message.source) {
found = true;
break;
}
}
if (!found) {
return;
}
const payload = message.data;
if (isIframeEventWrapper(payload)) {
if (payload.type === 'chat' && isChatEvent(payload.data)) {
this._chatStream.next(payload.data);
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
this._closePopupStream.next(payload.data);
}
else if(payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
scriptUtils.openTab(payload.data.url);
}
else if(payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
}
else if(payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(payload.data.url);
}
else if(payload.type === 'closeCoWebSite') {
scriptUtils.closeCoWebSite();
}
else if (payload.type === 'disablePlayerControl'){
this._disablePlayerControlStream.next();
}
else if (payload.type === 'restorePlayerControl'){
this._enablePlayerControlStream.next();
}
else if (payload.type === 'displayBubble'){
this._displayBubbleStream.next();
}
else if (payload.type === 'removeBubble'){
this._removeBubbleStream.next();
}
}
}, false);
}
/**
* Allows the passed iFrame to send/receive messages via the API.
*/
registerIframe(iframe: HTMLIFrameElement): void {
this.iframes.add(iframe);
}
unregisterIframe(iframe: HTMLIFrameElement): void {
this.iframes.delete(iframe);
}
registerScript(scriptUrl: string): void {
console.log('Loading map related script at ', scriptUrl)
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
// Using external iframe mode (
const iframe = document.createElement('iframe');
iframe.id = this.getIFrameId(scriptUrl);
iframe.style.display = 'none';
iframe.src = '/iframe.html?script='+encodeURIComponent(scriptUrl);
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation');
document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
} else {
// production code
const iframe = document.createElement('iframe');
iframe.id = this.getIFrameId(scriptUrl);
iframe.style.display = 'none';
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation');
const html = '<!doctype html>\n' +
'\n' +
'<html lang="en">\n' +
'<head>\n' +
'<script src="'+window.location.protocol+'//'+window.location.host+'/iframe_api.js" ></script>\n' +
'<script src="'+scriptUrl+'" ></script>\n' +
'</head>\n' +
'</html>\n';
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = html;
document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
}
}
private getIFrameId(scriptUrl: string): string {
return 'script'+crypto.createHash('md5').update(scriptUrl).digest("hex");
}
unregisterScript(scriptUrl: string): void {
const iFrameId = this.getIFrameId(scriptUrl);
const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId);
if (!iframe) {
throw new Error('Unknown iframe for script "'+scriptUrl+'"');
}
this.unregisterIframe(iframe);
iframe.remove();
this.scripts.delete(scriptUrl);
}
sendUserInputChat(message: string) {
this.postMessage({
'type': 'userInputChat',
'data': {
'message': message,
} as UserInputChatEvent
});
}
sendEnterEvent(name: string) {
this.postMessage({
'type': 'enterEvent',
'data': {
"name": name
} as EnterLeaveEvent
});
}
sendLeaveEvent(name: string) {
this.postMessage({
'type': 'leaveEvent',
'data': {
"name": name
} as EnterLeaveEvent
});
}
sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({
'type': 'buttonClickedEvent',
'data': {
popupId,
buttonId
} as ButtonClickedEvent
});
}
/**
* Sends the message... to all allowed iframes.
*/
private postMessage(message: IframeEvent) {
for (const iframe of this.iframes) {
iframe.contentWindow?.postMessage(message, '*');
}
}
}
export const iframeListener = new IframeListener();

View File

@ -0,0 +1,23 @@
import {coWebsiteManager} from "../WebRtc/CoWebsiteManager";
class ScriptUtils {
public openTab(url : string){
window.open(url);
}
public goToPage(url : string){
window.location.href = url;
}
public openCoWebsite(url : string){
coWebsiteManager.loadCoWebsite(url,url);
}
public closeCoWebSite(){
coWebsiteManager.closeCoWebsite();
}
}
export const scriptUtils = new ScriptUtils();

View File

@ -0,0 +1,34 @@
import {Subject} from "rxjs";
import {BanUserMessage, SendUserMessage} from "../Messages/generated/messages_pb";
export enum AdminMessageEventTypes {
admin = 'message',
audio = 'audio',
ban = 'ban',
}
interface AdminMessageEvent {
type: AdminMessageEventTypes,
text: string;
//todo add optional properties for other event types
}
//this class is designed to easily allow communication between the RoomConnection objects (that receive the message)
//and the various objects that may render the message on screen
class AdminMessagesService {
private _messageStream: Subject<AdminMessageEvent> = new Subject();
public messageStream = this._messageStream.asObservable();
constructor() {
this.messageStream.subscribe((event) => console.log('message', event))
}
onSendusermessage(message: SendUserMessage|BanUserMessage) {
this._messageStream.next({
type: message.getType() as unknown as AdminMessageEventTypes,
text: message.getMessage(),
})
}
}
export const adminMessagesService = new AdminMessagesService();

View File

@ -1,5 +1,5 @@
import Axios from "axios";
import {API_URL} from "../Enum/EnvironmentVariable";
import {PUSHER_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "./RoomConnection";
import {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
import {GameConnexionTypes, urlManager} from "../Url/UrlManager";
@ -7,12 +7,24 @@ import {localUserStore} from "./LocalUserStore";
import {LocalUser} from "./LocalUser";
import {Room} from "./Room";
const URL_ROOM_STARTED = '/Floor0/floor0.json';
class ConnectionManager {
private localUser!:LocalUser;
private connexionType?: GameConnexionTypes
private reconnectingTimeout: NodeJS.Timeout|null = null;
private _unloading:boolean = false;
get unloading () {
return this._unloading;
}
constructor() {
window.addEventListener('beforeunload', () => {
this._unloading = true;
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout)
})
}
/**
* Tries to login to the node server and return the starting map url to be loaded
*/
@ -22,16 +34,16 @@ class ConnectionManager {
this.connexionType = connexionType;
if(connexionType === GameConnexionTypes.register) {
const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${API_URL}/register`, {organizationMemberToken}).then(res => res.data);
const data = await Axios.post(`${PUSHER_URL}/register`, {organizationMemberToken}).then(res => res.data);
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
localUserStore.saveUser(this.localUser);
const organizationSlug = data.organizationSlug;
const worldSlug = data.worldSlug;
const roomSlug = data.roomSlug;
urlManager.editUrlForRoom(roomSlug, organizationSlug, worldSlug);
const room = new Room(window.location.pathname + window.location.hash);
const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.search + window.location.hash);
urlManager.pushRoomIdToUrl(room);
return Promise.resolve(room);
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
const localUser = localUserStore.getLocalUser();
@ -50,23 +62,22 @@ class ConnectionManager {
}
let roomId: string
if (connexionType === GameConnexionTypes.empty) {
const defaultMapUrl = window.location.host.replace('play.', 'maps.') + URL_ROOM_STARTED;
roomId = urlManager.editUrlForRoom(defaultMapUrl, null, null);
roomId = START_ROOM_URL;
} else {
roomId = window.location.pathname + window.location.hash;
roomId = window.location.pathname + window.location.search + window.location.hash;
}
return Promise.resolve(new Room(roomId));
}
return Promise.reject('Invalid URL');
return Promise.reject(new Error('Invalid URL'));
}
private async verifyToken(token: string): Promise<void> {
await Axios.get(`${API_URL}/verify`, {params: {token}});
await Axios.get(`${PUSHER_URL}/verify`, {params: {token}});
}
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${API_URL}/anonymLogin`).then(res => res.data);
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then(res => res.data);
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
if (!isBenchmark) { // In benchmark, we don't have a local storage.
localUserStore.saveUser(this.localUser);
@ -77,9 +88,9 @@ class ConnectionManager {
this.localUser = new LocalUser('', 'test', []);
}
public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface): Promise<OnConnectInterface> {
public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
return new Promise<OnConnectInterface>((resolve, reject) => {
const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport);
const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport, companion);
connection.onConnectError((error: object) => {
console.log('An error occurred while connecting to socket server. Retrying');
reject(error);
@ -97,10 +108,10 @@ class ConnectionManager {
}).catch((err) => {
// Let's retry in 4-6 seconds
return new Promise<OnConnectInterface>((resolve, reject) => {
setTimeout(() => {
this.reconnectingTimeout = setTimeout(() => {
//todo: allow a way to break recursion?
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
this.connectToRoomSocket(roomId, name, characterLayers, position, viewport).then((connection) => resolve(connection));
this.connectToRoomSocket(roomId, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
}, 4000 + Math.floor(Math.random() * 2000) );
});
});

View File

@ -1,8 +1,8 @@
import {PlayerAnimationNames} from "../Phaser/Player/Animation";
import {PlayerAnimationDirections} from "../Phaser/Player/Animation";
import {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
import {SignalData} from "simple-peer";
import {BodyResourceDescriptionInterface} from "../Phaser/Entity/body_character";
import {RoomConnection} from "./RoomConnection";
import {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
export enum EventMessage{
CONNECT = "connect",
@ -42,19 +42,12 @@ export interface PointInterface {
moving: boolean;
}
export class Point implements PointInterface{
constructor(public x : number, public y : number, public direction : string = PlayerAnimationNames.WalkDown, public moving : boolean = false) {
if(x === null || y === null){
throw Error("position x and y cannot be null");
}
}
}
export interface MessageUserPositionInterface {
userId: number;
name: string;
characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface;
companion: string|null;
}
export interface MessageUserMovedInterface {
@ -66,7 +59,8 @@ export interface MessageUserJoined {
userId: number;
name: string;
characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface
position: PointInterface;
companion: string|null;
}
export interface PositionInterface {
@ -80,23 +74,15 @@ export interface GroupCreatedUpdatedMessageInterface {
groupSize: number
}
export interface WebRtcStartMessageInterface {
roomId: string,
clients: UserSimplePeerInterface[]
}
export interface WebRtcDisconnectMessageInterface {
userId: number
}
export interface WebRtcSignalSentMessageInterface {
receiverId: number,
signal: SignalData
}
export interface WebRtcSignalReceivedMessageInterface {
userId: number,
signal: SignalData
signal: SignalData,
webRtcUser: string | undefined,
webRtcPassword: string | undefined
}
export interface StartMapInterface {
@ -111,11 +97,6 @@ export interface ViewportInterface {
bottom: number,
}
export interface BatchedMessageInterface {
event: string,
payload: unknown
}
export interface ItemEventMessageInterface {
itemId: number,
event: string,

View File

@ -1,3 +1,5 @@
import {MAX_USERNAME_LENGTH} from "../Enum/EnvironmentVariable";
export interface CharacterTexture {
id: number,
level: number,
@ -5,6 +7,23 @@ export interface CharacterTexture {
rights: string
}
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
export function isUserNameValid(value: string): boolean {
const regexp = new RegExp('^[A-Za-z]{1,'+maxUserNameLength+'}$');
return regexp.test(value);
}
export function areCharacterLayersValid(value: string[] | null): boolean {
if (!value || !value.length) return false;
for (let i = 0; i < value.length; i++) {
if (/^\w+$/.exec(value[i]) === null) {
return false;
}
}
return true;
}
export class LocalUser {
constructor(public readonly uuid:string, public readonly jwtToken: string, public readonly textures: CharacterTexture[]) {
}

View File

@ -1,12 +1,18 @@
import {LocalUser} from "./LocalUser";
import {areCharacterLayersValid, isUserNameValid, LocalUser} from "./LocalUser";
const playerNameKey = 'playerName';
const selectedPlayerKey = 'selectedPlayer';
const customCursorPositionKey = 'customCursorPosition';
const characterLayersKey = 'characterLayers';
const companionKey = 'companion';
const gameQualityKey = 'gameQuality';
const videoQualityKey = 'videoQuality';
const audioPlayerVolumeKey = 'audioVolume';
const audioPlayerMuteKey = 'audioMute';
const helpCameraSettingsShown = 'helpCameraSettingsShown';
const fullscreenKey = 'fullscreen';
//todo: add localstorage fallback
class LocalUserStore {
saveUser(localUser: LocalUser) {
localStorage.setItem('localUser', JSON.stringify(localUser));
}
@ -16,46 +22,92 @@ class LocalUserStore {
}
setName(name:string): void {
window.localStorage.setItem('playerName', name);
localStorage.setItem(playerNameKey, name);
}
getName(): string {
return window.localStorage.getItem('playerName') ?? '';
getName(): string|null {
const value = localStorage.getItem(playerNameKey) || '';
return isUserNameValid(value) ? value : null;
}
setPlayerCharacterIndex(playerCharacterIndex: number): void {
window.localStorage.setItem('selectedPlayer', ''+playerCharacterIndex);
localStorage.setItem(selectedPlayerKey, ''+playerCharacterIndex);
}
getPlayerCharacterIndex(): number {
return parseInt(window.localStorage.getItem('selectedPlayer') || '');
return parseInt(localStorage.getItem(selectedPlayerKey) || '');
}
setCustomCursorPosition(activeRow:number, selectedLayers: number[]): void {
window.localStorage.setItem('customCursorPosition', JSON.stringify({activeRow, selectedLayers}));
localStorage.setItem(customCursorPositionKey, JSON.stringify({activeRow, selectedLayers}));
}
getCustomCursorPosition(): {activeRow:number, selectedLayers:number[]}|null {
return JSON.parse(window.localStorage.getItem('customCursorPosition') || "null");
return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null");
}
setCharacterLayers(layers: string[]): void {
window.localStorage.setItem(characterLayersKey, JSON.stringify(layers));
localStorage.setItem(characterLayersKey, JSON.stringify(layers));
}
getCharacterLayers(): string[]|null {
return JSON.parse(window.localStorage.getItem(characterLayersKey) || "null");
const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null");
return areCharacterLayersValid(value) ? value : null;
}
getGameQualityValue(): number {
return parseInt(window.localStorage.getItem(gameQualityKey) || '') || 60;
setCompanion(companion: string|null): void {
return localStorage.setItem(companionKey, JSON.stringify(companion));
}
getCompanion(): string|null {
const companion = JSON.parse(localStorage.getItem(companionKey) || "null");
if (typeof companion !== "string" || companion === "") {
return null;
}
return companion;
}
wasCompanionSet(): boolean {
return localStorage.getItem(companionKey) ? true : false;
}
setGameQualityValue(value: number): void {
localStorage.setItem(gameQualityKey, '' + value);
}
getVideoQualityValue(): number {
return parseInt(window.localStorage.getItem(videoQualityKey) || '') || 20;
getGameQualityValue(): number {
return parseInt(localStorage.getItem(gameQualityKey) || '60');
}
setVideoQualityValue(value: number): void {
localStorage.setItem(videoQualityKey, '' + value);
}
getVideoQualityValue(): number {
return parseInt(localStorage.getItem(videoQualityKey) || '20');
}
setAudioPlayerVolume(value: number): void {
localStorage.setItem(audioPlayerVolumeKey, '' + value);
}
getAudioPlayerVolume(): number {
return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || '1');
}
setAudioPlayerMuted(value: boolean): void {
localStorage.setItem(audioPlayerMuteKey, value.toString());
}
getAudioPlayerMuted(): boolean {
return localStorage.getItem(audioPlayerMuteKey) === 'true';
}
setHelpCameraSettingsShown(): void {
localStorage.setItem(helpCameraSettingsShown, '1');
}
getHelpCameraSettingsShown(): boolean {
return localStorage.getItem(helpCameraSettingsShown) === '1';
}
setFullscreen(value: boolean): void {
localStorage.setItem(fullscreenKey, value.toString());
}
getFullscreen(): boolean {
return localStorage.getItem(fullscreenKey) === 'true';
}
}
export const localUserStore = new LocalUserStore();

View File

@ -1,29 +1,30 @@
import Axios from "axios";
import {API_URL} from "../Enum/EnvironmentVariable";
import {PUSHER_URL} from "../Enum/EnvironmentVariable";
export class Room {
public readonly id: string;
public readonly isPublic: boolean;
private mapUrl: string|undefined;
private instance: string|undefined;
private _search: URLSearchParams;
constructor(id: string) {
if (id.startsWith('/')) {
id = id.substr(1);
const url = new URL(id, 'https://example.com');
this.id = url.pathname;
if (this.id.startsWith('/')) {
this.id = this.id.substr(1);
}
this.id = id;
if (id.startsWith('_/')) {
if (this.id.startsWith('_/')) {
this.isPublic = true;
} else if (id.startsWith('@/')) {
} else if (this.id.startsWith('@/')) {
this.isPublic = false;
} else {
throw new Error('Invalid room ID');
}
const indexOfHash = this.id.indexOf('#');
if (indexOfHash !== -1) {
this.id = this.id.substr(0, indexOfHash);
}
this._search = new URLSearchParams(url.search);
}
public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): {roomId: string, hash: string} {
@ -66,14 +67,15 @@ export class Room {
// We have a private ID, we need to query the map URL from the server.
const urlParts = this.parsePrivateUrl(this.id);
Axios.get(`${API_URL}/map`, {
Axios.get(`${PUSHER_URL}/map`, {
params: urlParts
}).then(({data}) => {
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl);
resolve(data.mapUrl);
return;
}).catch((reason) => {
reject(reason);
});
}
});
}
@ -116,4 +118,17 @@ export class Room {
}
return results;
}
public isDisconnected(): boolean
{
const alone = this._search.get('alone');
if (alone && alone !== '0' && alone.toLowerCase() !== 'false') {
return true;
}
return false;
}
public get search(): URLSearchParams {
return this._search;
}
}

View File

@ -1,4 +1,4 @@
import {API_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import Axios from "axios";
import {
BatchMessage,
@ -27,7 +27,7 @@ import {
SendJitsiJwtMessage,
CharacterLayerMessage,
PingMessage,
SendUserMessage
SendUserMessage, BanUserMessage
} from "../Messages/generated/messages_pb"
import {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
@ -41,7 +41,11 @@ import {
ViewportInterface, WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface,
} from "./ConnexionModels";
import {BodyResourceDescriptionInterface} from "../Phaser/Entity/body_character";
import {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
import {adminMessagesService} from "./AdminMessagesService";
import {worldFullMessageStream} from "./WorldFullMessageStream";
import {worldFullWarningStream} from "./WorldFullWarningStream";
import {connectionManager} from "./ConnectionManager";
const manualPingDelay = 20000;
@ -62,9 +66,13 @@ export class RoomConnection implements RoomConnection {
* @param token A JWT token containing the UUID of the user
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]"
*/
public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) {
let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://');
url += '/room';
public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null) {
let url = new URL(PUSHER_URL, window.location.toString()).toString();
url = url.replace('http://', 'ws://').replace('https://', 'wss://');
if (!url.endsWith('/')) {
url += '/';
}
url += 'room';
url += '?roomId='+(roomId ?encodeURIComponent(roomId):'');
url += '&token='+(token ?encodeURIComponent(token):'');
url += '&name='+encodeURIComponent(name);
@ -78,6 +86,10 @@ export class RoomConnection implements RoomConnection {
url += '&left='+Math.floor(viewport.left);
url += '&right='+Math.floor(viewport.right);
if (typeof companion === 'string') {
url += '&companion='+encodeURIComponent(companion);
}
if (RoomConnection.websocketFactory) {
this.socket = RoomConnection.websocketFactory(url);
} else {
@ -100,7 +112,7 @@ export class RoomConnection implements RoomConnection {
}
// If we are not connected yet (if a JoinRoomMessage was not sent), we need to retry.
if (this.userId === null) {
if (this.userId === null && !this.closed) {
this.dispatch(EventMessage.CONNECTING_ERROR, event);
}
});
@ -140,8 +152,6 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasRoomjoinedmessage()) {
const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage;
//const users: Array<MessageUserJoined> = roomJoinedMessage.getUserList().map(this.toMessageUserJoined.bind(this));
//const groups: Array<GroupCreatedUpdatedMessageInterface> = roomJoinedMessage.getGroupList().map(this.toGroupCreatedUpdatedMessage.bind(this));
const items: { [itemId: number] : unknown } = {};
for (const item of roomJoinedMessage.getItemList()) {
items[item.getItemid()] = JSON.parse(item.getStatejson());
@ -150,24 +160,15 @@ export class RoomConnection implements RoomConnection {
this.userId = roomJoinedMessage.getCurrentuserid();
this.tags = roomJoinedMessage.getTagList();
//console.log('Dispatching CONNECT')
this.dispatch(EventMessage.CONNECT, {
connection: this,
room: {
//users,
//groups,
items
} as RoomJoinedMessageInterface
});
/*console.log('Dispatching START_ROOM')
this.dispatch(EventMessage.START_ROOM, {
//users,
//groups,
items
});*/
} else if (message.hasErrormessage()) {
console.error(EventMessage.MESSAGE_ERROR, message.getErrormessage()?.getMessage());
} else if (message.hasWorldfullmessage()) {
worldFullMessageStream.onMessage();
this.closed = true;
} else if (message.hasWebrtcsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
} else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
@ -185,7 +186,13 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasSendjitsijwtmessage()) {
this.dispatch(EventMessage.START_JITSI_ROOM, message.getSendjitsijwtmessage());
} else if (message.hasSendusermessage()) {
this.dispatch(EventMessage.USER_MESSAGE, message.getSendusermessage());
adminMessagesService.onSendusermessage(message.getSendusermessage() as SendUserMessage);
} else if (message.hasBanusermessage()) {
adminMessagesService.onSendusermessage(message.getSendusermessage() as BanUserMessage);
} else if (message.hasWorldfullwarningmessage()) {
worldFullWarningStream.onMessage();
} else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed.
} else {
throw new Error('Unknown message received');
}
@ -319,11 +326,14 @@ export class RoomConnection implements RoomConnection {
}
})
const companion = message.getCompanion();
return {
userId: message.getUserid(),
name: message.getName(),
characterLayers,
position: ProtobufClientUtils.toPointInterface(position)
position: ProtobufClientUtils.toPointInterface(position),
companion: companion ? companion.getName() : null
}
}
@ -385,9 +395,6 @@ export class RoomConnection implements RoomConnection {
this.socket.addEventListener('error', callback)
}
/*public onConnect(callback: (e: Event) => void): void {
this.socket.addEventListener('open', callback)
}*/
public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
//this.socket.addEventListener('open', callback)
this.onMessage(EventMessage.CONNECT, callback);
@ -427,7 +434,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 +445,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,14 +456,16 @@ 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,
});
});
}
public onServerDisconnected(callback: (event: CloseEvent) => void): void {
public onServerDisconnected(callback: () => void): void {
this.socket.addEventListener('close', (event) => {
if (this.closed === true) {
if (this.closed === true || connectionManager.unloading) {
return;
}
console.log('Socket closed with code '+event.code+". Reason: "+event.reason);
@ -460,11 +473,12 @@ export class RoomConnection implements RoomConnection {
// Normal closure case
return;
}
callback(event);
callback();
});
}
public getUserId(): number|null {
public getUserId(): number {
if (this.userId === null) throw 'UserId cannot be null!'
return this.userId;
}
@ -532,12 +546,6 @@ export class RoomConnection implements RoomConnection {
});
}
public receiveUserMessage(callback: (type: string, message: string) => void) {
return this.onMessage(EventMessage.USER_MESSAGE, (message: SendUserMessage) => {
callback(message.getType(), message.getMessage());
});
}
public emitGlobalMessage(message: PlayGlobalMessageInterface){
const playGlobalMessage = new PlayGlobalMessage();
playGlobalMessage.setId(message.id);

View File

@ -0,0 +1,14 @@
import {Subject} from "rxjs";
class WorldFullMessageStream {
private _stream:Subject<void> = new Subject();
public stream = this._stream.asObservable();
onMessage() {
this._stream.next();
}
}
export const worldFullMessageStream = new WorldFullMessageStream();

View File

@ -0,0 +1,14 @@
import {Subject} from "rxjs";
class WorldFullWarningStream {
private _stream:Subject<void> = new Subject();
public stream = this._stream.asObservable();
onMessage() {
this._stream.next();
}
}
export const worldFullWarningStream = new WorldFullWarningStream();

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

@ -1,26 +1,32 @@
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
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 || "admin.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 START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json';
// For compatibility reasons with older versions, API_URL is the old host name of PUSHER_URL
const PUSHER_URL = process.env.PUSHER_URL || (process.env.API_URL ? '//'+process.env.API_URL : "//pusher.workadventure.localhost");
const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost';
const ADMIN_URL = process.env.ADMIN_URL || "//workadventure.localhost";
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;
const ZOOM_LEVEL = 1/*3/4*/;
const POSITION_DELAY = 200; // Wait 200ms between sending position events
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8;
export {
DEBUG_MODE,
API_URL,
START_ROOM_URL,
PUSHER_URL,
UPLOADER_URL,
ADMIN_URL,
RESOLUTION,
ZOOM_LEVEL,
POSITION_DELAY,
MAX_EXTRAPOLATION_TIME,
STUN_SERVER,
TURN_SERVER,
TURN_USER,
TURN_PASSWORD,

View File

@ -0,0 +1,221 @@
import Sprite = Phaser.GameObjects.Sprite;
import Container = Phaser.GameObjects.Container;
import { lazyLoadCompanionResource } from "./CompanionTexturesLoadingManager";
import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
export interface CompanionStatus {
x: number;
y: number;
name: string;
moving: boolean;
direction: PlayerAnimationDirections;
}
export class Companion extends Container {
public sprites: Map<string, Sprite>;
private delta: number;
private invisible: boolean;
private updateListener: Function;
private target: { x: number, y: number, direction: PlayerAnimationDirections };
private companionName: string;
private direction: PlayerAnimationDirections;
private animationType: PlayerAnimationTypes;
constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise<string>) {
super(scene, x + 14, y + 4);
this.sprites = new Map<string, Sprite>();
this.delta = 0;
this.invisible = true;
this.target = { x, y, direction: PlayerAnimationDirections.Down };
this.direction = PlayerAnimationDirections.Down;
this.animationType = PlayerAnimationTypes.Idle;
this.companionName = name;
texturePromise.then(resource => {
this.addResource(resource);
this.invisible = false;
})
this.scene.physics.world.enableBody(this);
this.getBody().setImmovable(true);
this.getBody().setCollideWorldBounds(false);
this.setSize(16, 16);
this.getBody().setSize(16, 16);
this.getBody().setOffset(0, 8);
this.setDepth(-1);
this.updateListener = this.step.bind(this);
this.scene.events.addListener('update', this.updateListener);
this.scene.add.existing(this);
}
public setTarget(x: number, y: number, direction: PlayerAnimationDirections) {
this.target = { x, y: y + 4, direction };
}
public step(time: number, delta: number) {
if (typeof this.target === 'undefined') return;
this.delta += delta;
if (this.delta < 128) {
return;
}
this.delta = 0;
const xDist = this.target.x - this.x;
const yDist = this.target.y - this.y;
const distance = Math.pow(xDist, 2) + Math.pow(yDist, 2);
if (distance < 650) {
this.animationType = PlayerAnimationTypes.Idle;
this.direction = this.target.direction;
this.getBody().stop();
} else {
this.animationType = PlayerAnimationTypes.Walk;
const xDir = xDist / Math.max(Math.abs(xDist), 1);
const yDir = yDist / Math.max(Math.abs(yDist), 1);
const speed = 256;
this.getBody().setVelocity(Math.min(Math.abs(xDist * 2.5), speed) * xDir, Math.min(Math.abs(yDist * 2.5), speed) * yDir);
if (Math.abs(xDist) > Math.abs(yDist)) {
if (xDist < 0) {
this.direction = PlayerAnimationDirections.Left;
} else {
this.direction = PlayerAnimationDirections.Right;
}
} else {
if (yDist < 0) {
this.direction = PlayerAnimationDirections.Up;
} else {
this.direction = PlayerAnimationDirections.Down;
}
}
}
this.setDepth(this.y);
this.playAnimation(this.direction, this.animationType);
}
public getStatus(): CompanionStatus {
const { x, y, direction, animationType, companionName } = this;
return {
x,
y,
direction,
moving: animationType === PlayerAnimationTypes.Walk,
name: companionName
}
}
private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void {
if (this.invisible) return;
for (const [resource, sprite] of this.sprites.entries()) {
sprite.play(`${resource}-${direction}-${type}`, true);
}
}
private addResource(resource: string, frame?: string | number): void {
const sprite = new Sprite(this.scene, 0, 0, resource, frame);
this.add(sprite);
this.getAnimations(resource).forEach(animation => {
this.scene.anims.create(animation);
});
this.scene.sys.updateList.add(sprite);
this.sprites.set(resource, sprite);
}
private getAnimations(resource: string): Phaser.Types.Animations.Animation[] {
return [
{
key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [1]}),
frameRate: 10,
repeat: 1
},
{
key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [4]}),
frameRate: 10,
repeat: 1
},
{
key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [7]}),
frameRate: 10,
repeat: 1
},
{
key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [10]}),
frameRate: 10,
repeat: 1
},
{
key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [0, 1, 2]}),
frameRate: 15,
repeat: -1
},
{
key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [3, 4, 5]}),
frameRate: 15,
repeat: -1
},
{
key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [6, 7, 8]}),
frameRate: 15,
repeat: -1
},
{
key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`,
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [9, 10, 11]}),
frameRate: 15,
repeat: -1
}
]
}
private getBody(): Phaser.Physics.Arcade.Body {
const body = this.body;
if (!(body instanceof Phaser.Physics.Arcade.Body)) {
throw new Error('Container does not have arcade body');
}
return body;
}
public destroy(): void {
for (const sprite of this.sprites.values()) {
if (this.scene) {
this.scene.sys.updateList.remove(sprite);
}
}
if (this.scene) {
this.scene.events.removeListener('update', this.updateListener);
}
super.destroy();
}
}

View File

@ -0,0 +1,14 @@
export interface CompanionResourceDescriptionInterface {
name: string,
img: string,
behaviour: "dog" | "cat"
}
export const COMPANION_RESOURCES: CompanionResourceDescriptionInterface[] = [
{ name: "dog1", img: "resources/characters/pipoya/Dog 01-1.png", behaviour: "dog" },
{ name: "dog2", img: "resources/characters/pipoya/Dog 01-2.png", behaviour: "dog" },
{ name: "dog3", img: "resources/characters/pipoya/Dog 01-3.png", behaviour: "dog" },
{ name: "cat1", img: "resources/characters/pipoya/Cat 01-1.png", behaviour: "cat" },
{ name: "cat2", img: "resources/characters/pipoya/Cat 01-2.png", behaviour: "cat" },
{ name: "cat3", img: "resources/characters/pipoya/Cat 01-3.png", behaviour: "cat" },
]

View File

@ -0,0 +1,29 @@
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import { COMPANION_RESOURCES, CompanionResourceDescriptionInterface } from "./CompanionTextures";
export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => {
COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => {
lazyLoadCompanionResource(loader, resource.name);
});
return COMPANION_RESOURCES;
}
export const lazyLoadCompanionResource = (loader: LoaderPlugin, name: string): Promise<string> => {
return new Promise((resolve, reject) => {
const resource = COMPANION_RESOURCES.find(item => item.name === name);
if (typeof resource === 'undefined') {
return reject(`Texture '${name}' not found!`);
}
if (loader.textureManager.exists(resource.name)) {
return resolve(resource.name);
}
loader.spritesheet(resource.name, resource.img, { frameWidth: 32, frameHeight: 32, endFrame: 12 });
loader.once(`filecomplete-spritesheet-${resource.name}`, () => resolve(resource.name));
loader.start(); // It's only automatically started during the Scene preload.
});
}

View File

@ -0,0 +1,54 @@
import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig;
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(0xBBBBBB, 1);
progress.fillRect((scene.game.renderer.width - loadingBarWidth) / 2, scene.game.renderer.height / 2 + 50, loadingBarWidth * value, loadingBarHeight);
});
scene.load.on('complete', () => {
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

@ -1,46 +1,68 @@
const IGNORED_KEYS = new Set([
'Esc',
'Escape',
'Alt',
'Meta',
'Control',
'Ctrl',
'Space',
'Backspace'
])
export class TextInput extends Phaser.GameObjects.BitmapText {
private minUnderLineLength = 4;
private underLine: Phaser.GameObjects.Text;
private domInput = document.createElement('input');
constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string, onChange: (text: string) => void) {
constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string,
onChange: (text: string) => void) {
super(scene, x, y, 'main_font', text, 32);
this.setOrigin(0.5).setCenterAlign()
this.setOrigin(0.5).setCenterAlign();
this.scene.add.existing(this);
this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), { fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'})
this.underLine.setOrigin(0.5)
const style = {fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'};
this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), style);
this.underLine.setOrigin(0.5);
this.scene.input.keyboard.on('keydown', (event: KeyboardEvent) => {
if (event.keyCode === 8 && this.text.length > 0) {
this.deleteLetter();
} else if ((event.keyCode === 32 || (event.keyCode >= 48 && event.keyCode <= 90)) && this.text.length < maxLength) {
this.addLetter(event.key);
this.domInput.maxLength = maxLength;
this.domInput.style.opacity = "0";
if (text) {
this.domInput.value = text;
}
this.domInput.addEventListener('keydown', event => {
if (IGNORED_KEYS.has(event.key)) {
return;
}
if (!/[a-zA-Z0-9:.!&?()+-]/.exec(event.key)) {
event.preventDefault();
}
});
this.domInput.addEventListener('input', (event) => {
if (event.defaultPrevented) {
return;
}
this.text = this.domInput.value;
this.underLine.text = this.getUnderLineBody(this.text.length);
onChange(this.text);
});
document.body.append(this.domInput);
this.focus();
}
private getUnderLineBody(textLength:number): string {
if (textLength < this.minUnderLineLength) textLength = this.minUnderLineLength;
let text = '_______';
for (let i = this.minUnderLineLength; i < textLength; i++) {
text += '__'
text += '__';
}
return text;
}
private deleteLetter() {
this.text = this.text.substr(0, this.text.length - 1);
}
private addLetter(letter: string) {
this.text += letter;
}
getText(): string {
return this.text;
}
@ -56,4 +78,13 @@ export class TextInput extends Phaser.GameObjects.BitmapText {
this.underLine.y = y+1;
return this;
}
focus() {
this.domInput.focus();
}
destroy(): void {
super.destroy();
this.domInput.remove();
}
}

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