Merge branch 'develop' of github.com:thecodingmachine/workadventure into trigger-message-refv3

This commit is contained in:
David Négrier 2021-08-03 19:01:16 +02:00
commit 4713010929
90 changed files with 2336 additions and 10674 deletions

View File

@ -19,3 +19,6 @@ ACME_EMAIL=
MAX_PER_GROUP=4 MAX_PER_GROUP=4
MAX_USERNAME_LENGTH=8 MAX_USERNAME_LENGTH=8
OPID_CLIENT_ID=
OPID_CLIENT_SECRET=
OPID_CLIENT_ISSUER=

View File

@ -50,6 +50,7 @@ jobs:
run: yarn run build run: yarn run build
env: env:
PUSHER_URL: "//localhost:8080" PUSHER_URL: "//localhost:8080"
ADMIN_URL: "//localhost:80"
working-directory: "front" working-directory: "front"
- name: "Svelte check" - name: "Svelte check"
@ -81,7 +82,7 @@ jobs:
- name: "Setup NodeJS" - name: "Setup NodeJS"
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: '12.x' node-version: '14.x'
- name: Install Protoc - name: Install Protoc
uses: arduino/setup-protoc@v1 uses: arduino/setup-protoc@v1

View File

@ -47,6 +47,7 @@ jobs:
run: yarn run build-typings run: yarn run build-typings
env: env:
PUSHER_URL: "//localhost:8080" PUSHER_URL: "//localhost:8080"
ADMIN_URL: "//localhost:80"
working-directory: "front" working-directory: "front"
# We build the front to generate the typings of iframe_api, then we copy those typings in a separate package. # We build the front to generate the typings of iframe_api, then we copy those typings in a separate package.

View File

@ -1,4 +1,13 @@
## Version 1.4.x-dev ## Version develop
### Updates
- New scripting API features :
- Use `WA.room.loadTileset(url: string) : Promise<number>` to load a tileset from a JSON file.
- Rewrote the way authentification works: the auth jwt token can now contains an email instead of an uuid
- Added an OpenId login flow than can be plugged to any OIDC provider.
- You can send a message to all rooms of your world from the console global message (user with tag admin only).
## Version 1.4.11
### Updates ### Updates
@ -28,7 +37,7 @@
- Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`) - Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`)
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked. - Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
- The text chat was redesigned to be prettier and to use more features : - The text chat was redesigned to be prettier and to use more features :
- The chat is now persistent bewteen discussions and always accesible - The chat is now persistent between discussions and always accessible
- The chat now tracks incoming and outcoming users in your conversation - The chat now tracks incoming and outcoming users in your conversation
- The chat allows your to see the visit card of users - The chat allows your to see the visit card of users
- You can close the chat window with the escape key - You can close the chat window with the escape key

View File

@ -104,6 +104,15 @@ export class GameRoom {
public getUserById(id: number): User | undefined { public getUserById(id: number): User | undefined {
return this.users.get(id); return this.users.get(id);
} }
public getUsersByUuid(uuid: string): User[] {
const userList: User[] = [];
for (const user of this.users.values()) {
if (user.uuid === uuid) {
userList.push(user);
}
}
return userList;
}
public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User { public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User {
const positionMessage = joinRoomMessage.getPositionmessage(); const positionMessage = joinRoomMessage.getPositionmessage();

View File

@ -21,7 +21,7 @@ interface ZoneDescriptor {
} }
export class PositionNotifier { export class PositionNotifier {
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) // TODO: we need a way to clean the zones if no one is in the zone and no one listening (to free memory!)
private zones: Zone[][] = []; private zones: Zone[][] = [];

View File

@ -57,7 +57,7 @@ const roomManager: IRoomManagerServer = {
room = gameRoom; room = gameRoom;
user = myUser; user = myUser;
} else { } else {
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user. //Connection may have been closed before the init was finished, so we have to manually disconnect the user.
socketManager.leaveRoom(gameRoom, myUser); socketManager.leaveRoom(gameRoom, myUser);
} }
}) })
@ -272,7 +272,7 @@ const roomManager: IRoomManagerServer = {
sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void { sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
// FIXME: we could improve return message by returning a Success|ErrorMessage message // FIXME: we could improve return message by returning a Success|ErrorMessage message
socketManager socketManager
.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()) .sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage(), call.request.getType())
.catch((e) => console.error(e)); .catch((e) => console.error(e));
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },

View File

@ -27,7 +27,9 @@ class MapFetcher {
}); });
if (!isTiledMap(res.data)) { if (!isTiledMap(res.data)) {
throw new Error("Invalid map format for map " + mapUrl); //TODO fixme
//throw new Error("Invalid map format for map " + mapUrl);
console.error("Invalid map format for map " + mapUrl);
} }
return res.data; return res.data;

View File

@ -701,8 +701,8 @@ export class SocketManager {
return; return;
} }
const recipient = room.getUserByUuid(recipientUuid); const recipients = room.getUsersByUuid(recipientUuid);
if (recipient === undefined) { if (recipients.length === 0) {
console.error( console.error(
"In sendAdminMessage, could not find user with id '" + "In sendAdminMessage, could not find user with id '" +
recipientUuid + recipientUuid +
@ -711,14 +711,16 @@ export class SocketManager {
return; return;
} }
const sendUserMessage = new SendUserMessage(); for (const recipient of recipients) {
sendUserMessage.setMessage(message); const sendUserMessage = new SendUserMessage();
sendUserMessage.setType("ban"); //todo: is the type correct? sendUserMessage.setMessage(message);
sendUserMessage.setType("ban"); //todo: is the type correct?
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setSendusermessage(sendUserMessage); serverToClientMessage.setSendusermessage(sendUserMessage);
recipient.socket.write(serverToClientMessage); recipient.socket.write(serverToClientMessage);
}
} }
public async banUser(roomId: string, recipientUuid: string, message: string): Promise<void> { public async banUser(roomId: string, recipientUuid: string, message: string): Promise<void> {
@ -732,8 +734,8 @@ export class SocketManager {
return; return;
} }
const recipient = room.getUserByUuid(recipientUuid); const recipients = room.getUsersByUuid(recipientUuid);
if (recipient === undefined) { if (recipients.length === 0) {
console.error( console.error(
"In banUser, could not find user with id '" + "In banUser, could not find user with id '" +
recipientUuid + recipientUuid +
@ -742,22 +744,24 @@ export class SocketManager {
return; return;
} }
// Let's leave the room now. for (const recipient of recipients) {
room.leave(recipient); // Let's leave the room now.
room.leave(recipient);
const banUserMessage = new BanUserMessage(); const banUserMessage = new BanUserMessage();
banUserMessage.setMessage(message); banUserMessage.setMessage(message);
banUserMessage.setType("banned"); banUserMessage.setType("banned");
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setBanusermessage(banUserMessage); serverToClientMessage.setBanusermessage(banUserMessage);
// Let's close the connection when the user is banned. // Let's close the connection when the user is banned.
recipient.socket.write(serverToClientMessage); recipient.socket.write(serverToClientMessage);
recipient.socket.end(); recipient.socket.end();
}
} }
async sendAdminRoomMessage(roomId: string, message: string) { async sendAdminRoomMessage(roomId: string, message: string, type: string) {
const room = await this.roomsPromises.get(roomId); const room = await this.roomsPromises.get(roomId);
if (!room) { if (!room) {
//todo: this should cause the http call to return a 500 //todo: this should cause the http call to return a 500
@ -772,7 +776,7 @@ export class SocketManager {
room.getUsers().forEach((recipient) => { room.getUsers().forEach((recipient) => {
const sendUserMessage = new SendUserMessage(); const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(message); sendUserMessage.setMessage(message);
sendUserMessage.setType("message"); sendUserMessage.setType(type);
const clientMessage = new ServerToClientMessage(); const clientMessage = new ServerToClientMessage();
clientMessage.setSendusermessage(sendUserMessage); clientMessage.setSendusermessage(sendUserMessage);
@ -786,7 +790,7 @@ export class SocketManager {
if (!room) { if (!room) {
//todo: this should cause the http call to return a 500 //todo: this should cause the http call to return a 500
console.error( console.error(
"In sendAdminRoomMessage, could not find room with id '" + "In dispatchWorldFullWarning, could not find room with id '" +
roomId + roomId +
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?" "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
); );

View File

@ -13,7 +13,6 @@ RoomConnection.setWebsocketFactory((url: string) => {
}); });
async function startOneUser(): Promise<void> { async function startOneUser(): Promise<void> {
await connectionManager.anonymousLogin(true);
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'], const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
{ {
x: 783, x: 783,
@ -23,7 +22,7 @@ async function startOneUser(): Promise<void> {
bottom: 200, bottom: 200,
left: 500, left: 500,
right: 800 right: 800
}); }, null);
const connection = onConnect.connection; const connection = onConnect.connection;

View File

@ -53,7 +53,7 @@ services:
- "traefik.http.routers.front-ssl.service=front" - "traefik.http.routers.front-ssl.service=front"
pusher: pusher:
image: thecodingmachine/nodejs:12 image: thecodingmachine/nodejs:14
command: yarn dev command: yarn dev
#command: yarn run prod #command: yarn run prod
#command: yarn run profile #command: yarn run profile
@ -66,6 +66,10 @@ services:
API_URL: back:50051 API_URL: back:50051
JITSI_URL: $JITSI_URL JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS JITSI_ISS: $JITSI_ISS
FRONT_URL: http://localhost
OPID_CLIENT_ID: $OPID_CLIENT_ID
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
volumes: volumes:
- ./pusher:/usr/src/app - ./pusher:/usr/src/app
labels: labels:

View File

@ -55,7 +55,7 @@ services:
- "traefik.http.routers.front-ssl.service=front" - "traefik.http.routers.front-ssl.service=front"
pusher: pusher:
image: thecodingmachine/nodejs:12 image: thecodingmachine/nodejs:14
command: yarn dev command: yarn dev
environment: environment:
DEBUG: "socket:*" DEBUG: "socket:*"
@ -66,6 +66,10 @@ services:
API_URL: back:50051 API_URL: back:50051
JITSI_URL: $JITSI_URL JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS JITSI_ISS: $JITSI_ISS
FRONT_URL: http://play.workadventure.localhost
OPID_CLIENT_ID: $OPID_CLIENT_ID
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
volumes: volumes:
- ./pusher:/usr/src/app - ./pusher:/usr/src/app
labels: labels:

View File

@ -163,3 +163,17 @@ WA.room.setTiles([
{x: 9, y: 4, tile: 'blue', layer: 'setTiles'} {x: 9, y: 4, tile: 'blue', layer: 'setTiles'}
]); ]);
``` ```
### Loading a tileset
```
WA.room.loadTileset(url: string): Promise<number>
```
Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset.
You can create a tileset file in Tile Editor.
```javascript
WA.room.loadTileset("Assets/Tileset.json").then((firstId) => {
WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]);
})
```

35
docs/maps/text.md Normal file
View File

@ -0,0 +1,35 @@
{.section-title.accent.text-primary}
# Writing text on a map
## Solution 1: design a specific tileset (recommended)
If you want to write some text on a map, our recommendation is to create a tileset that contains
your text. You will obtain the most pleasant graphical result with this result, since you will be able
to control the fonts you use, and you will be able to disable the antialiasing of the font to get a
"crispy" result easily.
## Solution 2: using a "text" object in Tiled
On "object" layers, Tiled has support for "Text" objects. You can use these objects to add some
text on your map.
WorkAdventure will do its best to display the text properly. However, you need to know that:
- Tiled displays your system fonts.
- Computers have different sets of fonts. Therefore, browsers never rely on system fonts
- Which means if you select a font in Tiled, it is quite unlikely it will render properly in WorkAdventure
To circumvent this problem, in your text object in Tiled, you can add an additional property: `font-family`.
The `font-family` property can contain any "web-font" that can be loaded by your browser.
{.alert.alert-info}
**Pro-tip:** By default, WorkAdventure uses the **'"Press Start 2P"'** font, which is a great pixelated
font that has support for a variety of accents. It renders great when used at *8px* size.
<div>
<figure class="figure">
<img src="https://workadventu.re/img/docs/text-object.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
<figcaption class="figure-caption">The "font-family" property</figcaption>
</figure>
</div>

View File

@ -56,7 +56,7 @@ A few things to notice:
## Building walls and "collidable" areas ## Building walls and "collidable" areas
By default, the characters can traverse any tiles. If you want to prevent your characeter from going through a tile (like a wall or a desktop), you must make this tile "collidable". You can do this by settings the `collides` property on a given tile. By default, the characters can traverse any tiles. If you want to prevent your character from going through a tile (like a wall or a desktop), you must make this tile "collidable". You can do this by settings the `collides` property on a given tile.
To make a tile "collidable", you should: To make a tile "collidable", you should:

View File

@ -34,6 +34,7 @@
<title>WorkAdventure</title> <title>WorkAdventure</title>
</head> </head>
<body id="body" style="margin: 0; background-color: #000"> <body id="body" style="margin: 0; background-color: #000">
<div class="main-container" id="main-container"> <div class="main-container" id="main-container">
<!-- Create the editor container --> <!-- Create the editor container -->
<div id="game" class="game"> <div id="game" class="game">

View File

@ -60,6 +60,10 @@
<section> <section>
<button id="enableNotification">Enable notifications</button> <button id="enableNotification">Enable notifications</button>
</section> </section>
<!-- TODO activate authentication -->
<section hidden>
<button id="oidcLogin">Oauth Login</button>
</section>
<section> <section>
<button id="sparkButton">Create map</button> <button id="sparkButton">Create map</button>
</section> </section>

View File

@ -1,18 +0,0 @@
<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,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
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">
<!-- 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">
<link rel="apple-touch-icon" sizes="72x72" href="/static/images/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/static/images/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/static/images/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/static/images/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/static/images/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/images/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/static/images/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicons/favicon-16x16.png">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="/static/images/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#000000">
<title>WorkAdventure PWA</title>
<style>
body{
font-family: Whitney, Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
body img{
position: absolute;
top: calc( 50% - 25px);
height: 59px;
width: 307px;
left: calc( 50% - 150px);
}
body p{
position: absolute;
text-align: center;
top: calc( 50% + 50px);
left: calc( 50% - 150px);
height: 59px;
width: 307px;
font-size: 20px;
}
</style>
</head>
<body>
<img src="/static/images/logo.png" alt="WorkAdventure logo"/>
<p>Charging your workspace ...</p>
<script>
setTimeout(() => {
window.location = localStorage.getItem('lastRoomUrl');
}, 4000);
</script>
</body>
</html>

View File

@ -48,6 +48,14 @@ self.addEventListener('fetch', function(event) {
); );
}); });
self.addEventListener('activate', function(event) { self.addEventListener('wait', function(event) {
//TODO activate service worker //TODO wait
});
self.addEventListener('update', function(event) {
//TODO update
});
self.addEventListener('beforeinstallprompt', (e) => {
//TODO change prompt
}); });

View File

@ -128,11 +128,12 @@
"type": "image\/png" "type": "image\/png"
} }
], ],
"start_url": "/", "start_url": "/resources/service-worker.html",
"background_color": "#000000", "background_color": "#000000",
"display_override": ["window-control-overlay", "minimal-ui"], "display_override": ["window-control-overlay", "minimal-ui"],
"display": "standalone", "display": "standalone",
"scope": "/", "orientation": "portrait-primary",
"scope": "/resources/",
"lang": "en", "lang": "en",
"theme_color": "#000000", "theme_color": "#000000",
"shortcuts": [ "shortcuts": [

8393
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
"@types/mini-css-extract-plugin": "^1.4.3", "@types/mini-css-extract-plugin": "^1.4.3",
"@types/node": "^15.3.0", "@types/node": "^15.3.0",
"@types/quill": "^1.3.7", "@types/quill": "^1.3.7",
"@types/uuidv4": "^5.0.0",
"@types/webpack-dev-server": "^3.11.4", "@types/webpack-dev-server": "^3.11.4",
"@typescript-eslint/eslint-plugin": "^4.23.0", "@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0", "@typescript-eslint/parser": "^4.23.0",
@ -50,10 +51,12 @@
"phaser3-rex-plugins": "^1.1.42", "phaser3-rex-plugins": "^1.1.42",
"queue-typescript": "^1.0.1", "queue-typescript": "^1.0.1",
"quill": "1.3.6", "quill": "1.3.6",
"quill-delta-to-html": "^0.12.0",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"simple-peer": "^9.11.0", "simple-peer": "^9.11.0",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4" "standardized-audio-context": "^25.2.4",
"uuidv4": "^6.2.10"
}, },
"scripts": { "scripts": {
"start": "run-p templater serve svelte-check-watch", "start": "run-p templater serve svelte-check-watch",

View File

@ -1,92 +0,0 @@
import {HtmlUtils} from "./../WebRtc/HtmlUtils";
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import type {RoomConnection} from "../Connexion/RoomConnection";
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import {soundManager} from "../Phaser/Game/SoundManager";
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
export class GlobalMessageManager {
constructor(private Connection: RoomConnection) {
this.initialise();
}
initialise(){
//receive signal to show message
this.Connection.receivePlayGlobalMessage((message: PlayGlobalMessageInterface) => {
this.playMessage(message);
});
//receive signal to close message
this.Connection.receiveStopGlobalMessage((messageId: string) => {
this.stopMessage(messageId);
});
//receive signal to close message
this.Connection.receiveTeleportMessage((map: string) => {
console.log('map to teleport user', map);
//TODO teleport user on map
});
}
private playMessage(message : PlayGlobalMessageInterface){
const previousMessage = document.getElementById(this.getHtmlMessageId(message.id));
if(previousMessage){
previousMessage.remove();
}
if(AdminMessageEventTypes.audio === message.type){
this.playAudioMessage(message.id, message.message);
}
if(AdminMessageEventTypes.admin === message.type){
this.playTextMessage(message.id, message.message);
}
}
private playAudioMessage(messageId : string, urlMessage: string) {
soundPlayingStore.playSound(UPLOADER_URL + urlMessage);
}
private playTextMessage(messageId : string, htmlMessage: string){
//add button to clear message
const buttonText = document.createElement('p');
buttonText.id = 'button-clear-message';
buttonText.innerText = 'Clear';
const buttonMainConsole = document.createElement('div');
buttonMainConsole.classList.add('clear');
buttonMainConsole.appendChild(buttonText);
buttonMainConsole.addEventListener('click', () => {
messageContainer.style.top = '-80%';
setTimeout(() => {
messageContainer.remove();
buttonMainConsole.remove();
});
});
//create content message
const messageCotent = document.createElement('div');
messageCotent.innerHTML = htmlMessage;
messageCotent.className = "content-message";
//add message container
const messageContainer = document.createElement('div');
messageContainer.id = this.getHtmlMessageId(messageId);
messageContainer.className = "message-container";
messageContainer.appendChild(messageCotent);
messageContainer.appendChild(buttonMainConsole);
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainSectionDiv.appendChild(messageContainer);
}
private stopMessage(messageId: string){
HtmlUtils.removeElementByIdOrFail<HTMLDivElement>(this.getHtmlMessageId(messageId));
}
private getHtmlMessageId(messageId: string) : string{
return `message-${messageId}`;
}
}

View File

@ -1,95 +0,0 @@
import type {TypeMessageInterface} from "./UserMessageManager";
import {HtmlUtils} from "../WebRtc/HtmlUtils";
let modalTimeOut : NodeJS.Timeout;
export class TypeMessageExt implements TypeMessageInterface{
private nbSecond = 0;
private maxNbSecond = 10;
private titleMessage = 'IMPORTANT !';
showMessage(message: string, canDeleteMessage: boolean = true): void {
//delete previous modal
try{
if(modalTimeOut){
clearTimeout(modalTimeOut);
}
const modal = HtmlUtils.getElementByIdOrFail('report-message-user');
modal.remove();
}catch (err){
console.error(err);
}
//create new modal
const div : HTMLDivElement = document.createElement('div');
div.classList.add('modal-report-user');
div.id = 'report-message-user';
div.style.backgroundColor = '#000000e0';
const img : HTMLImageElement = document.createElement('img');
img.src = 'resources/logos/report.svg';
div.appendChild(img);
const title : HTMLParagraphElement = document.createElement('p');
title.id = 'title-report-user';
title.innerText = `${this.titleMessage} (${this.maxNbSecond})`;
div.appendChild(title);
const p : HTMLParagraphElement = document.createElement('p');
p.id = 'body-report-user'
p.innerText = message;
div.appendChild(p);
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainSectionDiv.appendChild(div);
const reportMessageAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('report-message');
// FIXME: this will fail on iOS
// We should move the sound playing into the GameScene and listen to the event of a report using a store
try {
reportMessageAudio.play();
} catch (e) {
console.error(e);
}
this.nbSecond = this.maxNbSecond;
setTimeout((c) => {
this.forMessage(title, canDeleteMessage);
}, 1000);
}
forMessage(title: HTMLParagraphElement, canDeleteMessage: boolean = true){
this.nbSecond -= 1;
title.innerText = `${this.titleMessage} (${this.nbSecond})`;
if(this.nbSecond > 0){
modalTimeOut = setTimeout(() => {
this.forMessage(title, canDeleteMessage);
}, 1000);
}else {
title.innerText = this.titleMessage;
if (!canDeleteMessage) {
return;
}
const imgCancel: HTMLImageElement = document.createElement('img');
imgCancel.id = 'cancel-report-user';
imgCancel.src = 'resources/logos/close.svg';
const div = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('report-message-user');
div.appendChild(imgCancel);
imgCancel.addEventListener('click', () => {
div.remove();
});
}
}
}
export class Message extends TypeMessageExt {}
export class Ban extends TypeMessageExt {}
export class Banned extends TypeMessageExt {
showMessage(message: string){
super.showMessage(message, false);
}
}

View File

@ -1,43 +1,34 @@
import * as TypeMessages from "./TypeMessage"; import { AdminMessageEventTypes, adminMessagesService } from "../Connexion/AdminMessagesService";
import {Banned} from "./TypeMessage"; import { textMessageContentStore, textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore";
import {adminMessagesService} from "../Connexion/AdminMessagesService"; import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import { UPLOADER_URL } from "../Enum/EnvironmentVariable";
export interface TypeMessageInterface { import { banMessageContentStore, banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore";
showMessage(message: string): void;
}
class UserMessageManager { class UserMessageManager {
typeMessages: Map<string, TypeMessageInterface> = new Map<string, TypeMessageInterface>();
receiveBannedMessageListener!: Function; receiveBannedMessageListener!: Function;
constructor() { 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);
});
adminMessagesService.messageStream.subscribe((event) => { adminMessagesService.messageStream.subscribe((event) => {
const typeMessage = this.showMessage(event.type, event.text); textMessageVisibleStore.set(false);
if(typeMessage instanceof Banned) { banMessageVisibleStore.set(false);
if (event.type === AdminMessageEventTypes.admin) {
textMessageContentStore.set(event.text);
textMessageVisibleStore.set(true);
} else if (event.type === AdminMessageEventTypes.audio) {
soundPlayingStore.playSound(UPLOADER_URL + event.text);
} else if (event.type === AdminMessageEventTypes.ban) {
banMessageContentStore.set(event.text);
banMessageVisibleStore.set(true);
} else if (event.type === AdminMessageEventTypes.banned) {
banMessageContentStore.set(event.text);
banMessageVisibleStore.set(true);
this.receiveBannedMessageListener(); this.receiveBannedMessageListener();
} }
}) });
} }
showMessage(type: string, message: string) { setReceiveBanListener(callback: Function) {
const classTypeMessage = this.typeMessages.get(type.toLowerCase());
if (!classTypeMessage) {
console.error('Message unknown');
return;
}
classTypeMessage.showMessage(message);
return classTypeMessage;
}
setReceiveBanListener(callback: Function){
this.receiveBannedMessageListener = callback; this.receiveBannedMessageListener = callback;
} }
} }
export const userMessageManager = new UserMessageManager() export const userMessageManager = new UserMessageManager();

View File

@ -1,5 +1,4 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
import type { GameStateEvent } from "./GameStateEvent";
import type { ButtonClickedEvent } from "./ButtonClickedEvent"; import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ChatEvent } from "./ChatEvent"; import type { ChatEvent } from "./ChatEvent";
import type { ClosePopupEvent } from "./ClosePopupEvent"; import type { ClosePopupEvent } from "./ClosePopupEvent";
@ -19,16 +18,18 @@ import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent"; import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent"; import type { SetTilesEvent } from "./SetTilesEvent";
import type { SetVariableEvent } from "./SetVariableEvent";
import { isGameStateEvent } from "./GameStateEvent";
import { isMapDataEvent } from "./MapDataEvent";
import { isSetVariableEvent } from "./SetVariableEvent";
import type { LoadTilesetEvent } from "./LoadTilesetEvent";
import { isLoadTilesetEvent } from "./LoadTilesetEvent";
import type { import type {
MessageReferenceEvent, MessageReferenceEvent,
removeTriggerMessage, removeTriggerMessage,
triggerMessage, triggerMessage,
TriggerMessageEvent, TriggerMessageEvent,
} from "./ui/TriggerMessageEvent"; } from "./ui/TriggerMessageEvent";
import type { SetVariableEvent } from "./SetVariableEvent";
import { isGameStateEvent } from "./GameStateEvent";
import { isMapDataEvent } from "./MapDataEvent";
import { isSetVariableEvent } from "./SetVariableEvent";
import { isMessageReferenceEvent, isTriggerMessageEvent } from "./ui/TriggerMessageEvent"; import { isMessageReferenceEvent, isTriggerMessageEvent } from "./ui/TriggerMessageEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
@ -59,6 +60,7 @@ export type IframeEventMap = {
playSound: PlaySoundEvent; playSound: PlaySoundEvent;
stopSound: null; stopSound: null;
getState: undefined; getState: undefined;
loadTileset: LoadTilesetEvent;
registerMenuCommand: MenuItemRegisterEvent; registerMenuCommand: MenuItemRegisterEvent;
setTiles: SetTilesEvent; setTiles: SetTilesEvent;
@ -111,6 +113,10 @@ export const iframeQueryMapTypeGuards = {
query: isSetVariableEvent, query: isSetVariableEvent,
answer: tg.isUndefined, answer: tg.isUndefined,
}, },
loadTileset: {
query: isLoadTilesetEvent,
answer: tg.isNumber,
},
triggerMessage: { triggerMessage: {
query: isTriggerMessageEvent, query: isTriggerMessageEvent,
answer: tg.isUndefined, answer: tg.isUndefined,

View File

@ -0,0 +1,12 @@
import * as tg from "generic-type-guard";
export const isLoadTilesetEvent = 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 LoadTilesetEvent = tg.GuardedType<typeof isLoadTilesetEvent>;

View File

@ -27,9 +27,6 @@ import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent"; import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent"; import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
import type { MapDataEvent } from "./Events/MapDataEvent";
import type { GameStateEvent } from "./Events/GameStateEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent"; import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";

View File

@ -105,6 +105,14 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
} }
return mapURL; return mapURL;
} }
async loadTileset(url: string): Promise<number> {
return await queryWorkadventure({
type: "loadTileset",
data: {
url: url,
},
});
}
} }
export default new WorkadventureRoomCommands(); export default new WorkadventureRoomCommands();

View File

@ -27,6 +27,12 @@
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore"; import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte"; import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
import AdminMessage from "./TypeMessage/BanMessage.svelte";
import TextMessage from "./TypeMessage/TextMessage.svelte";
import {banMessageVisibleStore} from "../Stores/TypeMessageStore/BanMessageStore";
import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore";
import {warningContainerStore} from "../Stores/MenuStore";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
export let game: Game; export let game: Game;
@ -58,6 +64,16 @@
<EnableCameraScene game={game}></EnableCameraScene> <EnableCameraScene game={game}></EnableCameraScene>
</div> </div>
{/if} {/if}
{#if $banMessageVisibleStore}
<div>
<AdminMessage></AdminMessage>
</div>
{/if}
{#if $textMessageVisibleStore}
<div>
<TextMessage></TextMessage>
</div>
{/if}
{#if $soundPlayingStore} {#if $soundPlayingStore}
<div> <div>
<AudioPlaying url={$soundPlayingStore} /> <AudioPlaying url={$soundPlayingStore} />
@ -91,4 +107,7 @@
{#if $chatVisibilityStore} {#if $chatVisibilityStore}
<Chat></Chat> <Chat></Chat>
{/if} {/if}
{#if $warningContainerStore}
<WarningContainer></WarningContainer>
{/if}
</div> </div>

View File

@ -3,9 +3,12 @@
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore"; import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
import ChatMessageForm from './ChatMessageForm.svelte'; import ChatMessageForm from './ChatMessageForm.svelte';
import ChatElement from './ChatElement.svelte'; import ChatElement from './ChatElement.svelte';
import { afterUpdate, beforeUpdate } from "svelte"; import {afterUpdate, beforeUpdate} from "svelte";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
let listDom: HTMLElement; let listDom: HTMLElement;
let chatWindowElement: HTMLElement;
let handleFormBlur: { blur():void };
let autoscroll: boolean; let autoscroll: boolean;
beforeUpdate(() => { beforeUpdate(() => {
@ -16,6 +19,12 @@
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight); if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
}); });
function onClick(event: MouseEvent) {
if (HtmlUtils.isClickedOutside(event, chatWindowElement)) {
handleFormBlur.blur();
}
}
function closeChat() { function closeChat() {
chatVisibilityStore.set(false); chatVisibilityStore.set(false);
} }
@ -26,10 +35,10 @@
} }
</script> </script>
<svelte:window on:keydown={onKeyDown}/> <svelte:window on:keydown={onKeyDown} on:click={onClick}/>
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}"> <aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}" bind:this={chatWindowElement}>
<p class="close-icon" on:click={closeChat}>&times</p> <p class="close-icon" on:click={closeChat}>&times</p>
<section class="messagesList" bind:this={listDom}> <section class="messagesList" bind:this={listDom}>
<ul> <ul>
@ -40,7 +49,7 @@
</ul> </ul>
</section> </section>
<section class="messageForm"> <section class="messageForm">
<ChatMessageForm></ChatMessageForm> <ChatMessageForm bind:handleForm={handleFormBlur}></ChatMessageForm>
</section> </section>
</aside> </aside>

View File

@ -7,13 +7,14 @@
export let message: ChatMessage; export let message: ChatMessage;
export let line: number; export let line: number;
const chatStyleLink = "color: white; text-decoration: underline;";
$: author = message.author as PlayerInterface; $: author = message.author as PlayerInterface;
$: targets = message.targets || []; $: targets = message.targets || [];
$: texts = message.text || []; $: texts = message.text || [];
function urlifyText(text: string): string { function urlifyText(text: string): string {
return HtmlUtils.urlify(text) return HtmlUtils.urlify(text, chatStyleLink);
} }
function renderDate(date: Date) { function renderDate(date: Date) {
return date.toLocaleTimeString(navigator.language, { return date.toLocaleTimeString(navigator.language, {

View File

@ -1,6 +1,12 @@
<script lang="ts"> <script lang="ts">
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore"; import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore";
export const handleForm = {
blur() {
inputElement.blur();
}
}
let inputElement: HTMLElement;
let newMessageText = ''; let newMessageText = '';
function onFocus() { function onFocus() {
@ -18,7 +24,7 @@
</script> </script>
<form on:submit|preventDefault={saveMessage}> <form on:submit|preventDefault={saveMessage}>
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} > <input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} bind:this={inputElement}>
<button type="submit"> <button type="submit">
<img src="/static/images/send.png" alt="Send" width="20"> <img src="/static/images/send.png" alt="Send" width="20">
</button> </button>

View File

@ -1,12 +1,27 @@
<script lang="typescript"> <script lang="typescript">
import { fly } from 'svelte/transition';
import InputTextGlobalMessage from "./InputTextGlobalMessage.svelte"; import InputTextGlobalMessage from "./InputTextGlobalMessage.svelte";
import UploadAudioGlobalMessage from "./UploadAudioGlobalMessage.svelte"; import UploadAudioGlobalMessage from "./UploadAudioGlobalMessage.svelte";
import {gameManager} from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import type {Game} from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
export let game: Game; export let game: Game;
let inputSendTextActive = true; let inputSendTextActive = true;
let uploadMusicActive = false; let uploadMusicActive = false;
let handleSendText: { sendTextMessage(broadcast: boolean): void };
let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
let broadcastToWorld = false;
function closeConsoleGlobalMessage() {
consoleGlobalMessageManagerVisibleStore.set(false)
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
closeConsoleGlobalMessage();
}
}
function inputSendTextActivate() { function inputSendTextActivate() {
inputSendTextActive = true; inputSendTextActive = true;
@ -17,28 +32,121 @@
uploadMusicActive = true; uploadMusicActive = true;
inputSendTextActive = false; inputSendTextActive = false;
} }
function send() {
if (inputSendTextActive) {
handleSendText.sendTextMessage(broadcastToWorld);
}
if (uploadMusicActive) {
handleSendAudio.sendAudioMessage(broadcastToWorld);
}
}
</script> </script>
<svelte:window on:keydown={onKeyDown}/>
<div class="main-console nes-container is-rounded"> <div class="console-global-message">
<!-- <div class="console nes-container is-rounded"> <div class="menu-console-global-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
<img class="btn-close" src="resources/logos/send-yellow.svg" alt="Close"> <button type="button" class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={inputSendTextActivate}>Message</button>
</div>--> <button type="button" class="nes-btn {uploadMusicActive ? 'is-disabled' : ''}" on:click|preventDefault={inputUploadMusicActivate}>Audio</button>
<div class="main-global-message"> </div>
<h2> Global Message </h2> <div class="main-console-global-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
<div class="global-message"> <div class="title-console-global-message">
<div class="menu"> <h2>Global Message</h2>
<button class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={inputSendTextActivate}>Message</button> <button type="button" class="nes-btn is-error" on:click|preventDefault={closeConsoleGlobalMessage}><i class="nes-icon close is-small"></i></button>
<button class="nes-btn {uploadMusicActive ? 'is-disabled' : ''}" on:click|preventDefault={inputUploadMusicActivate}>Audio</button> </div>
</div> <div class="content-console-global-message">
<div class="main-input"> {#if inputSendTextActive}
{#if inputSendTextActive} <InputTextGlobalMessage game={game} gameManager={gameManager} bind:handleSending={handleSendText}/>
<InputTextGlobalMessage game={game} gameManager={gameManager}></InputTextGlobalMessage> {/if}
{/if} {#if uploadMusicActive}
{#if uploadMusicActive} <UploadAudioGlobalMessage game={game} gameManager={gameManager} bind:handleSending={handleSendAudio}/>
<UploadAudioGlobalMessage game={game} gameManager={gameManager}></UploadAudioGlobalMessage> {/if}
{/if} </div>
</div> <div class="footer-console-global-message">
<label>
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld}>
<span>Broadcast to all rooms of the world</span>
</label>
<button class="nes-btn is-primary" on:click|preventDefault={send}>Send</button>
</div> </div>
</div> </div>
</div> </div>
<style lang="scss">
.nes-container {
padding: 0 5px;
}
div.console-global-message {
top: 20vh;
width: 50vw;
height: 50vh;
position: relative;
display: flex;
flex-direction: row;
margin-left: auto;
margin-right: auto;
padding: 0;
pointer-events: auto;
div.menu-console-global-message {
flex: 1 1 auto;
max-width: 180px;
text-align: center;
background-color: #333333;
button {
width: 136px;
margin-bottom: 10px;
}
}
div.main-console-global-message {
flex: 1 1 auto;
display: flex;
flex-direction: column;
background-color: #333333;
div.title-console-global-message {
flex: 0 0 auto;
height: 50px;
margin-bottom: 10px;
text-align: center;
color: whitesmoke;
.nes-btn {
position: absolute;
top: 0;
right: 0;
}
}
div.content-console-global-message {
flex: 1 1 auto;
max-height: calc(100% - 120px);
}
div.footer-console-global-message {
height: 50px;
margin-top: 10px;
text-align: center;
label {
margin: 0;
position: absolute;
left: 0;
max-width: 30%;
}
}
}
}
</style>

View File

@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import {consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore"; import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import {onMount} from "svelte"; import {onDestroy, onMount} from "svelte";
import type {Game} from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import type {GameManager} from "../../Phaser/Game/GameManager"; import type { GameManager } from "../../Phaser/Game/GameManager";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels"; import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import {AdminMessageEventTypes} from "../../Connexion/AdminMessagesService"; import type { Quill } from "quill";
import type {Quill} from "quill"; import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
import {LoginSceneName} from "../../Phaser/Login/LoginScene";
//toolbar //toolbar
export const toolbarOptions = [ const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // toggled buttons ['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'], ['blockquote', 'code-block'],
@ -35,12 +34,31 @@
export let game: Game; export let game: Game;
export let gameManager: GameManager; export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.scene.getScene(LoginSceneName)); const gameScene = gameManager.getCurrentGameScene(game.findAnyScene());
let quill: Quill; let quill: Quill;
let INPUT_CONSOLE_MESSAGE: HTMLDivElement; let INPUT_CONSOLE_MESSAGE: HTMLDivElement;
const MESSAGE_TYPE = AdminMessageEventTypes.admin; const MESSAGE_TYPE = AdminMessageEventTypes.admin;
export const handleSending = {
sendTextMessage(broadcastToWorld: boolean) {
if (gameScene == undefined) {
return;
}
const text = JSON.stringify(quill.getContents(0, quill.getLength()));
const textGlobalMessage: PlayGlobalMessageInterface = {
type: MESSAGE_TYPE,
content: text,
broadcastToWorld: broadcastToWorld
};
quill.deleteText(0, quill.getLength());
gameScene.connection?.emitGlobalMessage(textGlobalMessage);
disableConsole();
}
}
//Quill //Quill
onMount(async () => { onMount(async () => {
@ -48,49 +66,28 @@
const {default: Quill} = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any const {default: Quill} = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
quill = new Quill(INPUT_CONSOLE_MESSAGE, { quill = new Quill(INPUT_CONSOLE_MESSAGE, {
placeholder: 'Enter your message here...',
theme: 'snow', theme: 'snow',
modules: { modules: {
toolbar: toolbarOptions toolbar: toolbarOptions
}, },
}); });
quill.on('selection-change', function (range, oldRange) { consoleGlobalMessageManagerFocusStore.set(true);
if (range === null && oldRange !== null) {
consoleGlobalMessageManagerFocusStore.set(false);
} else if (range !== null && oldRange === null)
consoleGlobalMessageManagerFocusStore.set(true);
});
}); });
onDestroy(() => {
consoleGlobalMessageManagerFocusStore.set(false);
})
function disableConsole() { function disableConsole() {
consoleGlobalMessageManagerVisibleStore.set(false); consoleGlobalMessageManagerVisibleStore.set(false);
consoleGlobalMessageManagerFocusStore.set(false); consoleGlobalMessageManagerFocusStore.set(false);
} }
function SendTextMessage() {
if (gameScene == undefined) {
return;
}
const text = quill.getText(0, quill.getLength());
const GlobalMessage: PlayGlobalMessageInterface = {
id: "1", // FIXME: use another ID?
message: text,
type: MESSAGE_TYPE
};
quill.deleteText(0, quill.getLength());
gameScene.connection?.emitGlobalMessage(GlobalMessage);
disableConsole();
}
</script> </script>
<section class="section-input-send-text"> <section class="section-input-send-text">
<div class="input-send-text" bind:this={INPUT_CONSOLE_MESSAGE}></div> <div class="input-send-text" bind:this={INPUT_CONSOLE_MESSAGE}></div>
<div class="btn-action">
<button class="nes-btn is-primary" on:click|preventDefault={SendTextMessage}>Send</button>
</div>
</section> </section>

View File

@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import {HtmlUtils} from "../../WebRtc/HtmlUtils"; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import type {Game} from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import type {GameManager} from "../../Phaser/Game/GameManager"; import type { GameManager } from "../../Phaser/Game/GameManager";
import {consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore"; import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import {AdminMessageEventTypes} from "../../Connexion/AdminMessagesService"; import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
import uploadFile from "../images/music-file.svg"; import uploadFile from "../images/music-file.svg";
import {LoginSceneName} from "../../Phaser/Login/LoginScene"; import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
interface EventTargetFiles extends EventTarget { interface EventTargetFiles extends EventTarget {
files: Array<File>; files: Array<File>;
@ -15,38 +14,39 @@
export let game: Game; export let game: Game;
export let gameManager: GameManager; export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.scene.getScene(LoginSceneName)); let gameScene = gameManager.getCurrentGameScene(game.findAnyScene());
let fileinput: HTMLInputElement; let fileInput: HTMLInputElement;
let filename: string; let fileName: string;
let filesize: string; let fileSize: string;
let errorfile: boolean; let errorFile: boolean;
const AUDIO_TYPE = AdminMessageEventTypes.audio; const AUDIO_TYPE = AdminMessageEventTypes.audio;
export const handleSending = {
async sendAudioMessage(broadcast: boolean) {
if (gameScene == undefined) {
return;
}
const inputAudio = HtmlUtils.getElementByIdOrFail<HTMLInputElement>("input-send-audio");
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
if (!selectedFile) {
errorFile = true;
throw 'no file selected';
}
async function SendAudioMessage() { const fd = new FormData();
if (gameScene == undefined) { fd.append('file', selectedFile);
return; const res = await gameScene.connection?.uploadAudio(fd);
}
const inputAudio = HtmlUtils.getElementByIdOrFail<HTMLInputElement>("input-send-audio");
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
if (!selectedFile) {
errorfile = true;
throw 'no file selected';
}
const fd = new FormData(); const audioGlobalMessage: PlayGlobalMessageInterface = {
fd.append('file', selectedFile); content: (res as { path: string }).path,
const res = await gameScene.connection?.uploadAudio(fd); type: AUDIO_TYPE,
broadcastToWorld: broadcast
const GlobalMessage: PlayGlobalMessageInterface = { }
id: (res as { id: string }).id, inputAudio.value = '';
message: (res as { path: string }).path, gameScene.connection?.emitGlobalMessage(audioGlobalMessage);
type: AUDIO_TYPE disableConsole();
} }
inputAudio.value = '';
gameScene.connection?.emitGlobalMessage(GlobalMessage);
disableConsole();
} }
function inputAudioFile(event: Event) { function inputAudioFile(event: Event) {
@ -60,9 +60,9 @@
return; return;
} }
filename = file.name; fileName = file.name;
filesize = getFileSize(file.size); fileSize = getFileSize(file.size);
errorfile = false; errorFile = false;
} }
function getFileSize(number: number) { function getFileSize(number: number) {
@ -85,46 +85,46 @@
<section class="section-input-send-audio"> <section class="section-input-send-audio">
<div class="input-send-audio"> <img class="nes-pointer" src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileInput.click();}}>
<img src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileinput.click();}}> {#if fileName !== undefined}
{#if filename != undefined} <p>{fileName} : {fileSize}</p>
<label for="input-send-audio">{filename} : {filesize}</label> {/if}
{/if} {#if errorFile}
{#if errorfile} <p class="err">No file selected. You need to upload a file before sending it.</p>
<p class="err">No file selected. You need to upload a file before sending it.</p> {/if}
{/if} <input type="file" id="input-send-audio" bind:this={fileInput} on:change={(e) => {inputAudioFile(e)}}>
<input type="file" id="input-send-audio" bind:this={fileinput} on:change={(e) => {inputAudioFile(e)}}>
</div>
<div class="btn-action">
<button class="nes-btn is-primary" on:click|preventDefault={SendAudioMessage}>Send</button>
</div>
</section> </section>
<style lang="scss"> <style lang="scss">
//UploadAudioGlobalMessage section.section-input-send-audio {
.section-input-send-audio { display: flex;
margin: 10px; flex-direction: column;
}
.section-input-send-audio .input-send-audio { height: 100%;
text-align: center; text-align: center;
}
.section-input-send-audio #input-send-audio{ img {
display: none; flex: 1 1 auto;
}
.section-input-send-audio div.input-send-audio label{ max-height: 80%;
color: white; margin-bottom: 20px;
} }
.section-input-send-audio div.input-send-audio p.err { p {
color: #ce372b; flex: 1 1 auto;
text-align: center;
}
.section-input-send-audio div.input-send-audio img{ margin-bottom: 5px;
height: 150px;
cursor: url('../../../style/images/cursor_pointer.png'), pointer; color: whitesmoke;
font-size: 1rem;
&.err {
color: #ce372b;
}
}
input {
display: none;
}
} }
</style> </style>

View File

@ -0,0 +1,96 @@
<script lang="ts">
import { fly } from "svelte/transition";
import {banMessageVisibleStore, banMessageContentStore} from "../../Stores/TypeMessageStore/BanMessageStore";
import {onMount} from "svelte";
const text = $banMessageContentStore;
const NAME_BUTTON = 'Ok';
let nbSeconds = 10;
let nameButton = '';
onMount(() => {
timeToRead()
})
function timeToRead() {
nbSeconds -= 1;
nameButton = nbSeconds.toString();
if ( nbSeconds > 0 ) {
setTimeout( () => {
timeToRead();
}, 1000);
} else {
nameButton = NAME_BUTTON;
}
}
function closeBanMessage() {
banMessageVisibleStore.set(false);
}
</script>
<div class="main-ban-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
<h2 class="title-ban-message"><img src="resources/logos/report.svg" alt="***"/> Important message <img src="resources/logos/report.svg" alt="***"/></h2>
<div class="content-ban-message">
<p>{text}</p>
</div>
<div class="footer-ban-message">
<button type="button" class="nes-btn {nameButton === NAME_BUTTON ? 'is-primary' : 'is-error'}" disabled="{!(nameButton === NAME_BUTTON)}" on:click|preventDefault={closeBanMessage}>{nameButton}</button>
</div>
<audio id="report-message" autoplay>
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
</audio>
</div>
<style lang="scss">
div.main-ban-message {
display: flex;
flex-direction: column;
position: relative;
top: 15vh;
height: 70vh;
width: 60vw;
margin-left: auto;
margin-right: auto;
padding-bottom: 0;
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
h2.title-ban-message {
flex: 1 1 auto;
max-height: 50px;
margin-bottom: 20px;
text-align: center;
img {
height: 50px;
}
}
div.content-ban-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
overflow: auto;
p {
white-space: pre-wrap;
}
}
div.footer-ban-message {
height: 50px;
margin-top: 10px;
text-align: center;
button {
width: 88px;
height: 44px;
}
}
}
</style>

View File

@ -0,0 +1,59 @@
<script lang="ts">
import { fly } from "svelte/transition";
import {textMessageContentStore, textMessageVisibleStore} from "../../Stores/TypeMessageStore/TextMessageStore";
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
const content = JSON.parse($textMessageContentStore);
const converter = new QuillDeltaToHtmlConverter(content.ops, {inlineStyles: true});
const NAME_BUTTON = 'Ok';
function closeTextMessage() {
textMessageVisibleStore.set(false);
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
closeTextMessage();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<div class="main-text-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
<div class="content-text-message">
{@html converter.convert()}
</div>
<div class="footer-text-message">
<button type="button" class="nes-btn is-primary" on:click|preventDefault={closeTextMessage}>{NAME_BUTTON}</button>
</div>
</div>
<style lang="scss">
div.main-text-message {
display: flex;
flex-direction: column;
max-height: 25vh;
width: 80vw;
margin-right: auto;
margin-left: auto;
padding-bottom: 0;
pointer-events: auto;
background-color: #333333;
div.content-text-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
color: whitesmoke;
overflow: auto;
}
div.footer-text-message {
height: 50px;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,37 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import {userIsAdminStore} from "../../Stores/GameStore";
import {ADMIN_URL} from "../../Enum/EnvironmentVariable";
const upgradeLink = ADMIN_URL+'/pricing';
</script>
<main class="warningMain" transition:fly="{{ y: -200, duration: 500 }}">
<h2>Warning!</h2>
{#if $userIsAdminStore}
<p>This world is close to its limit!. You can upgrade its capacity <a href="{upgradeLink}" target="_blank">here</a></p>
{:else}
<p>This world is close to its limit!</p>
{/if}
</main>
<style lang="scss">
main.warningMain {
pointer-events: auto;
width: 100vw;
background-color: red;
text-align: center;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
font-family: Lato;
min-width: 300px;
opacity: 0.9;
z-index: 2;
h2 {
padding: 5px;
}
}
</style>

View File

@ -6,6 +6,7 @@ import { GameConnexionTypes, urlManager } from "../Url/UrlManager";
import { localUserStore } from "./LocalUserStore"; import { localUserStore } from "./LocalUserStore";
import { CharacterTexture, LocalUser } from "./LocalUser"; import { CharacterTexture, LocalUser } from "./LocalUser";
import { Room } from "./Room"; import { Room } from "./Room";
import { _ServiceWorker } from "../Network/ServiceWorker";
class ConnectionManager { class ConnectionManager {
private localUser!: LocalUser; private localUser!: LocalUser;
@ -13,6 +14,9 @@ class ConnectionManager {
private connexionType?: GameConnexionTypes; private connexionType?: GameConnexionTypes;
private reconnectingTimeout: NodeJS.Timeout | null = null; private reconnectingTimeout: NodeJS.Timeout | null = null;
private _unloading: boolean = false; private _unloading: boolean = false;
private authToken: string | null = null;
private serviceWorker?: _ServiceWorker;
get unloading() { get unloading() {
return this._unloading; return this._unloading;
@ -24,23 +28,58 @@ class ConnectionManager {
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout); if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout);
}); });
} }
public loadOpenIDScreen() {
localUserStore.setAuthToken(null);
const state = localUserStore.generateState();
const nonce = localUserStore.generateNonce();
window.location.assign(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`);
}
public logout() {
localUserStore.setAuthToken(null);
window.location.reload();
}
/** /**
* Tries to login to the node server and return the starting map url to be loaded * Tries to login to the node server and return the starting map url to be loaded
*/ */
public async initGameConnexion(): Promise<Room> { public async initGameConnexion(): Promise<Room> {
const connexionType = urlManager.getGameConnexionType(); const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType; this.connexionType = connexionType;
if (connexionType === GameConnexionTypes.register) { let room: Room | null = null;
if (connexionType === GameConnexionTypes.jwt) {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
const state = urlParams.get("state");
if (!state || !localUserStore.verifyState(state)) {
throw "Could not validate state!";
}
if (!code) {
throw "No Auth code provided";
}
const nonce = localUserStore.getNonce();
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce } }).then(
(res) => res.data
);
localUserStore.setAuthToken(authToken);
this.authToken = authToken;
room = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(room);
} else if (connexionType === GameConnexionTypes.register) {
//@deprecated
const organizationMemberToken = urlManager.getOrganizationToken(); const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then( const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
(res) => res.data (res) => res.data
); );
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures); this.localUser = new LocalUser(data.userUuid, data.textures);
this.authToken = data.authToken;
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);
localUserStore.setAuthToken(this.authToken);
const roomUrl = data.roomUrl; const roomUrl = data.roomUrl;
const room = await Room.createRoom( room = await Room.createRoom(
new URL( new URL(
window.location.protocol + window.location.protocol +
"//" + "//" +
@ -51,30 +90,17 @@ class ConnectionManager {
) )
); );
urlManager.pushRoomIdToUrl(room); urlManager.pushRoomIdToUrl(room);
return Promise.resolve(room);
} else if ( } else if (
connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.organization ||
connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.anonymous ||
connexionType === GameConnexionTypes.empty connexionType === GameConnexionTypes.empty
) { ) {
let localUser = localUserStore.getLocalUser(); this.authToken = localUserStore.getAuthToken();
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) { //todo: add here some kind of warning if authToken has expired.
this.localUser = localUser; if (!this.authToken) {
try {
await this.verifyToken(localUser.jwtToken);
} catch (e) {
// If the token is invalid, let's generate an anonymous one.
console.error("JWT token invalid. Did it expire? Login anonymously instead.");
await this.anonymousLogin();
}
} else {
await this.anonymousLogin(); await this.anonymousLogin();
} }
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
localUser = localUserStore.getLocalUser();
if (!localUser) {
throw "Error to store local user data";
}
let roomPath: string; let roomPath: string;
if (connexionType === GameConnexionTypes.empty) { if (connexionType === GameConnexionTypes.empty) {
@ -90,44 +116,44 @@ class ConnectionManager {
} }
//get detail map for anonymous login and set texture in local storage //get detail map for anonymous login and set texture in local storage
const room = await Room.createRoom(new URL(roomPath)); room = await Room.createRoom(new URL(roomPath));
if (room.textures != undefined && room.textures.length > 0) { if (room.textures != undefined && room.textures.length > 0) {
//check if texture was changed //check if texture was changed
if (localUser.textures.length === 0) { if (this.localUser.textures.length === 0) {
localUser.textures = room.textures; this.localUser.textures = room.textures;
} else { } else {
room.textures.forEach((newTexture) => { room.textures.forEach((newTexture) => {
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id); const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
if (localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1) { if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
return; return;
} }
localUser?.textures.push(newTexture); this.localUser.textures.push(newTexture);
}); });
} }
this.localUser = localUser; localUserStore.saveUser(this.localUser);
localUserStore.saveUser(localUser);
} }
return Promise.resolve(room); }
if (room == undefined) {
return Promise.reject(new Error("Invalid URL"));
} }
return Promise.reject(new Error("Invalid URL")); this.serviceWorker = new _ServiceWorker();
} return Promise.resolve(room);
private async verifyToken(token: string): Promise<void> {
await Axios.get(`${PUSHER_URL}/verify`, { params: { token } });
} }
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> { public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${PUSHER_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, []); this.localUser = new LocalUser(data.userUuid, []);
this.authToken = data.authToken;
if (!isBenchmark) { if (!isBenchmark) {
// In benchmark, we don't have a local storage. // In benchmark, we don't have a local storage.
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);
localUserStore.setAuthToken(this.authToken);
} }
} }
public initBenchmark(): void { public initBenchmark(): void {
this.localUser = new LocalUser("", "test", []); this.localUser = new LocalUser("", []);
} }
public connectToRoomSocket( public connectToRoomSocket(
@ -140,7 +166,7 @@ class ConnectionManager {
): Promise<OnConnectInterface> { ): Promise<OnConnectInterface> {
return new Promise<OnConnectInterface>((resolve, reject) => { return new Promise<OnConnectInterface>((resolve, reject) => {
const connection = new RoomConnection( const connection = new RoomConnection(
this.localUser.jwtToken, this.authToken,
roomUrl, roomUrl,
name, name,
characterLayers, characterLayers,
@ -148,6 +174,7 @@ class ConnectionManager {
viewport, viewport,
companion companion
); );
connection.onConnectError((error: object) => { connection.onConnectError((error: object) => {
console.log("An error occurred while connecting to socket server. Retrying"); console.log("An error occurred while connecting to socket server. Retrying");
reject(error); reject(error);
@ -166,6 +193,9 @@ class ConnectionManager {
}); });
connection.onConnect((connect: OnConnectInterface) => { connection.onConnect((connect: OnConnectInterface) => {
//save last room url connected
localUserStore.setLastRoomUrl(roomUrl);
resolve(connect); resolve(connect);
}); });
}).catch((err) => { }).catch((err) => {

View File

@ -110,9 +110,9 @@ export interface RoomJoinedMessageInterface {
} }
export interface PlayGlobalMessageInterface { export interface PlayGlobalMessageInterface {
id: string;
type: string; type: string;
message: string; content: string;
broadcastToWorld: boolean;
} }
export interface OnConnectInterface { export interface OnConnectInterface {

View File

@ -1,10 +1,10 @@
import {MAX_USERNAME_LENGTH} from "../Enum/EnvironmentVariable"; import { MAX_USERNAME_LENGTH } from "../Enum/EnvironmentVariable";
export interface CharacterTexture { export interface CharacterTexture {
id: number, id: number;
level: number, level: number;
url: string, url: string;
rights: string rights: string;
} }
export const maxUserNameLength: number = MAX_USERNAME_LENGTH; export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
@ -24,6 +24,5 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
} }
export class LocalUser { export class LocalUser {
constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) { constructor(public readonly uuid: string, public textures: CharacterTexture[]) {}
}
} }

View File

@ -1,60 +1,65 @@
import {areCharacterLayersValid, isUserNameValid, LocalUser} from "./LocalUser"; import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
import { v4 as uuidv4 } from "uuid";
const playerNameKey = 'playerName'; const playerNameKey = "playerName";
const selectedPlayerKey = 'selectedPlayer'; const selectedPlayerKey = "selectedPlayer";
const customCursorPositionKey = 'customCursorPosition'; const customCursorPositionKey = "customCursorPosition";
const characterLayersKey = 'characterLayers'; const characterLayersKey = "characterLayers";
const companionKey = 'companion'; const companionKey = "companion";
const gameQualityKey = 'gameQuality'; const gameQualityKey = "gameQuality";
const videoQualityKey = 'videoQuality'; const videoQualityKey = "videoQuality";
const audioPlayerVolumeKey = 'audioVolume'; const audioPlayerVolumeKey = "audioVolume";
const audioPlayerMuteKey = 'audioMute'; const audioPlayerMuteKey = "audioMute";
const helpCameraSettingsShown = 'helpCameraSettingsShown'; const helpCameraSettingsShown = "helpCameraSettingsShown";
const fullscreenKey = 'fullscreen'; const fullscreenKey = "fullscreen";
const lastRoomUrl = "lastRoomUrl";
const authToken = "authToken";
const state = "state";
const nonce = "nonce";
class LocalUserStore { class LocalUserStore {
saveUser(localUser: LocalUser) { saveUser(localUser: LocalUser) {
localStorage.setItem('localUser', JSON.stringify(localUser)); localStorage.setItem("localUser", JSON.stringify(localUser));
} }
getLocalUser(): LocalUser|null { getLocalUser(): LocalUser | null {
const data = localStorage.getItem('localUser'); const data = localStorage.getItem("localUser");
return data ? JSON.parse(data) : null; return data ? JSON.parse(data) : null;
} }
setName(name:string): void { setName(name: string): void {
localStorage.setItem(playerNameKey, name); localStorage.setItem(playerNameKey, name);
} }
getName(): string|null { getName(): string | null {
const value = localStorage.getItem(playerNameKey) || ''; const value = localStorage.getItem(playerNameKey) || "";
return isUserNameValid(value) ? value : null; return isUserNameValid(value) ? value : null;
} }
setPlayerCharacterIndex(playerCharacterIndex: number): void { setPlayerCharacterIndex(playerCharacterIndex: number): void {
localStorage.setItem(selectedPlayerKey, ''+playerCharacterIndex); localStorage.setItem(selectedPlayerKey, "" + playerCharacterIndex);
} }
getPlayerCharacterIndex(): number { getPlayerCharacterIndex(): number {
return parseInt(localStorage.getItem(selectedPlayerKey) || ''); return parseInt(localStorage.getItem(selectedPlayerKey) || "");
} }
setCustomCursorPosition(activeRow:number, selectedLayers: number[]): void { setCustomCursorPosition(activeRow: number, selectedLayers: number[]): void {
localStorage.setItem(customCursorPositionKey, JSON.stringify({activeRow, selectedLayers})); localStorage.setItem(customCursorPositionKey, JSON.stringify({ activeRow, selectedLayers }));
} }
getCustomCursorPosition(): {activeRow:number, selectedLayers:number[]}|null { getCustomCursorPosition(): { activeRow: number; selectedLayers: number[] } | null {
return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null"); return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null");
} }
setCharacterLayers(layers: string[]): void { setCharacterLayers(layers: string[]): void {
localStorage.setItem(characterLayersKey, JSON.stringify(layers)); localStorage.setItem(characterLayersKey, JSON.stringify(layers));
} }
getCharacterLayers(): string[]|null { getCharacterLayers(): string[] | null {
const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null"); const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null");
return areCharacterLayersValid(value) ? value : null; return areCharacterLayersValid(value) ? value : null;
} }
setCompanion(companion: string|null): void { setCompanion(companion: string | null): void {
return localStorage.setItem(companionKey, JSON.stringify(companion)); return localStorage.setItem(companionKey, JSON.stringify(companion));
} }
getCompanion(): string|null { getCompanion(): string | null {
const companion = JSON.parse(localStorage.getItem(companionKey) || "null"); const companion = JSON.parse(localStorage.getItem(companionKey) || "null");
if (typeof companion !== "string" || companion === "") { if (typeof companion !== "string" || companion === "") {
@ -68,45 +73,82 @@ class LocalUserStore {
} }
setGameQualityValue(value: number): void { setGameQualityValue(value: number): void {
localStorage.setItem(gameQualityKey, '' + value); localStorage.setItem(gameQualityKey, "" + value);
} }
getGameQualityValue(): number { getGameQualityValue(): number {
return parseInt(localStorage.getItem(gameQualityKey) || '60'); return parseInt(localStorage.getItem(gameQualityKey) || "60");
} }
setVideoQualityValue(value: number): void { setVideoQualityValue(value: number): void {
localStorage.setItem(videoQualityKey, '' + value); localStorage.setItem(videoQualityKey, "" + value);
} }
getVideoQualityValue(): number { getVideoQualityValue(): number {
return parseInt(localStorage.getItem(videoQualityKey) || '20'); return parseInt(localStorage.getItem(videoQualityKey) || "20");
} }
setAudioPlayerVolume(value: number): void { setAudioPlayerVolume(value: number): void {
localStorage.setItem(audioPlayerVolumeKey, '' + value); localStorage.setItem(audioPlayerVolumeKey, "" + value);
} }
getAudioPlayerVolume(): number { getAudioPlayerVolume(): number {
return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || '1'); return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || "1");
} }
setAudioPlayerMuted(value: boolean): void { setAudioPlayerMuted(value: boolean): void {
localStorage.setItem(audioPlayerMuteKey, value.toString()); localStorage.setItem(audioPlayerMuteKey, value.toString());
} }
getAudioPlayerMuted(): boolean { getAudioPlayerMuted(): boolean {
return localStorage.getItem(audioPlayerMuteKey) === 'true'; return localStorage.getItem(audioPlayerMuteKey) === "true";
} }
setHelpCameraSettingsShown(): void { setHelpCameraSettingsShown(): void {
localStorage.setItem(helpCameraSettingsShown, '1'); localStorage.setItem(helpCameraSettingsShown, "1");
} }
getHelpCameraSettingsShown(): boolean { getHelpCameraSettingsShown(): boolean {
return localStorage.getItem(helpCameraSettingsShown) === '1'; return localStorage.getItem(helpCameraSettingsShown) === "1";
} }
setFullscreen(value: boolean): void { setFullscreen(value: boolean): void {
localStorage.setItem(fullscreenKey, value.toString()); localStorage.setItem(fullscreenKey, value.toString());
} }
getFullscreen(): boolean { getFullscreen(): boolean {
return localStorage.getItem(fullscreenKey) === 'true'; return localStorage.getItem(fullscreenKey) === "true";
}
setLastRoomUrl(roomUrl: string): void {
localStorage.setItem(lastRoomUrl, roomUrl.toString());
}
getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? "";
}
setAuthToken(value: string | null) {
value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken);
}
getAuthToken(): string | null {
return localStorage.getItem(authToken);
}
generateState(): string {
const newState = uuidv4();
localStorage.setItem(state, newState);
return newState;
}
verifyState(value: string): boolean {
const oldValue = localStorage.getItem(state);
localStorage.removeItem(state);
return oldValue === value;
}
generateNonce(): string {
const newNonce = uuidv4();
localStorage.setItem(nonce, newNonce);
return newNonce;
}
getNonce(): string | null {
const oldValue = localStorage.getItem(nonce);
localStorage.removeItem(nonce);
return oldValue;
} }
} }

View File

@ -55,9 +55,9 @@ import {
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
import { adminMessagesService } from "./AdminMessagesService"; import { adminMessagesService } from "./AdminMessagesService";
import { worldFullMessageStream } from "./WorldFullMessageStream"; import { worldFullMessageStream } from "./WorldFullMessageStream";
import { worldFullWarningStream } from "./WorldFullWarningStream";
import { connectionManager } from "./ConnectionManager"; import { connectionManager } from "./ConnectionManager";
import { emoteEventStream } from "./EmoteEventStream"; import { emoteEventStream } from "./EmoteEventStream";
import { warningContainerStore } from "../Stores/MenuStore";
const manualPingDelay = 20000; const manualPingDelay = 20000;
@ -76,7 +76,7 @@ export class RoomConnection implements RoomConnection {
/** /**
* *
* @param token A JWT token containing the UUID of the user * @param token A JWT token containing the email of the user
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]" * @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
*/ */
public constructor( public constructor(
@ -217,6 +217,9 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasWorldfullmessage()) { } else if (message.hasWorldfullmessage()) {
worldFullMessageStream.onMessage(); worldFullMessageStream.onMessage();
this.closed = true; this.closed = true;
} else if (message.hasTokenexpiredmessage()) {
connectionManager.loadOpenIDScreen();
this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
} else if (message.hasWorldconnexionmessage()) { } else if (message.hasWorldconnexionmessage()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
this.closed = true; this.closed = true;
@ -244,7 +247,7 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasBanusermessage()) { } else if (message.hasBanusermessage()) {
adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage); adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage);
} else if (message.hasWorldfullwarningmessage()) { } else if (message.hasWorldfullwarningmessage()) {
worldFullWarningStream.onMessage(); warningContainerStore.activateWarningContainer();
} else if (message.hasRefreshroommessage()) { } else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed. //todo: implement a way to notify the user the room was refreshed.
} else { } else {
@ -594,7 +597,7 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) { /* public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => { return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
callback({ callback({
id: message.getId(), id: message.getId(),
@ -602,7 +605,7 @@ export class RoomConnection implements RoomConnection {
message: message.getMessage(), message: message.getMessage(),
}); });
}); });
} }*/
public receiveStopGlobalMessage(callback: (messageId: string) => void) { public receiveStopGlobalMessage(callback: (messageId: string) => void) {
return this.onMessage(EventMessage.STOP_GLOBAL_MESSAGE, (message: StopGlobalMessage) => { return this.onMessage(EventMessage.STOP_GLOBAL_MESSAGE, (message: StopGlobalMessage) => {
@ -616,11 +619,11 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public emitGlobalMessage(message: PlayGlobalMessageInterface) { public emitGlobalMessage(message: PlayGlobalMessageInterface): void {
const playGlobalMessage = new PlayGlobalMessage(); const playGlobalMessage = new PlayGlobalMessage();
playGlobalMessage.setId(message.id);
playGlobalMessage.setType(message.type); playGlobalMessage.setType(message.type);
playGlobalMessage.setMessage(message.message); playGlobalMessage.setContent(message.content);
playGlobalMessage.setBroadcasttoworld(message.broadcastToWorld);
const clientToServerMessage = new ClientToServerMessage(); const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setPlayglobalmessage(playGlobalMessage); clientToServerMessage.setPlayglobalmessage(playGlobalMessage);

View File

@ -1,14 +0,0 @@
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,22 +1,24 @@
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json'; const START_ROOM_URL: string =
const PUSHER_URL = process.env.PUSHER_URL || '//pusher.workadventure.localhost'; process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor1/floor1.json";
const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost'; const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost";
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re";
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302"; const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = process.env.TURN_SERVER || ""; const TURN_SERVER: string = process.env.TURN_SERVER || "";
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true"; const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true";
const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true"; const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true";
const TURN_USER: string = process.env.TURN_USER || ''; const TURN_USER: string = process.env.TURN_USER || "";
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || ''; const TURN_PASSWORD: string = process.env.TURN_PASSWORD || "";
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true"; const JITSI_PRIVATE_MODE: boolean = process.env.JITSI_PRIVATE_MODE == "true";
const POSITION_DELAY = 200; // Wait 200ms between sending position events 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 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 const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || "") || 8;
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4'); export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == 'true'; export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true";
export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window.innerHeight <= 600 ) ); export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
export { export {
DEBUG_MODE, DEBUG_MODE,
@ -32,5 +34,5 @@ export {
TURN_USER, TURN_USER,
TURN_PASSWORD, TURN_PASSWORD,
JITSI_URL, JITSI_URL,
JITSI_PRIVATE_MODE JITSI_PRIVATE_MODE,
} };

View File

@ -0,0 +1,20 @@
export class _ServiceWorker {
constructor() {
if ("serviceWorker" in navigator) {
this.init();
}
}
init() {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/resources/service-worker.js")
.then((serviceWorker) => {
console.info("Service Worker registered: ", serviceWorker);
})
.catch((error) => {
console.error("Error registering the Service Worker: ", error);
});
});
}
}

View File

@ -1,10 +1,11 @@
import type { ITiledMapObject } from '../Map/ITiledMap'; import type { ITiledMapObject } from "../Map/ITiledMap";
import type { GameScene } from '../Game/GameScene'; import type { GameScene } from "../Game/GameScene";
import { type } from "os";
export class TextUtils { export class TextUtils {
public static createTextFromITiledMapObject(scene: GameScene, object: ITiledMapObject): void { public static createTextFromITiledMapObject(scene: GameScene, object: ITiledMapObject): void {
if (object.text === undefined) { if (object.text === undefined) {
throw new Error('This object has not textual representation.'); throw new Error("This object has not textual representation.");
} }
const options: { const options: {
fontStyle?: string; fontStyle?: string;
@ -18,18 +19,25 @@ export class TextUtils {
}; };
} = {}; } = {};
if (object.text.italic) { if (object.text.italic) {
options.fontStyle = 'italic'; options.fontStyle = "italic";
} }
// Note: there is no support for "strikeout" and "underline" // Note: there is no support for "strikeout" and "underline"
let fontSize: number = 16; let fontSize: number = 16;
if (object.text.pixelsize) { if (object.text.pixelsize) {
fontSize = object.text.pixelsize; fontSize = object.text.pixelsize;
} }
options.fontSize = fontSize + 'px'; options.fontSize = fontSize + "px";
if (object.text.fontfamily) { if (object.text.fontfamily) {
options.fontFamily = '"' + object.text.fontfamily + '"'; options.fontFamily = '"' + object.text.fontfamily + '"';
} }
let color = '#000000'; if (object.properties !== undefined) {
for (const property of object.properties) {
if (property.name === "font-family" && typeof property.value === "string") {
options.fontFamily = property.value;
}
}
}
let color = "#000000";
if (object.text.color !== undefined) { if (object.text.color !== undefined) {
color = object.text.color; color = object.text.color;
} }

View File

@ -1,14 +0,0 @@
export const warningContainerKey = 'warningContainer';
export const warningContainerHtml = 'resources/html/warningContainer.html';
export class WarningContainer extends Phaser.GameObjects.DOMElement {
constructor(scene: Phaser.Scene) {
super(scene, 100, 0);
this.setOrigin(0, 0);
this.createFromCache(warningContainerKey);
this.scene.add.existing(this);
}
}

View File

@ -107,7 +107,7 @@ export const createLoadingPromise = (
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig); loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig);
const errorCallback = (file: { src: string }) => { const errorCallback = (file: { src: string }) => {
if (file.src !== playerResourceDescriptor.img) return; if (file.src !== playerResourceDescriptor.img) return;
console.error("failed loading player ressource: ", playerResourceDescriptor); console.error("failed loading player resource: ", playerResourceDescriptor);
rej(playerResourceDescriptor); rej(playerResourceDescriptor);
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.name, successCallback); loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.name, successCallback);
loadPlugin.off("loaderror", errorCallback); loadPlugin.off("loaderror", errorCallback);

View File

@ -1,7 +1,7 @@
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable"; import { SKIP_RENDER_OPTIMIZATIONS } from "../../Enum/EnvironmentVariable";
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import {waScaleManager} from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import {ResizableScene} from "../Login/ResizableScene"; import { ResizableScene } from "../Login/ResizableScene";
const Events = Phaser.Core.Events; const Events = Phaser.Core.Events;
@ -14,10 +14,8 @@ const Events = Phaser.Core.Events;
* It also automatically calls "onResize" on any scenes extending ResizableScene. * It also automatically calls "onResize" on any scenes extending ResizableScene.
*/ */
export class Game extends Phaser.Game { export class Game extends Phaser.Game {
private _isDirty = false; private _isDirty = false;
constructor(GameConfig: Phaser.Types.Core.GameConfig) { constructor(GameConfig: Phaser.Types.Core.GameConfig) {
super(GameConfig); super(GameConfig);
@ -27,7 +25,7 @@ export class Game extends Phaser.Game {
scene.onResize(); scene.onResize();
} }
} }
}) });
/*window.addEventListener('resize', (event) => { /*window.addEventListener('resize', (event) => {
// Let's trigger the onResize method of any active scene that is a ResizableScene // Let's trigger the onResize method of any active scene that is a ResizableScene
@ -39,11 +37,9 @@ export class Game extends Phaser.Game {
});*/ });*/
} }
public step(time: number, delta: number) public step(time: number, delta: number) {
{
// @ts-ignore // @ts-ignore
if (this.pendingDestroy) if (this.pendingDestroy) {
{
// @ts-ignore // @ts-ignore
return this.runDestroy(); return this.runDestroy();
} }
@ -100,15 +96,17 @@ export class Game extends Phaser.Game {
} }
// Loop through the scenes in forward order // Loop through the scenes in forward order
for (let i = 0; i < this.scene.scenes.length; i++) for (let i = 0; i < this.scene.scenes.length; i++) {
{
const scene = this.scene.scenes[i]; const scene = this.scene.scenes[i];
const sys = scene.sys; const sys = scene.sys;
if (sys.settings.visible && sys.settings.status >= Phaser.Scenes.LOADING && sys.settings.status < Phaser.Scenes.SLEEPING) if (
{ sys.settings.visible &&
sys.settings.status >= Phaser.Scenes.LOADING &&
sys.settings.status < Phaser.Scenes.SLEEPING
) {
// @ts-ignore // @ts-ignore
if(typeof scene.isDirty === 'function') { if (typeof scene.isDirty === "function") {
// @ts-ignore // @ts-ignore
const isDirty = scene.isDirty() || scene.tweens.getAllTweens().length > 0; const isDirty = scene.isDirty() || scene.tweens.getAllTweens().length > 0;
if (isDirty) { if (isDirty) {
@ -129,4 +127,11 @@ export class Game extends Phaser.Game {
public markDirty(): void { public markDirty(): void {
this._isDirty = true; this._isDirty = true;
} }
/**
* Return the first scene found in the game
*/
public findAnyScene(): Phaser.Scene {
return this.scene.getScenes()[0];
}
} }

View File

@ -1,5 +1,4 @@
import type { Subscription } from "rxjs"; import type { Subscription } from "rxjs";
import { GlobalMessageManager } from "../../Administration/GlobalMessageManager";
import { userMessageManager } from "../../Administration/UserMessageManager"; import { userMessageManager } from "../../Administration/UserMessageManager";
import { iframeListener } from "../../Api/IframeListener"; import { iframeListener } from "../../Api/IframeListener";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
@ -75,8 +74,6 @@ import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey }
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager"; import { EmoteManager } from "./EmoteManager";
import EVENT_TYPE = Phaser.Scenes.Events; import EVENT_TYPE = Phaser.Scenes.Events;
import RenderTexture = Phaser.GameObjects.RenderTexture;
import Tilemap = Phaser.Tilemaps.Tilemap;
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import AnimatedTiles from "phaser-animated-tiles"; import AnimatedTiles from "phaser-animated-tiles";
@ -85,10 +82,12 @@ import { soundManager } from "./SoundManager";
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import { isMessageReferenceEvent, isTriggerMessageEvent } from "../../Api/Events/ui/TriggerMessageEvent";
import { SharedVariablesManager } from "./SharedVariablesManager"; import { SharedVariablesManager } from "./SharedVariablesManager";
import { playersStore } from "../../Stores/PlayersStore"; import { playersStore } from "../../Stores/PlayersStore";
import { chatVisibilityStore } from "../../Stores/ChatStore"; import { chatVisibilityStore } from "../../Stores/ChatStore";
import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore";
import { isMessageReferenceEvent, isTriggerMessageEvent } from "../../Api/Events/ui/TriggerMessageEvent";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -156,7 +155,6 @@ export class GameScene extends DirtyScene {
private playersPositionInterpolator = new PlayersPositionInterpolator(); private playersPositionInterpolator = new PlayersPositionInterpolator();
public connection: RoomConnection | undefined; public connection: RoomConnection | undefined;
private simplePeer!: SimplePeer; private simplePeer!: SimplePeer;
private GlobalMessageManager!: GlobalMessageManager;
private connectionAnswerPromise: Promise<RoomJoinedMessageInterface>; private connectionAnswerPromise: Promise<RoomJoinedMessageInterface>;
private connectionAnswerPromiseResolve!: ( private connectionAnswerPromiseResolve!: (
value: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface> value: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface>
@ -195,7 +193,7 @@ export class GameScene extends DirtyScene {
private popUpElements: Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>(); private popUpElements: Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>();
private originalMapUrl: string | undefined; private originalMapUrl: string | undefined;
private pinchManager: PinchManager | undefined; private pinchManager: PinchManager | undefined;
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time. private mapTransitioning: boolean = false; //used to prevent transitions happening at the same time.
private emoteManager!: EmoteManager; private emoteManager!: EmoteManager;
private preloading: boolean = true; private preloading: boolean = true;
private startPositionCalculator!: StartPositionCalculator; private startPositionCalculator!: StartPositionCalculator;
@ -222,6 +220,9 @@ export class GameScene extends DirtyScene {
//hook preload scene //hook preload scene
preload(): void { preload(): void {
//initialize frame event of scripting API
this.listenToIframeEvents();
const localUser = localUserStore.getLocalUser(); const localUser = localUserStore.getLocalUser();
const textures = localUser?.textures; const textures = localUser?.textures;
if (textures) { if (textures) {
@ -437,7 +438,7 @@ export class GameScene extends DirtyScene {
this.characterLayers = gameManager.getCharacterLayers(); this.characterLayers = gameManager.getCharacterLayers();
this.companion = gameManager.getCompanion(); this.companion = gameManager.getCompanion();
//initalise map //initialise map
this.Map = this.add.tilemap(this.MapUrlFile); this.Map = this.add.tilemap(this.MapUrlFile);
const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => { this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => {
@ -550,7 +551,6 @@ export class GameScene extends DirtyScene {
); );
this.triggerOnMapLayerPropertyChange(); this.triggerOnMapLayerPropertyChange();
this.listenToIframeEvents();
if (!this.room.isDisconnected()) { if (!this.room.isDisconnected()) {
this.connect(); this.connect();
@ -605,6 +605,8 @@ export class GameScene extends DirtyScene {
playersStore.connectToRoomConnection(this.connection); playersStore.connectToRoomConnection(this.connection);
userIsAdminStore.set(this.connection.hasTag("admin"));
this.connection.onUserJoins((message: MessageUserJoined) => { this.connection.onUserJoins((message: MessageUserJoined) => {
const userMessage: AddPlayerInterface = { const userMessage: AddPlayerInterface = {
userId: message.userId, userId: message.userId,
@ -691,7 +693,6 @@ export class GameScene extends DirtyScene {
peerStore.connectToSimplePeer(this.simplePeer); peerStore.connectToSimplePeer(this.simplePeer);
screenSharingPeerStore.connectToSimplePeer(this.simplePeer); screenSharingPeerStore.connectToSimplePeer(this.simplePeer);
videoFocusStore.connectToSimplePeer(this.simplePeer); videoFocusStore.connectToSimplePeer(this.simplePeer);
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
const self = this; const self = this;
@ -1082,8 +1083,74 @@ ${escapedMessage}
for (const eventTile of eventTiles) { for (const eventTile of eventTiles) {
this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer); this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer);
} }
this.markDirty();
}) })
); );
iframeListener.registerAnswerer("loadTileset", (eventTileset) => {
return this.connectionAnswerPromise.then(() => {
const jsonTilesetDir = eventTileset.url.substr(0, eventTileset.url.lastIndexOf("/"));
//Initialise the firstgid to 1 because if there is no tileset in the tilemap, the firstgid will be 1
let newFirstgid = 1;
const lastTileset = this.mapFile.tilesets[this.mapFile.tilesets.length - 1];
if (lastTileset) {
//If there is at least one tileset in the tilemap then calculate the firstgid of the new tileset
newFirstgid = lastTileset.firstgid + lastTileset.tilecount;
}
return new Promise((resolve, reject) => {
this.load.on("filecomplete-json-" + eventTileset.url, () => {
let jsonTileset = this.cache.json.get(eventTileset.url);
const imageUrl = jsonTilesetDir + "/" + jsonTileset.image;
this.load.image(imageUrl, imageUrl);
this.load.on("filecomplete-image-" + imageUrl, () => {
//Add the firstgid of the tileset to the json file
jsonTileset = { ...jsonTileset, firstgid: newFirstgid };
this.mapFile.tilesets.push(jsonTileset);
this.Map.tilesets.push(
new Tileset(
jsonTileset.name,
jsonTileset.firstgid,
jsonTileset.tileWidth,
jsonTileset.tileHeight,
jsonTileset.margin,
jsonTileset.spacing,
jsonTileset.tiles
)
);
this.Terrains.push(
this.Map.addTilesetImage(
jsonTileset.name,
imageUrl,
jsonTileset.tilewidth,
jsonTileset.tileheight,
jsonTileset.margin,
jsonTileset.spacing
)
);
//destroy the tilemapayer because they are unique and we need to reuse their key and layerdData
for (const layer of this.Map.layers) {
layer.tilemapLayer.destroy(false);
}
//Create a new GameMap with the changed file
this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains);
//Destroy the colliders of the old tilemapLayer
this.physics.add.world.colliders.destroy();
//Create new colliders with the new GameMap
this.createCollisionWithPlayer();
//Create new trigger with the new GameMap
this.triggerOnMapLayerPropertyChange();
resolve(newFirstgid);
});
});
this.load.on("loaderror", () => {
console.error("Error while loading " + eventTileset.url + ".");
reject(-1);
});
this.load.json(eventTileset.url, eventTileset.url);
this.load.start();
});
});
});
iframeListener.registerAnswerer( iframeListener.registerAnswerer(
"triggerMessage", "triggerMessage",
@ -1171,7 +1238,7 @@ ${escapedMessage}
let targetRoom: Room; let targetRoom: Room;
try { try {
targetRoom = await Room.createRoom(roomUrl); targetRoom = await Room.createRoom(roomUrl);
} catch (e: unknown) { } catch (e) {
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e); console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
this.mapTransitioning = false; this.mapTransitioning = false;
return; return;
@ -1224,6 +1291,8 @@ ${escapedMessage}
this.peerStoreUnsubscribe(); this.peerStoreUnsubscribe();
this.chatVisibilityUnsubscribe(); this.chatVisibilityUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("loadTileset");
iframeListener.unregisterAnswerer("getMapData"); iframeListener.unregisterAnswerer("getMapData");
iframeListener.unregisterAnswerer("getState"); iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("triggerMessage"); iframeListener.unregisterAnswerer("triggerMessage");
@ -1300,7 +1369,7 @@ ${escapedMessage}
try { try {
const room = await Room.createRoom(exitRoomPath); const room = await Room.createRoom(exitRoomPath);
return gameManager.loadMap(room, this.scene); return gameManager.loadMap(room, this.scene);
} catch (e: unknown) { } catch (e) {
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e); console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
} }
} }

View File

@ -45,7 +45,7 @@ export class StartPositionCalculator {
/** /**
* *
* @param selectedLayer this is always the layer that is selected with the hash in the url * @param selectedLayer this is always the layer that is selected with the hash in the url
* @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} didnt yield any start points * @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} did not yield any start points
*/ */
public initPositionFromLayerName(selectedOrDefaultLayer: string | null, selectedLayer: string | null) { public initPositionFromLayerName(selectedOrDefaultLayer: string | null, selectedLayer: string | null) {
if (!selectedOrDefaultLayer) { if (!selectedOrDefaultLayer) {
@ -73,7 +73,7 @@ export class StartPositionCalculator {
/** /**
* *
* @param selectedLayer this is always the layer that is selected with the hash in the url * @param selectedLayer this is always the layer that is selected with the hash in the url
* @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} didnt yield any start points * @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} did not yield any start points
*/ */
private startUser(selectedOrDefaultLayer: ITiledMapTileLayer, selectedLayer: string | null): PositionInterface { private startUser(selectedOrDefaultLayer: ITiledMapTileLayer, selectedLayer: string | null): PositionInterface {
const tiles = selectedOrDefaultLayer.data; const tiles = selectedOrDefaultLayer.data;

View File

@ -244,6 +244,7 @@ export class CustomizeScene extends AbstractCharacterScene {
update(time: number, delta: number): void { update(time: number, delta: number): void {
if (this.lazyloadingAttempt) { if (this.lazyloadingAttempt) {
this.moveLayers(); this.moveLayers();
this.doMoveCursorHorizontally(this.moveHorizontally);
this.lazyloadingAttempt = false; this.lazyloadingAttempt = false;
} }

View File

@ -6,8 +6,6 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu"; import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import { GameConnexionTypes } from "../../Url/UrlManager"; import { GameConnexionTypes } from "../../Url/UrlManager";
import { WarningContainer, warningContainerHtml, warningContainerKey } from "../Components/WarningContainer";
import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream";
import { menuIconVisible } from "../../Stores/MenuStore"; import { menuIconVisible } from "../../Stores/MenuStore";
import { videoConstraintStore } from "../../Stores/MediaStore"; import { videoConstraintStore } from "../../Stores/MediaStore";
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore"; import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
@ -21,6 +19,7 @@ import { get } from "svelte/store";
import { playersStore } from "../../Stores/PlayersStore"; import { playersStore } from "../../Stores/PlayersStore";
import { mediaManager } from "../../WebRtc/MediaManager"; import { mediaManager } from "../../WebRtc/MediaManager";
import { chatVisibilityStore } from "../../Stores/ChatStore"; import { chatVisibilityStore } from "../../Stores/ChatStore";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
export const MenuSceneName = "MenuScene"; export const MenuSceneName = "MenuScene";
const gameMenuKey = "gameMenu"; const gameMenuKey = "gameMenu";
@ -45,8 +44,6 @@ export class MenuScene extends Phaser.Scene {
private gameQualityValue: number; private gameQualityValue: number;
private videoQualityValue: number; private videoQualityValue: number;
private menuButton!: Phaser.GameObjects.DOMElement; private menuButton!: Phaser.GameObjects.DOMElement;
private warningContainer: WarningContainer | null = null;
private warningContainerTimeout: NodeJS.Timeout | null = null;
private subscriptions = new Subscription(); private subscriptions = new Subscription();
constructor() { constructor() {
super({ key: MenuSceneName }); super({ key: MenuSceneName });
@ -91,7 +88,6 @@ export class MenuScene extends Phaser.Scene {
this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html"); this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html");
this.load.html(gameShare, "resources/html/gameShare.html"); this.load.html(gameShare, "resources/html/gameShare.html");
this.load.html(gameReportKey, gameReportRessource); this.load.html(gameReportKey, gameReportRessource);
this.load.html(warningContainerKey, warningContainerHtml);
} }
create() { create() {
@ -147,7 +143,6 @@ export class MenuScene extends Phaser.Scene {
this.menuElement.addListener("click"); this.menuElement.addListener("click");
this.menuElement.on("click", this.onMenuClick.bind(this)); this.menuElement.on("click", this.onMenuClick.bind(this));
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
chatVisibilityStore.subscribe((v) => { chatVisibilityStore.subscribe((v) => {
this.menuButton.setVisible(!v); this.menuButton.setVisible(!v);
}); });
@ -194,20 +189,6 @@ export class MenuScene extends Phaser.Scene {
}); });
} }
private showWorldCapacityWarning() {
if (!this.warningContainer) {
this.warningContainer = new WarningContainer(this);
}
if (this.warningContainerTimeout) {
clearTimeout(this.warningContainerTimeout);
}
this.warningContainerTimeout = setTimeout(() => {
this.warningContainer?.destroy();
this.warningContainer = null;
this.warningContainerTimeout = null;
}, 120000);
}
private closeSideMenu(): void { private closeSideMenu(): void {
if (!this.sideMenuOpened) return; if (!this.sideMenuOpened) return;
this.sideMenuOpened = false; this.sideMenuOpened = false;
@ -363,6 +344,9 @@ export class MenuScene extends Phaser.Scene {
case "editGameSettingsButton": case "editGameSettingsButton":
this.openGameSettingsMenu(); this.openGameSettingsMenu();
break; break;
case "oidcLogin":
connectionManager.loadOpenIDScreen();
break;
case "toggleFullscreen": case "toggleFullscreen":
this.toggleFullscreen(); this.toggleFullscreen();
break; break;
@ -403,6 +387,10 @@ export class MenuScene extends Phaser.Scene {
private gotToCreateMapPage() { private gotToCreateMapPage() {
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html'; //const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
//TODO fix me: this button can to send us on WorkAdventure BO. //TODO fix me: this button can to send us on WorkAdventure BO.
//const sparkHost = ADMIN_URL + "/getting-started";
//The redirection must be only on workadventu.re domain
//To day the domain staging cannot be use by customer
const sparkHost = "https://workadventu.re/getting-started"; const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank"); window.open(sparkHost, "_blank");
} }

View File

@ -1,12 +1,12 @@
import type { Direction } from "../../types"; import type { Direction } from "../../types";
import type {GameScene} from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import { touchScreenManager } from "../../Touch/TouchScreenManager";
import {MobileJoystick} from "../Components/MobileJoystick"; import { MobileJoystick } from "../Components/MobileJoystick";
import {enableUserInputsStore} from "../../Stores/UserInputStore"; import { enableUserInputsStore } from "../../Stores/UserInputStore";
interface UserInputManagerDatum { interface UserInputManagerDatum {
keyInstance: Phaser.Input.Keyboard.Key; keyInstance: Phaser.Input.Keyboard.Key;
event: UserInputEvent event: UserInputEvent;
} }
export enum UserInputEvent { export enum UserInputEvent {
@ -20,10 +20,9 @@ export enum UserInputEvent {
JoystickMove, JoystickMove,
} }
//we cannot use a map structure so we have to create a replacement
//we cannot use a map structure so we have to create a replacment
export class ActiveEventList { export class ActiveEventList {
private eventMap : Map<UserInputEvent, boolean> = new Map<UserInputEvent, boolean>(); private eventMap: Map<UserInputEvent, boolean> = new Map<UserInputEvent, boolean>();
get(event: UserInputEvent): boolean { get(event: UserInputEvent): boolean {
return this.eventMap.get(event) || false; return this.eventMap.get(event) || false;
@ -43,7 +42,7 @@ export class ActiveEventList {
export class UserInputManager { export class UserInputManager {
private KeysCode!: UserInputManagerDatum[]; private KeysCode!: UserInputManagerDatum[];
private Scene: GameScene; private Scene: GameScene;
private isInputDisabled : boolean; private isInputDisabled: boolean;
private joystick!: MobileJoystick; private joystick!: MobileJoystick;
private joystickEvents = new ActiveEventList(); private joystickEvents = new ActiveEventList();
@ -61,8 +60,8 @@ export class UserInputManager {
} }
enableUserInputsStore.subscribe((enable) => { enableUserInputsStore.subscribe((enable) => {
enable ? this.restoreControls() : this.disableControls() enable ? this.restoreControls() : this.disableControls();
}) });
} }
initVirtualJoystick() { initVirtualJoystick() {
@ -91,39 +90,81 @@ export class UserInputManager {
}); });
} }
initKeyBoardEvent(){ initKeyBoardEvent() {
this.KeysCode = [ this.KeysCode = [
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false) }, {
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false) }, event: UserInputEvent.MoveUp,
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false) }, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false),
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false) }, },
{event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false) }, {
{event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false) }, event: UserInputEvent.MoveUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false),
},
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false),
},
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false),
},
{
event: UserInputEvent.MoveDown,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false),
},
{
event: UserInputEvent.MoveRight,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false),
},
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false) }, {
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false) }, event: UserInputEvent.MoveUp,
{event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false) }, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false),
{event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false) }, },
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false),
},
{
event: UserInputEvent.MoveDown,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false),
},
{
event: UserInputEvent.MoveRight,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false),
},
{event: UserInputEvent.SpeedUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false) }, {
event: UserInputEvent.SpeedUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false),
},
{event: UserInputEvent.Interact, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false) }, {
{event: UserInputEvent.Interact, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false) }, event: UserInputEvent.Interact,
{event: UserInputEvent.Shout, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false) }, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false),
},
{
event: UserInputEvent.Interact,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false),
},
{
event: UserInputEvent.Shout,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),
},
]; ];
} }
clearAllListeners(){ clearAllListeners() {
this.Scene.input.keyboard.removeAllListeners(); this.Scene.input.keyboard.removeAllListeners();
} }
//todo: should we also disable the joystick? //todo: should we also disable the joystick?
disableControls(){ disableControls() {
this.Scene.input.keyboard.removeAllKeys(); this.Scene.input.keyboard.removeAllKeys();
this.isInputDisabled = true; this.isInputDisabled = true;
} }
restoreControls(){ restoreControls() {
this.initKeyBoardEvent(); this.initKeyBoardEvent();
this.isInputDisabled = false; this.isInputDisabled = false;
} }
@ -135,27 +176,27 @@ export class UserInputManager {
this.joystickEvents.forEach((value, key) => { this.joystickEvents.forEach((value, key) => {
if (value) { if (value) {
switch (key) { switch (key) {
case UserInputEvent.MoveUp: case UserInputEvent.MoveUp:
case UserInputEvent.MoveDown: case UserInputEvent.MoveDown:
this.joystickForceAccuY += this.joystick.forceY; this.joystickForceAccuY += this.joystick.forceY;
if (Math.abs(this.joystickForceAccuY) > this.joystickForceThreshold) { if (Math.abs(this.joystickForceAccuY) > this.joystickForceThreshold) {
eventsMap.set(key, value); eventsMap.set(key, value);
this.joystickForceAccuY = 0; this.joystickForceAccuY = 0;
} }
break; break;
case UserInputEvent.MoveLeft: case UserInputEvent.MoveLeft:
case UserInputEvent.MoveRight: case UserInputEvent.MoveRight:
this.joystickForceAccuX += this.joystick.forceX; this.joystickForceAccuX += this.joystick.forceX;
if (Math.abs(this.joystickForceAccuX) > this.joystickForceThreshold) { if (Math.abs(this.joystickForceAccuX) > this.joystickForceThreshold) {
eventsMap.set(key, value); eventsMap.set(key, value);
this.joystickForceAccuX = 0; this.joystickForceAccuX = 0;
} }
break; break;
} }
} }
}); });
eventsMap.set(UserInputEvent.JoystickMove, this.joystickEvents.any()); eventsMap.set(UserInputEvent.JoystickMove, this.joystickEvents.any());
this.KeysCode.forEach(d => { this.KeysCode.forEach((d) => {
if (d.keyInstance.isDown) { if (d.keyInstance.isDown) {
eventsMap.set(d.event, true); eventsMap.set(d.event, true);
} }
@ -163,18 +204,18 @@ export class UserInputManager {
return eventsMap; return eventsMap;
} }
spaceEvent(callback : Function){ spaceEvent(callback: Function) {
this.Scene.input.keyboard.on('keyup-SPACE', (event: Event) => { this.Scene.input.keyboard.on("keyup-SPACE", (event: Event) => {
callback(); callback();
return event; return event;
}); });
} }
addSpaceEventListner(callback : Function){ addSpaceEventListner(callback: Function) {
this.Scene.input.keyboard.addListener('keyup-SPACE', callback); this.Scene.input.keyboard.addListener("keyup-SPACE", callback);
} }
removeSpaceEventListner(callback : Function){ removeSpaceEventListner(callback: Function) {
this.Scene.input.keyboard.removeListener('keyup-SPACE', callback); this.Scene.input.keyboard.removeListener("keyup-SPACE", callback);
} }
destroy(): void { destroy(): void {
@ -182,8 +223,11 @@ export class UserInputManager {
} }
private initMouseWheel() { private initMouseWheel() {
this.Scene.input.on('wheel', (pointer: unknown, gameObjects: unknown, deltaX: number, deltaY: number, deltaZ: number) => { this.Scene.input.on(
this.Scene.zoomByFactor(1 - deltaY / 53 * 0.1); "wheel",
}); (pointer: unknown, gameObjects: unknown, deltaX: number, deltaY: number, deltaZ: number) => {
this.Scene.zoomByFactor(1 - (deltaY / 53) * 0.1);
}
);
} }
} }

View File

@ -2,4 +2,6 @@ import { writable } from "svelte/store";
export const userMovingStore = writable(false); export const userMovingStore = writable(false);
export const requestVisitCardsStore = writable<string|null>(null); export const requestVisitCardsStore = writable<string | null>(null);
export const userIsAdminStore = writable(false);

View File

@ -274,12 +274,12 @@ export const mediaStreamConstraintsStore = derived(
currentAudioConstraint = false; currentAudioConstraint = false;
} }
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone) // Disable webcam for privacy reasons (the game is not visible and we were talking to no one)
if ($privacyShutdownStore === true) { if ($privacyShutdownStore === true) {
currentVideoConstraint = false; currentVideoConstraint = false;
} }
// Disable webcam for energy reasons (the user is not moving and we are talking to noone) // Disable webcam for energy reasons (the user is not moving and we are talking to no one)
if ($cameraEnergySavingStore === true) { if ($cameraEnergySavingStore === true) {
currentVideoConstraint = false; currentVideoConstraint = false;
currentAudioConstraint = false; currentAudioConstraint = false;

View File

@ -1,3 +1,23 @@
import { derived, writable, Writable } from "svelte/store"; import { writable } from "svelte/store";
import Timeout = NodeJS.Timeout;
export const menuIconVisible = writable(false); export const menuIconVisible = writable(false);
let warningContainerTimeout: Timeout | null = null;
function createWarningContainerStore() {
const { subscribe, set } = writable<boolean>(false);
return {
subscribe,
activateWarningContainer() {
set(true);
if (warningContainerTimeout) clearTimeout(warningContainerTimeout);
warningContainerTimeout = setTimeout(() => {
set(false);
warningContainerTimeout = null;
}, 120000);
},
};
}
export const warningContainerStore = createWarningContainerStore();

View File

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

View File

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

View File

@ -1,45 +1,46 @@
import type {Room} from "../Connexion/Room"; import type { Room } from "../Connexion/Room";
export enum GameConnexionTypes { export enum GameConnexionTypes {
anonymous=1, anonymous = 1,
organization, organization,
register, register,
empty, empty,
unknown, unknown,
jwt,
} }
//this class is responsible with analysing and editing the game's url //this class is responsible with analysing and editing the game's url
class UrlManager { class UrlManager {
//todo: use that to detect if we can find a token in localstorage
public getGameConnexionType(): GameConnexionTypes { public getGameConnexionType(): GameConnexionTypes {
const url = window.location.pathname.toString(); const url = window.location.pathname.toString();
if (url.includes('_/')) { if (url === "/jwt") {
return GameConnexionTypes.jwt;
} else if (url.includes("_/")) {
return GameConnexionTypes.anonymous; return GameConnexionTypes.anonymous;
} else if (url.includes('@/')) { } else if (url.includes("@/")) {
return GameConnexionTypes.organization; return GameConnexionTypes.organization;
} else if(url.includes('register/')) { } else if (url.includes("register/")) {
return GameConnexionTypes.register; return GameConnexionTypes.register;
} else if(url === '/') { } else if (url === "/") {
return GameConnexionTypes.empty; return GameConnexionTypes.empty;
} else { } else {
return GameConnexionTypes.unknown; return GameConnexionTypes.unknown;
} }
} }
public getOrganizationToken(): string|null { public getOrganizationToken(): string | null {
const match = /\/register\/(.+)/.exec(window.location.pathname.toString()); const match = /\/register\/(.+)/.exec(window.location.pathname.toString());
return match ? match [1] : null; return match ? match[1] : null;
} }
public pushRoomIdToUrl(room:Room): void { public pushRoomIdToUrl(room: Room): void {
if (window.location.pathname === room.id) return; if (window.location.pathname === room.id) return;
const hash = window.location.hash; const hash = window.location.hash;
const search = room.search.toString(); const search = room.search.toString();
history.pushState({}, 'WorkAdventure', room.id+(search?'?'+search:'')+hash); history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);
} }
public getStartLayerNameFromUrl(): string|null { public getStartLayerNameFromUrl(): string | null {
const hash = window.location.hash; const hash = window.location.hash;
return hash.length > 1 ? hash.substring(1) : null; return hash.length > 1 ? hash.substring(1) : null;
} }

View File

@ -31,7 +31,7 @@ export class HtmlUtils {
return p.innerHTML; return p.innerHTML;
} }
public static urlify(text: string): string { public static urlify(text: string, style: string = ""): string {
const urlRegex = /(https?:\/\/[^\s]+)/g; const urlRegex = /(https?:\/\/[^\s]+)/g;
text = HtmlUtils.escapeHtml(text); text = HtmlUtils.escapeHtml(text);
return text.replace(urlRegex, (url: string) => { return text.replace(urlRegex, (url: string) => {
@ -40,10 +40,19 @@ export class HtmlUtils {
link.target = "_blank"; link.target = "_blank";
const text = document.createTextNode(url); const text = document.createTextNode(url);
link.appendChild(text); link.appendChild(text);
link.setAttribute("style", style);
return link.outerHTML; return link.outerHTML;
}); });
} }
public static isClickedInside(event: MouseEvent, target: HTMLElement): boolean {
return !!event.composedPath().find((et) => et === target);
}
public static isClickedOutside(event: MouseEvent, target: HTMLElement): boolean {
return !this.isClickedInside(event, target);
}
private static isHtmlElement<T extends HTMLElement>(elem: HTMLElement | null): elem is T { private static isHtmlElement<T extends HTMLElement>(elem: HTMLElement | null): elem is T {
return elem !== null; return elem !== null;
} }

View File

@ -295,7 +295,7 @@ export class SimplePeer {
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
peer.destroy(); peer.destroy();
//Comment this peer connexion because if we delete and try to reshare screen, the RTCPeerConnection send renegociate event. This array will be remove when user left circle discussion //Comment this peer connection because if we delete and try to reshare screen, the RTCPeerConnection send renegotiate event. This array will be remove when user left circle discussion
/*if(!this.PeerScreenSharingConnectionArray.delete(userId)){ /*if(!this.PeerScreenSharingConnectionArray.delete(userId)){
throw 'Couln\'t delete peer screen sharing connexion'; throw 'Couln\'t delete peer screen sharing connexion';
}*/ }*/
@ -370,14 +370,14 @@ export class SimplePeer {
console.error( console.error(
'Could not find peer whose ID is "' + data.userId + '" in receiveWebrtcScreenSharingSignal' 'Could not find peer whose ID is "' + data.userId + '" in receiveWebrtcScreenSharingSignal'
); );
console.info("Attempt to create new peer connexion"); console.info("Attempt to create new peer connection");
if (stream) { if (stream) {
this.sendLocalScreenSharingStreamToUser(data.userId, stream); this.sendLocalScreenSharingStreamToUser(data.userId, stream);
} }
} }
} catch (e) { } catch (e) {
console.error(`receiveWebrtcSignal => ${data.userId}`, e); console.error(`receiveWebrtcSignal => ${data.userId}`, e);
//Comment this peer connexion because if we delete and try to reshare screen, the RTCPeerConnection send renegociate event. This array will be remove when user left circle discussion //Comment this peer connection because if we delete and try to reshare screen, the RTCPeerConnection send renegotiate event. This array will be remove when user left circle discussion
//this.PeerScreenSharingConnectionArray.delete(data.userId); //this.PeerScreenSharingConnectionArray.delete(data.userId);
this.receiveWebrtcScreenSharingSignal(data); this.receiveWebrtcScreenSharingSignal(data);
} }
@ -485,7 +485,7 @@ export class SimplePeer {
if (!PeerConnectionScreenSharing.isReceivingScreenSharingStream()) { if (!PeerConnectionScreenSharing.isReceivingScreenSharingStream()) {
PeerConnectionScreenSharing.destroy(); PeerConnectionScreenSharing.destroy();
//Comment this peer connexion because if we delete and try to reshare screen, the RTCPeerConnection send renegociate event. This array will be remove when user left circle discussion //Comment this peer connection because if we delete and try to reshare screen, the RTCPeerConnection send renegotiate event. This array will be remove when user left circle discussion
//this.PeerScreenSharingConnectionArray.delete(userId); //this.PeerScreenSharingConnectionArray.delete(userId);
} }
} }

View File

@ -162,16 +162,3 @@ const app = new App({
}); });
export default app; export default app;
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker
.register("/resources/service-worker.js")
.then((serviceWorker) => {
console.log("Service Worker registered: ", serviceWorker);
})
.catch((error) => {
console.error("Error registering the Service Worker: ", error);
});
});
}

View File

@ -3,4 +3,4 @@
@import "style"; @import "style";
@import "mobile-style.scss"; @import "mobile-style.scss";
@import "fonts.scss"; @import "fonts.scss";
@import "svelte-style.scss"; @import "inputTextGlobalMessageSvelte-Style.scss";

View File

@ -0,0 +1,31 @@
//InputTextGlobalMessage
section.section-input-send-text {
height: 100%;
.ql-toolbar{
max-height: 100px;
background: whitesmoke;
}
div.input-send-text{
height: calc(100% - 100px);
overflow: auto;
color: whitesmoke;
font-size: 1rem;
.ql-editor.ql-blank::before {
color: whitesmoke;
font-size: 1rem;
}
.ql-tooltip {
top: 40% !important;
left: 20% !important;
color: whitesmoke;
background-color: #333333;
}
}
}

View File

@ -1,60 +0,0 @@
//Contains all styles not unique to a svelte component.
//ConsoleGlobalMessage
div.main-console.nes-container {
pointer-events: auto;
margin-left: auto;
margin-right: auto;
top: 20vh;
width: 50vw;
height: 50vh;
padding: 0;
background-color: #333333;
.btn-action{
margin: 10px;
text-align: center;
}
.main-global-message {
width: 100%;
max-height: 100%;
}
.main-global-message h2 {
text-align: center;
color: white;
}
div.global-message {
display: flex;
max-height: 100%;
width: 100%;
}
div.menu {
flex: auto;
}
div.menu button {
margin: 7px;
}
.main-input {
width: 95%;
}
//InputTextGlobalMessage
.section-input-send-text {
margin: 10px;
}
.section-input-send-text .input-send-text .ql-editor{
color: white;
min-height: 200px;
}
.section-input-send-text .ql-toolbar{
background: white;
}
}

View File

@ -1,28 +1,28 @@
import "jasmine"; import "jasmine";
import {getRessourceDescriptor} from "../../../src/Phaser/Entity/PlayerTexturesLoadingManager"; import { getRessourceDescriptor } from "../../../src/Phaser/Entity/PlayerTexturesLoadingManager";
describe("getRessourceDescriptor()", () => { describe("getRessourceDescriptor()", () => {
it(", if given a valid descriptor as parameter, should return it", () => { it(", if given a valid descriptor as parameter, should return it", () => {
const desc = getRessourceDescriptor({name: 'name', img: 'url'}); const desc = getRessourceDescriptor({ name: "name", img: "url" });
expect(desc.name).toEqual('name'); expect(desc.name).toEqual("name");
expect(desc.img).toEqual('url'); expect(desc.img).toEqual("url");
}); });
it(", if given a string as parameter, should search trough hardcoded values", () => { it(", if given a string as parameter, should search through hardcoded values", () => {
const desc = getRessourceDescriptor('male1'); const desc = getRessourceDescriptor("male1");
expect(desc.name).toEqual('male1'); expect(desc.name).toEqual("male1");
expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png"); expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png");
}); });
it(", if given a string as parameter, should search trough hardcoded values (bis)", () => { it(", if given a string as parameter, should search through hardcoded values (bis)", () => {
const desc = getRessourceDescriptor('color_2'); const desc = getRessourceDescriptor("color_2");
expect(desc.name).toEqual('color_2'); expect(desc.name).toEqual("color_2");
expect(desc.img).toEqual("resources/customisation/character_color/character_color1.png"); expect(desc.img).toEqual("resources/customisation/character_color/character_color1.png");
}); });
it(", if given a descriptor without url as parameter, should search trough hardcoded values", () => { it(", if given a descriptor without url as parameter, should search through hardcoded values", () => {
const desc = getRessourceDescriptor({name: 'male1', img: ''}); const desc = getRessourceDescriptor({ name: "male1", img: "" });
expect(desc.name).toEqual('male1'); expect(desc.name).toEqual("male1");
expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png"); expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png");
}); });
}); });

View File

@ -189,7 +189,7 @@ module.exports = {
DISABLE_NOTIFICATIONS: false, DISABLE_NOTIFICATIONS: false,
PUSHER_URL: undefined, PUSHER_URL: undefined,
UPLOADER_URL: null, UPLOADER_URL: null,
ADMIN_URL: null, ADMIN_URL: undefined,
DEBUG_MODE: null, DEBUG_MODE: null,
STUN_SERVER: null, STUN_SERVER: null,
TURN_SERVER: null, TURN_SERVER: null,

View File

@ -291,6 +291,18 @@
dependencies: dependencies:
source-map "^0.6.1" source-map "^0.6.1"
"@types/uuid@8.3.0":
version "8.3.0"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
"@types/uuidv4@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/uuidv4/-/uuidv4-5.0.0.tgz#2c94e67b0c06d5adb28fb7ced1a1b5f0866ecd50"
integrity sha512-xUrhYSJnkTq9CP79cU3svoKTLPCIbMMnu9Twf/tMpHATYSHCAAeDNeb2a/29YORhk5p4atHhCTMsIBU/tvdh6A==
dependencies:
uuidv4 "*"
"@types/webpack-dev-server@^3.11.4": "@types/webpack-dev-server@^3.11.4":
version "3.11.4" version "3.11.4"
resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.4.tgz#90d47dd660b696d409431ab8c1e9fa3615103a07" resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.4.tgz#90d47dd660b696d409431ab8c1e9fa3615103a07"
@ -3463,6 +3475,11 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.truncate@^4.4.2: lodash.truncate@^4.4.2:
version "4.4.2" version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
@ -4485,6 +4502,13 @@ queue-typescript@^1.0.1:
dependencies: dependencies:
linked-list-typescript "^1.0.11" linked-list-typescript "^1.0.11"
quill-delta-to-html@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/quill-delta-to-html/-/quill-delta-to-html-0.12.0.tgz#ee572cfa208390d99d7646795358ddb81eaea17c"
integrity sha512-Yy6U2e7ov+ZlrFbj5/GbqOBCRjyNu+vuphy0Pk+7668zIMTVHJglZY2JNa++1/zkSiqptPBmP/CpsDzC4Wznsw==
dependencies:
lodash.isequal "^4.5.0"
quill-delta@^3.6.2: quill-delta@^3.6.2:
version "3.6.3" version "3.6.3"
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032" resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032"
@ -5775,11 +5799,24 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^3.3.2, uuid@^3.4.0: uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuidv4@*, uuidv4@^6.2.10:
version "6.2.10"
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.10.tgz#42fc1c12b6f85ad536c2c5c1e836079d1e15003c"
integrity sha512-FMo1exd9l5UvoUPHRR6NrtJ/OJRePh0ca7IhPwBuMNuYRqjtuh8lE3WDxAUvZ4Yss5FbCOsPFjyWJf9lVTEmnw==
dependencies:
"@types/uuid" "8.3.0"
uuid "8.3.2"
v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0: v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"

View File

@ -176,7 +176,7 @@ Tuomo Untinen CC-BY-3.0
Casper Nilsson Casper Nilsson
~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Asain themed shrine including red lantern - Asian themed shrine including red lantern
- foodog statue - foodog statue
- Toro - Toro
- Cherry blossom tree - Cherry blossom tree

View File

@ -0,0 +1,159 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":1,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[33, 34, 33, 34, 34, 34, 35, 37, 38, 39, 41, 42, 41, 42, 42, 42, 43, 45, 46, 47, 33, 34, 60, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 49, 50, 50, 50, 50, 50, 51, 53, 54, 55],
"height":10,
"id":2,
"name":"bottom",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[57, 58, 0, 0, 0, 0, 0, 0, 0, 0, 59, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":3,
"name":"openwebsite",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"https:\/\/fr.wikipedia.org\/wiki\/Wikip%C3%A9dia:Accueil_principal"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1,
"orientation":"orthogonal",
"properties":[
{
"name":"script",
"type":"string",
"value":"scriptTileset.js"
}],
"renderorder":"right-down",
"tiledversion":"1.7.0",
"tileheight":32,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"tileset_dungeon.png",
"imageheight":256,
"imagewidth":256,
"margin":0,
"name":"Dungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tiles":[
{
"id":36,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":37,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":38,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":44,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":45,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":46,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":52,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":53,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":54,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":"1.6",
"width":10
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,106 @@
{ "columns":11,
"image":"Yellow.jpg",
"imageheight":128,
"imagewidth":352,
"margin":0,
"name":"Yellow",
"spacing":0,
"tilecount":44,
"tiledversion":"1.7.0",
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
},
{
"name":"name",
"type":"string",
"value":"Mur"
}]
},
{
"id":2,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":11,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":12,
"properties":[
{
"name":"name",
"type":"string",
"value":"sol"
},
{
"name":"openWebsite",
"type":"string",
"value":"https:\/\/fr.wikipedia.org\/wiki\/Wikip%C3%A9dia:Accueil_principal"
}]
},
{
"id":13,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":22,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":23,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":24,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32,
"type":"tileset",
"version":"1.6"
}

View File

@ -0,0 +1,6 @@
WA.room.loadTileset("http://maps.workadventure.localhost/tests/LoadTileset/Yellow.json").then((firstgid) => {
WA.room.setTiles([
{x: 5, y: 5, tile: firstgid + 1, layer: 'bottom'},
{x: 5, y: 3, tile: 'sol', layer: 'bottom'}
]);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -58,11 +58,17 @@
"height":94.6489098314831, "height":94.6489098314831,
"id":1, "id":1,
"name":"", "name":"",
"properties":[
{
"name":"font-family",
"type":"string",
"value":"\"Press Start 2P\""
}],
"rotation":0, "rotation":0,
"text": "text":
{ {
"fontfamily":"Sans Serif", "fontfamily":"Sans Serif",
"pixelsize":11, "pixelsize":8,
"text":"Test:\nWalk on the carpet and press space\nResult:\nJitsi opens on meet.jit.si (check this in the network tab). Note: this test only makes sense if the default configured Jitsi instance is NOT meet.jit.si (check your .env file)", "text":"Test:\nWalk on the carpet and press space\nResult:\nJitsi opens on meet.jit.si (check this in the network tab). Note: this test only makes sense if the default configured Jitsi instance is NOT meet.jit.si (check your .env file)",
"wrap":true "wrap":true
}, },

View File

@ -123,9 +123,9 @@ message VariableWithTagMessage {
} }
message PlayGlobalMessage { message PlayGlobalMessage {
string id = 1; string type = 1;
string type = 2; string content = 2;
string message = 3; bool broadcastToWorld = 3;
} }
message StopGlobalMessage { message StopGlobalMessage {
@ -247,6 +247,8 @@ message RefreshRoomMessage{
message WorldFullMessage{ message WorldFullMessage{
} }
message TokenExpiredMessage{
}
message WorldConnexionMessage{ message WorldConnexionMessage{
string message = 2; string message = 2;
@ -278,6 +280,7 @@ message ServerToClientMessage {
RefreshRoomMessage refreshRoomMessage = 17; RefreshRoomMessage refreshRoomMessage = 17;
WorldConnexionMessage worldConnexionMessage = 18; WorldConnexionMessage worldConnexionMessage = 18;
EmoteEventMessage emoteEventMessage = 19; EmoteEventMessage emoteEventMessage = 19;
TokenExpiredMessage tokenExpiredMessage = 20;
} }
} }
@ -442,6 +445,7 @@ message AdminMessage {
message AdminRoomMessage { message AdminRoomMessage {
string message = 1; string message = 1;
string roomId = 2; string roomId = 2;
string type = 3;
} }
// A message sent by an administrator to absolutely everybody // A message sent by an administrator to absolutely everybody

View File

@ -49,6 +49,7 @@
"grpc": "^1.24.4", "grpc": "^1.24.4",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"openid-client": "^4.7.4",
"prom-client": "^12.0.0", "prom-client": "^12.0.0",
"query-string": "^6.13.3", "query-string": "^6.13.3",
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0", "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",

View File

@ -4,6 +4,7 @@ import { BaseController } from "./BaseController";
import { adminApi } from "../Services/AdminApi"; import { adminApi } from "../Services/AdminApi";
import { jwtTokenManager } from "../Services/JWTTokenManager"; import { jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string"; import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient";
export interface TokenInterface { export interface TokenInterface {
userUuid: string; userUuid: string;
@ -12,11 +13,58 @@ export interface TokenInterface {
export class AuthenticateController extends BaseController { export class AuthenticateController extends BaseController {
constructor(private App: TemplatedApp) { constructor(private App: TemplatedApp) {
super(); super();
this.openIDLogin();
this.openIDCallback();
this.register(); this.register();
this.verify();
this.anonymLogin(); this.anonymLogin();
} }
openIDLogin() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/login-screen", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { nonce, state } = parse(req.getQuery());
if (!state || !nonce) {
res.writeStatus("400 Unauthorized").end("missing state and nonce URL parameters");
return;
}
try {
const loginUri = await openIDClient.authorizationUrl(state as string, nonce as string);
res.writeStatus("302");
res.writeHeader("Location", loginUri);
return res.end();
} catch (e) {
return this.errorToResponse(e, res);
}
});
}
openIDCallback() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/login-callback", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { code, nonce } = parse(req.getQuery());
try {
const userInfo = await openIDClient.getUserInfo(code as string, nonce as string);
const email = userInfo.email || userInfo.sub;
if (!email) {
throw new Error("No email in the response");
}
const authToken = jwtTokenManager.createAuthToken(email);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken }));
} catch (e) {
return this.errorToResponse(e, res);
}
});
}
//Try to login with an admin token //Try to login with an admin token
private register() { private register() {
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
@ -39,11 +87,12 @@ export class AuthenticateController extends BaseController {
if (typeof organizationMemberToken != "string") throw new Error("No organization token"); if (typeof organizationMemberToken != "string") throw new Error("No organization token");
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
const userUuid = data.userUuid; const userUuid = data.userUuid;
const email = data.email;
const roomUrl = data.roomUrl; const roomUrl = data.roomUrl;
const mapUrlStart = data.mapUrlStart; const mapUrlStart = data.mapUrlStart;
const textures = data.textures; const textures = data.textures;
const authToken = jwtTokenManager.createJWTToken(userUuid); const authToken = jwtTokenManager.createAuthToken(email || userUuid);
res.writeStatus("200 OK"); res.writeStatus("200 OK");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end( res.end(
@ -63,45 +112,6 @@ export class AuthenticateController extends BaseController {
}); });
} }
private verify() {
this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
res.end();
});
this.App.get("/verify", (res: HttpResponse, req: HttpRequest) => {
(async () => {
const query = parse(req.getQuery());
res.onAborted(() => {
console.warn("verify request was aborted");
});
try {
await jwtTokenManager.getUserUuidFromToken(query.token as string);
} catch (e) {
res.writeStatus("400 Bad Request");
this.addCorsHeaders(res);
res.end(
JSON.stringify({
success: false,
message: "Invalid JWT token",
})
);
return;
}
res.writeStatus("200 OK");
this.addCorsHeaders(res);
res.end(
JSON.stringify({
success: true,
})
);
})();
});
}
//permit to login on application. Return token to connect on Websocket IO. //permit to login on application. Return token to connect on Websocket IO.
private anonymLogin() { private anonymLogin() {
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
@ -115,7 +125,7 @@ export class AuthenticateController extends BaseController {
}); });
const userUuid = v4(); const userUuid = v4();
const authToken = jwtTokenManager.createJWTToken(userUuid); const authToken = jwtTokenManager.createAuthToken(userUuid);
res.writeStatus("200 OK"); res.writeStatus("200 OK");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end( res.end(

View File

@ -22,7 +22,7 @@ import {
import { UserMovesMessage } from "../Messages/generated/messages_pb"; import { UserMovesMessage } from "../Messages/generated/messages_pb";
import { TemplatedApp } from "uWebSockets.js"; import { TemplatedApp } from "uWebSockets.js";
import { parse } from "query-string"; import { parse } from "query-string";
import { jwtTokenManager } from "../Services/JWTTokenManager"; import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager";
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi"; import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { SocketManager, socketManager } from "../Services/SocketManager"; import { SocketManager, socketManager } from "../Services/SocketManager";
import { emitInBatch } from "../Services/IoSocketHelpers"; import { emitInBatch } from "../Services/IoSocketHelpers";
@ -173,31 +173,34 @@ export class IoSocketController {
characterLayers = [characterLayers]; characterLayers = [characterLayers];
} }
const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId); const tokenData =
token && typeof token === "string" ? jwtTokenManager.decodeJWTToken(token) : null;
const userIdentifier = tokenData ? tokenData.identifier : "";
let memberTags: string[] = []; let memberTags: string[] = [];
let memberVisitCardUrl: string | null = null; let memberVisitCardUrl: string | null = null;
let memberMessages: unknown; let memberMessages: unknown;
let memberTextures: CharacterTexture[] = []; let memberTextures: CharacterTexture[] = [];
const room = await socketManager.getOrCreateRoom(roomId); const room = await socketManager.getOrCreateRoom(roomId);
let userData: FetchMemberDataByUuidResponse = {
userUuid: userIdentifier,
tags: [],
visitCardUrl: null,
textures: [],
messages: [],
anonymous: true,
};
if (ADMIN_API_URL) { if (ADMIN_API_URL) {
try { try {
let userData: FetchMemberDataByUuidResponse = {
uuid: v4(),
tags: [],
visitCardUrl: null,
textures: [],
messages: [],
anonymous: true,
};
try { try {
userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId); userData = await adminApi.fetchMemberDataByUuid(userIdentifier, roomId, IPAddress);
} catch (err) { } catch (err) {
if (err?.response?.status == 404) { if (err?.response?.status == 404) {
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login! // If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
console.warn( console.warn(
'Cannot find user with uuid "' + 'Cannot find user with email "' +
userUuid + (userIdentifier || "anonymous") +
'". Performing an anonymous login instead.' '". Performing an anonymous login instead.'
); );
} else if (err?.response?.status == 403) { } else if (err?.response?.status == 403) {
@ -235,7 +238,12 @@ export class IoSocketController {
throw new Error("Use the login URL to connect"); throw new Error("Use the login URL to connect");
} }
} catch (e) { } catch (e) {
console.log("access not granted for user " + userUuid + " and room " + roomId); console.log(
"access not granted for user " +
(userIdentifier || "anonymous") +
" and room " +
roomId
);
console.error(e); console.error(e);
throw new Error("User cannot access this world"); throw new Error("User cannot access this world");
} }
@ -257,7 +265,7 @@ export class IoSocketController {
// Data passed here is accessible on the "websocket" socket object. // Data passed here is accessible on the "websocket" socket object.
url, url,
token, token,
userUuid, userUuid: userData.userUuid,
IPAddress, IPAddress,
roomId, roomId,
name, name,
@ -287,15 +295,10 @@ export class IoSocketController {
context context
); );
} catch (e) { } catch (e) {
/*if (e instanceof Error) { res.upgrade(
console.log(e.message);
res.writeStatus("401 Unauthorized").end(e.message);
} else {
res.writeStatus("500 Internal Server Error").end('An error occurred');
}*/
return res.upgrade(
{ {
rejected: true, rejected: true,
reason: e.reason || null,
message: e.message ? e.message : "500 Internal Server Error", message: e.message ? e.message : "500 Internal Server Error",
}, },
websocketKey, websocketKey,
@ -310,12 +313,14 @@ export class IoSocketController {
open: (ws) => { open: (ws) => {
if (ws.rejected === true) { if (ws.rejected === true) {
//FIX ME to use status code //FIX ME to use status code
if (ws.message === "World is full") { if (ws.reason === tokenInvalidException) {
socketManager.emitTokenExpiredMessage(ws);
} else if (ws.message === "World is full") {
socketManager.emitWorldFullMessage(ws); socketManager.emitWorldFullMessage(ws);
} else { } else {
socketManager.emitConnexionErrorMessage(ws, ws.message as string); socketManager.emitConnexionErrorMessage(ws, ws.message as string);
} }
ws.close(); setTimeout(() => ws.close(), 0);
return; return;
} }

View File

@ -1,11 +1,8 @@
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY"; const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
const API_URL = process.env.API_URL || ""; const API_URL = process.env.API_URL || "";
const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || "") || 600;
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
const JITSI_ISS = process.env.JITSI_ISS || ""; const JITSI_ISS = process.env.JITSI_ISS || "";
@ -13,14 +10,16 @@ const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080; const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080;
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
export const FRONT_URL = process.env.FRONT_URL || "http://localhost";
export const OPID_CLIENT_ID = process.env.OPID_CLIENT_ID || "";
export const OPID_CLIENT_SECRET = process.env.OPID_CLIENT_SECRET || "";
export const OPID_CLIENT_ISSUER = process.env.OPID_CLIENT_ISSUER || "";
export { export {
SECRET_KEY, SECRET_KEY,
MINIMUM_DISTANCE,
API_URL, API_URL,
ADMIN_API_URL, ADMIN_API_URL,
ADMIN_API_TOKEN, ADMIN_API_TOKEN,
MAX_USERS_PER_ROOM,
GROUP_RADIUS,
ALLOW_ARTILLERY, ALLOW_ARTILLERY,
CPU_OVERHEAT_THRESHOLD, CPU_OVERHEAT_THRESHOLD,
JITSI_URL, JITSI_URL,

View File

@ -21,7 +21,7 @@ interface ZoneDescriptor {
} }
export class PositionDispatcher { export class PositionDispatcher {
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) // TODO: we need a way to clean the zones if no one is in the zone and no one listening (to free memory!)
private zones: Zone[][] = []; private zones: Zone[][] = [];

View File

@ -7,6 +7,7 @@ import { RoomRedirect } from "./AdminApi/RoomRedirect";
export interface AdminApiData { export interface AdminApiData {
roomUrl: string; roomUrl: string;
email: string | null;
mapUrlStart: string; mapUrlStart: string;
tags: string[]; tags: string[];
policy_type: number; policy_type: number;
@ -21,7 +22,7 @@ export interface AdminBannedData {
} }
export interface FetchMemberDataByUuidResponse { export interface FetchMemberDataByUuidResponse {
uuid: string; userUuid: string;
tags: string[]; tags: string[];
visitCardUrl: string | null; visitCardUrl: string | null;
textures: CharacterTexture[]; textures: CharacterTexture[];
@ -46,12 +47,16 @@ class AdminApi {
return res.data; return res.data;
} }
async fetchMemberDataByUuid(uuid: string, roomId: string): Promise<FetchMemberDataByUuidResponse> { async fetchMemberDataByUuid(
userIdentifier: string | null,
roomId: string,
ipAddress: string
): Promise<FetchMemberDataByUuidResponse> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!")); return Promise.reject(new Error("No admin backoffice set!"));
} }
const res = await Axios.get(ADMIN_API_URL + "/api/room/access", { const res = await Axios.get(ADMIN_API_URL + "/api/room/access", {
params: { uuid, roomId }, params: { userIdentifier, roomId, ipAddress },
headers: { Authorization: `${ADMIN_API_TOKEN}` }, headers: { Authorization: `${ADMIN_API_TOKEN}` },
}); });
return res.data; return res.data;
@ -118,6 +123,18 @@ class AdminApi {
return data.data; return data.data;
}); });
} }
async getUrlRoomsFromSameWorld(roomUrl: string): Promise<string[]> {
if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!"));
}
return Axios.get(ADMIN_API_URL + "/api/room/sameWorld" + "?roomUrl=" + encodeURIComponent(roomUrl), {
headers: { Authorization: `${ADMIN_API_TOKEN}` },
}).then((data) => {
return data.data;
});
}
} }
export const adminApi = new AdminApi(); export const adminApi = new AdminApi();

View File

@ -1,100 +1,25 @@
import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable";
import { uuid } from "uuidv4"; import { uuid } from "uuidv4";
import Jwt from "jsonwebtoken"; import Jwt, { verify } from "jsonwebtoken";
import { TokenInterface } from "../Controller/AuthenticateController"; import { TokenInterface } from "../Controller/AuthenticateController";
import { adminApi, AdminBannedData } from "../Services/AdminApi"; import { adminApi, AdminBannedData } from "../Services/AdminApi";
export interface AuthTokenData {
identifier: string; //will be a email if logged in or an uuid if anonymous
}
export const tokenInvalidException = "tokenInvalid";
class JWTTokenManager { class JWTTokenManager {
public createJWTToken(userUuid: string) { public createAuthToken(identifier: string) {
return Jwt.sign({ userUuid: userUuid }, SECRET_KEY, { expiresIn: "200d" }); //todo: add a mechanic to refresh or recreate token return Jwt.sign({ identifier }, SECRET_KEY, { expiresIn: "3d" });
} }
public async getUserUuidFromToken(token: unknown, ipAddress?: string, roomUrl?: string): Promise<string> { public decodeJWTToken(token: string): AuthTokenData {
if (!token) { try {
throw new Error("An authentication error happened, a user tried to connect without a token."); return Jwt.verify(token, SECRET_KEY, { ignoreExpiration: false }) as AuthTokenData;
} catch (e) {
throw { reason: tokenInvalidException, message: e.message };
} }
if (typeof token !== "string") {
throw new Error("Token is expected to be a string");
}
if (token === "test") {
if (ALLOW_ARTILLERY) {
return uuid();
} else {
throw new Error(
"In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"
);
}
}
return new Promise<string>((resolve, reject) => {
Jwt.verify(token, SECRET_KEY, {}, (err, tokenDecoded) => {
const tokenInterface = tokenDecoded as TokenInterface;
if (err) {
console.error("An authentication error happened, invalid JsonWebToken.", err);
reject(new Error("An authentication error happened, invalid JsonWebToken. " + err.message));
return;
}
if (tokenDecoded === undefined) {
console.error("Empty token found.");
reject(new Error("Empty token found."));
return;
}
//verify token
if (!this.isValidToken(tokenInterface)) {
reject(new Error("Authentication error, invalid token structure."));
return;
}
if (ADMIN_API_URL) {
//verify user in admin
let promise = new Promise((resolve) => resolve());
if (ipAddress && roomUrl) {
promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, roomUrl);
}
promise
.then(() => {
adminApi
.fetchCheckUserByToken(tokenInterface.userUuid)
.then(() => {
resolve(tokenInterface.userUuid);
})
.catch((err) => {
//anonymous user
if (err.response && err.response.status && err.response.status === 404) {
resolve(tokenInterface.userUuid);
return;
}
reject(err);
});
})
.catch((err) => {
reject(err);
});
} else {
resolve(tokenInterface.userUuid);
}
});
});
}
private verifyBanUser(userUuid: string, ipAddress: string, roomUrl: string): Promise<AdminBannedData> {
return adminApi
.verifyBanUser(userUuid, ipAddress, roomUrl)
.then((data: AdminBannedData) => {
if (data && data.is_banned) {
throw new Error("User was banned");
}
return data;
})
.catch((err) => {
throw err;
});
}
private isValidToken(token: object): token is TokenInterface {
return !(typeof (token as TokenInterface).userUuid !== "string");
} }
} }

View File

@ -0,0 +1,43 @@
import { Issuer, Client } from "openid-client";
import { OPID_CLIENT_ID, OPID_CLIENT_SECRET, OPID_CLIENT_ISSUER, FRONT_URL } from "../Enum/EnvironmentVariable";
const opidRedirectUri = FRONT_URL + "/jwt";
class OpenIDClient {
private issuerPromise: Promise<Client> | null = null;
private initClient(): Promise<Client> {
if (!this.issuerPromise) {
this.issuerPromise = Issuer.discover(OPID_CLIENT_ISSUER).then((issuer) => {
return new issuer.Client({
client_id: OPID_CLIENT_ID,
client_secret: OPID_CLIENT_SECRET,
redirect_uris: [opidRedirectUri],
response_types: ["code"],
});
});
}
return this.issuerPromise;
}
public authorizationUrl(state: string, nonce: string) {
return this.initClient().then((client) => {
return client.authorizationUrl({
scope: "openid email",
prompt: "login",
state: state,
nonce: nonce,
});
});
}
public getUserInfo(code: string, nonce: string): Promise<{ email: string; sub: string }> {
return this.initClient().then((client) => {
return client.callback(opidRedirectUri, { code }, { nonce }).then((tokenSet) => {
return client.userinfo(tokenSet);
});
});
}
}
export const openIDClient = new OpenIDClient();

View File

@ -1,44 +1,45 @@
import { PusherRoom } from "../Model/PusherRoom"; import { PusherRoom } from "../Model/PusherRoom";
import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface";
import { import {
AdminMessage,
AdminPusherToBackMessage,
AdminRoomMessage,
BanMessage,
CharacterLayerMessage,
EmoteEventMessage,
EmotePromptMessage,
GroupDeleteMessage, GroupDeleteMessage,
ItemEventMessage, ItemEventMessage,
JoinRoomMessage,
PlayGlobalMessage, PlayGlobalMessage,
PusherToBackMessage,
QueryJitsiJwtMessage,
RefreshRoomMessage,
ReportPlayerMessage,
RoomJoinedMessage, RoomJoinedMessage,
SendJitsiJwtMessage,
ServerToAdminClientMessage,
ServerToClientMessage, ServerToClientMessage,
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
SilentMessage, SilentMessage,
SubMessage, SubMessage,
ReportPlayerMessage, UserJoinedRoomMessage,
UserLeftMessage, UserLeftMessage,
UserLeftRoomMessage,
UserMovesMessage, UserMovesMessage,
ViewportMessage, ViewportMessage,
WebRtcSignalToServerMessage, WebRtcSignalToServerMessage,
QueryJitsiJwtMessage,
SendJitsiJwtMessage,
JoinRoomMessage,
CharacterLayerMessage,
PusherToBackMessage,
WorldFullMessage,
WorldConnexionMessage, WorldConnexionMessage,
AdminPusherToBackMessage, TokenExpiredMessage,
ServerToAdminClientMessage,
EmoteEventMessage,
UserJoinedRoomMessage,
UserLeftRoomMessage,
AdminMessage,
BanMessage,
RefreshRoomMessage,
EmotePromptMessage,
VariableMessage, VariableMessage,
ErrorMessage, ErrorMessage,
WorldFullMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { ADMIN_API_URL, JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
import { adminApi } from "./AdminApi"; import { adminApi } from "./AdminApi";
import { emitInBatch } from "./IoSocketHelpers"; import { emitInBatch } from "./IoSocketHelpers";
import Jwt from "jsonwebtoken"; import Jwt from "jsonwebtoken";
import { JITSI_URL } from "../Enum/EnvironmentVariable";
import { clientEventsEmitter } from "./ClientEventsEmitter"; import { clientEventsEmitter } from "./ClientEventsEmitter";
import { gaugeManager } from "./GaugeManager"; import { gaugeManager } from "./GaugeManager";
import { apiClientRepository } from "./ApiClientRepository"; import { apiClientRepository } from "./ApiClientRepository";
@ -117,7 +118,7 @@ export class SocketManager implements ZoneEventListener {
console.warn("Admin connection lost to back server"); console.warn("Admin connection lost to back server");
// Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start.
if (!client.disconnecting) { if (!client.disconnecting) {
this.closeWebsocketConnection(client, 1011, "Connection lost to back server"); this.closeWebsocketConnection(client, 1011, "Admin Connection lost to back server");
} }
console.log("A user left"); console.log("A user left");
}) })
@ -140,24 +141,6 @@ export class SocketManager implements ZoneEventListener {
} }
} }
getAdminSocketDataFor(roomId: string): AdminSocketData {
throw new Error("Not reimplemented yet");
/*const data:AdminSocketData = {
rooms: {},
users: {},
}
const room = this.Worlds.get(roomId);
if (room === undefined) {
return data;
}
const users = room.getUsers();
data.rooms[roomId] = users.size;
users.forEach(user => {
data.users[user.uuid] = true
})
return data;*/
}
async handleJoinRoom(client: ExSocketInterface): Promise<void> { async handleJoinRoom(client: ExSocketInterface): Promise<void> {
const viewport = client.viewport; const viewport = client.viewport;
try { try {
@ -406,17 +389,6 @@ export class SocketManager implements ZoneEventListener {
} }
} }
emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) {
if (!client.tags.includes("admin")) {
//In case of xss injection, we just kill the connection.
throw "Client is not an admin!";
}
const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setPlayglobalmessage(playglobalmessage);
client.backConnection.write(pusherToBackMessage);
}
public getWorlds(): Map<string, PusherRoom> { public getWorlds(): Map<string, PusherRoom> {
return this.rooms; return this.rooms;
} }
@ -463,7 +435,7 @@ export class SocketManager implements ZoneEventListener {
client.send(serverToClientMessage.serializeBinary().buffer, true); client.send(serverToClientMessage.serializeBinary().buffer, true);
} catch (e) { } catch (e) {
console.error("An error occured while generating the Jitsi JWT token: ", e); console.error("An error occurred while generating the Jitsi JWT token: ", e);
} }
} }
@ -598,7 +570,20 @@ export class SocketManager implements ZoneEventListener {
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setWorldfullmessage(errorMessage); serverToClientMessage.setWorldfullmessage(errorMessage);
client.send(serverToClientMessage.serializeBinary().buffer, true); if (!client.disconnecting) {
client.send(serverToClientMessage.serializeBinary().buffer, true);
}
}
public emitTokenExpiredMessage(client: WebSocket) {
const errorMessage = new TokenExpiredMessage();
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setTokenexpiredmessage(errorMessage);
if (!client.disconnecting) {
client.send(serverToClientMessage.serializeBinary().buffer, true);
}
} }
public emitConnexionErrorMessage(client: WebSocket, message: string) { public emitConnexionErrorMessage(client: WebSocket, message: string) {
@ -625,6 +610,36 @@ export class SocketManager implements ZoneEventListener {
client.backConnection.write(pusherToBackMessage); client.backConnection.write(pusherToBackMessage);
} }
public async emitPlayGlobalMessage(
client: ExSocketInterface,
playGlobalMessageEvent: PlayGlobalMessage
): Promise<void> {
if (!client.tags.includes("admin")) {
throw "Client is not an admin!";
}
const clientRoomUrl = client.roomId;
let tabUrlRooms: string[];
if (playGlobalMessageEvent.getBroadcasttoworld()) {
tabUrlRooms = await adminApi.getUrlRoomsFromSameWorld(clientRoomUrl);
} else {
tabUrlRooms = [clientRoomUrl];
}
const roomMessage = new AdminRoomMessage();
roomMessage.setMessage(playGlobalMessageEvent.getContent());
roomMessage.setType(playGlobalMessageEvent.getType());
for (const roomUrl of tabUrlRooms) {
const apiRoom = await apiClientRepository.getClient(roomUrl);
roomMessage.setRoomid(roomUrl);
apiRoom.sendAdminMessageToRoom(roomMessage, (response) => {
return;
});
}
}
} }
export const socketManager = new SocketManager(); export const socketManager = new SocketManager();

File diff suppressed because it is too large Load Diff