merge develop

This commit is contained in:
_Bastler 2021-04-06 10:24:46 +02:00
commit 052da2281d
90 changed files with 3634 additions and 294 deletions

View File

@ -1,9 +1,3 @@
import { Group } from "./Group";
import { PointInterface } from "./Websocket/PointInterface";
import {Zone} from "_Model/Zone";
import {Movable} from "_Model/Movable";
import {PositionNotifier} from "_Model/PositionNotifier";
import {ServerDuplexStream} from "grpc";
import { import {
BatchMessage, BatchMessage,
PusherToBackMessage, PusherToBackMessage,
@ -11,7 +5,6 @@ import {
ServerToClientMessage, ServerToClientMessage,
SubMessage, UserJoinedRoomMessage, UserLeftRoomMessage SubMessage, UserJoinedRoomMessage, UserLeftRoomMessage
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
import {AdminSocket} from "../RoomManager"; import {AdminSocket} from "../RoomManager";

View File

@ -38,12 +38,10 @@ export class GameRoom {
private readonly positionNotifier: PositionNotifier; private readonly positionNotifier: PositionNotifier;
public readonly roomId: string; public readonly roomId: string;
public readonly anonymous: boolean;
public tags: string[];
public policyType: GameRoomPolicyTypes;
public readonly roomSlug: string; public readonly roomSlug: string;
public readonly worldSlug: string = ''; public readonly worldSlug: string = '';
public readonly organizationSlug: string = ''; public readonly organizationSlug: string = '';
private versionNumber:number = 1;
private nextUserId: number = 1; private nextUserId: number = 1;
constructor(roomId: string, constructor(roomId: string,
@ -56,11 +54,8 @@ export class GameRoom {
onLeaves: LeavesCallback) onLeaves: LeavesCallback)
{ {
this.roomId = roomId; this.roomId = roomId;
this.anonymous = isRoomAnonymous(roomId);
this.tags = [];
this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY;
if (this.anonymous) { if (isRoomAnonymous(roomId)) {
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
} else { } else {
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId);
@ -304,10 +299,6 @@ export class GameRoom {
return this.itemsState; return this.itemsState;
} }
public canAccess(userTags: string[]): boolean {
return arrayIntersect(userTags, this.tags);
}
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> { public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
return this.positionNotifier.addZoneListener(call, x, y); return this.positionNotifier.addZoneListener(call, x, y);
} }
@ -328,4 +319,9 @@ export class GameRoom {
public adminLeave(admin: Admin): void { public adminLeave(admin: Admin): void {
this.admins.delete(admin); this.admins.delete(admin);
} }
public incrementVersion(): number {
this.versionNumber++
return this.versionNumber;
}
} }

View File

@ -10,7 +10,7 @@ import {
JoinRoomMessage, JoinRoomMessage,
PlayGlobalMessage, PlayGlobalMessage,
PusherToBackMessage, PusherToBackMessage,
QueryJitsiJwtMessage, QueryJitsiJwtMessage, RefreshRoomPromptMessage,
ServerToAdminClientMessage, ServerToAdminClientMessage,
ServerToClientMessage, ServerToClientMessage,
SilentMessage, SilentMessage,
@ -193,6 +193,10 @@ const roomManager: IRoomManagerServer = {
socketManager.dispatchWorlFullWarning(call.request.getRoomid()); socketManager.dispatchWorlFullWarning(call.request.getRoomid());
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
sendRefreshRoomPrompt(call: ServerUnaryCall<RefreshRoomPromptMessage>, callback: sendUnaryData<EmptyMessage>): void {
socketManager.dispatchRoomRefresh(call.request.getRoomid());
callback(null, new EmptyMessage());
},
}; };
export {roomManager}; export {roomManager};

View File

@ -1,49 +0,0 @@
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
import Axios from "axios";
export interface AdminApiData {
organizationSlug: string
worldSlug: string
roomSlug: string
mapUrlStart: string
tags: string[]
policy_type: number
userUuid: string
messages?: unknown[],
textures: CharacterTexture[]
}
export interface CharacterTexture {
id: number,
level: number,
url: string,
rights: string
}
class AdminApi {
async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise<AdminApiData> {
if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!');
}
const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = {
organizationSlug,
worldSlug
};
if (roomSlug) {
params.roomSlug = roomSlug;
}
const res = await Axios.get(ADMIN_API_URL + '/api/map',
{
headers: {"Authorization": `${ADMIN_API_TOKEN}`},
params
}
)
return res.data;
}
}
export const adminApi = new AdminApi();

View File

@ -26,7 +26,7 @@ import {
GroupLeftZoneMessage, GroupLeftZoneMessage,
WorldFullWarningMessage, WorldFullWarningMessage,
UserLeftZoneMessage, UserLeftZoneMessage,
BanUserMessage, BanUserMessage, RefreshRoomMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {User, UserSocket} from "../Model/User"; import {User, UserSocket} from "../Model/User";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
@ -41,7 +41,6 @@ import {
} from "../Enum/EnvironmentVariable"; } from "../Enum/EnvironmentVariable";
import {Movable} from "../Model/Movable"; import {Movable} from "../Model/Movable";
import {PositionInterface} from "../Model/PositionInterface"; import {PositionInterface} from "../Model/PositionInterface";
import {adminApi, CharacterTexture} from "./AdminApi";
import Jwt from "jsonwebtoken"; import Jwt from "jsonwebtoken";
import {JITSI_URL} from "../Enum/EnvironmentVariable"; import {JITSI_URL} from "../Enum/EnvironmentVariable";
import {clientEventsEmitter} from "./ClientEventsEmitter"; import {clientEventsEmitter} from "./ClientEventsEmitter";
@ -130,14 +129,6 @@ export class SocketManager {
throw new Error('Viewport not found in message'); throw new Error('Viewport not found in message');
} }
// sending to all clients in room except sender
/*client.position = {
x: position.x,
y: position.y,
direction,
moving: position.moving,
};
client.viewport = viewport;*/
// update position in the world // update position in the world
room.updatePosition(user, ProtobufUtils.toPointInterface(position)); room.updatePosition(user, ProtobufUtils.toPointInterface(position));
@ -192,21 +183,6 @@ export class SocketManager {
} }
} }
// TODO: handle this message in pusher
/*async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) {
try {
const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid());
if (!reportedSocket) {
throw 'reported socket user not found';
}
//TODO report user on admin application
await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid)
} catch (e) {
console.error('An error occurred on "handleReportMessage"');
console.error(e);
}
}*/
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void { emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
//send only at user //send only at user
const remoteUser = room.getUsers().get(data.getReceiverid()); const remoteUser = room.getUsers().get(data.getReceiverid());
@ -289,11 +265,6 @@ export class SocketManager {
(thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener), (thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener),
(thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener) (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener)
); );
if (!world.anonymous) {
const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug)
world.tags = data.tags
world.policyType = Number(data.policy_type)
}
gaugeManager.incNbRoomGauge(); gaugeManager.incNbRoomGauge();
this.rooms.set(roomId, world); this.rooms.set(roomId, world);
} }
@ -772,6 +743,25 @@ export class SocketManager {
recipient.socket.write(clientMessage); recipient.socket.write(clientMessage);
}); });
} }
dispatchRoomRefresh(roomId: string,): void {
const room = this.rooms.get(roomId);
if (!room) {
return;
}
const versionNumber = room.incrementVersion();
room.getUsers().forEach((recipient) => {
const worldFullMessage = new RefreshRoomMessage();
worldFullMessage.setRoomid(roomId)
worldFullMessage.setVersionnumber(versionNumber)
const clientMessage = new ServerToClientMessage();
clientMessage.setRefreshroommessage(worldFullMessage);
recipient.socket.write(clientMessage);
});
}
} }
export const socketManager = new SocketManager(); export const socketManager = new SocketManager();

View File

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

View File

@ -37,7 +37,7 @@ services:
DEBUG_MODE: "$DEBUG_MODE" DEBUG_MODE: "$DEBUG_MODE"
JITSI_URL: $JITSI_URL JITSI_URL: $JITSI_URL
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
API_URL: pusher.${DOMAIN} PUSHER_URL: //pusher.${DOMAIN}
TURN_SERVER: "${TURN_SERVER}" TURN_SERVER: "${TURN_SERVER}"
TURN_USER: "${TURN_USER}" TURN_USER: "${TURN_USER}"
TURN_PASSWORD: "${TURN_PASSWORD}" TURN_PASSWORD: "${TURN_PASSWORD}"

View File

@ -82,14 +82,12 @@
}, },
"ports": [80], "ports": [80],
"env": { "env": {
"API_URL": "pusher."+url, "PUSHER_URL": "//pusher."+url,
"UPLOADER_URL": "uploader."+url, "UPLOADER_URL": "//uploader."+url,
"ADMIN_URL": url, "ADMIN_URL": "//"+url,
"JITSI_URL": env.JITSI_URL, "JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
"TURN_USER": "workadventure",
"TURN_PASSWORD": "WorkAdventure123",
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false", "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
"START_ROOM_URL": "/_/global/maps."+url+"/Floor0/floor0.json" "START_ROOM_URL": "/_/global/maps."+url+"/Floor0/floor0.json"
//"GA_TRACKING_ID": "UA-10196481-11" //"GA_TRACKING_ID": "UA-10196481-11"

View File

@ -0,0 +1,207 @@
version: "3"
services:
reverse-proxy:
image: traefik:v2.0
command:
- --api.insecure=true
- --providers.docker
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
ports:
- "80:80"
- "443:443"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
depends_on:
- back
- front
volumes:
- /var/run/docker.sock:/var/run/docker.sock
front:
image: thecodingmachine/nodejs:14
environment:
DEBUG_MODE: "$DEBUG_MODE"
JITSI_URL: $JITSI_URL
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
HOST: "0.0.0.0"
NODE_ENV: development
PUSHER_URL: /pusher
UPLOADER_URL: /uploader
ADMIN_URL: /admin
MAPS_URL: /maps
STARTUP_COMMAND_1: yarn install
TURN_SERVER: "turn:localhost:3478,turns:localhost:5349"
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
TURN_USER: ""
TURN_PASSWORD: ""
START_ROOM_URL: "$START_ROOM_URL"
command: yarn run start
volumes:
- ./front:/usr/src/app
labels:
- "traefik.http.routers.front.rule=PathPrefix(`/`)"
- "traefik.http.routers.front.entryPoints=web,traefik"
- "traefik.http.services.front.loadbalancer.server.port=8080"
- "traefik.http.routers.front-ssl.rule=PathPrefix(`/`)"
- "traefik.http.routers.front-ssl.entryPoints=websecure"
- "traefik.http.routers.front-ssl.tls=true"
- "traefik.http.routers.front-ssl.service=front"
pusher:
image: thecodingmachine/nodejs:12
command: yarn dev
#command: yarn run prod
#command: yarn run profile
environment:
DEBUG: "*"
STARTUP_COMMAND_1: yarn install
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
API_URL: back:50051
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
volumes:
- ./pusher:/usr/src/app
labels:
- "traefik.http.middlewares.strip-pusher-prefix.stripprefix.prefixes=/pusher"
- "traefik.http.routers.pusher.rule=PathPrefix(`/pusher`)"
- "traefik.http.routers.pusher.middlewares=strip-pusher-prefix@docker"
- "traefik.http.routers.pusher.entryPoints=web"
- "traefik.http.services.pusher.loadbalancer.server.port=8080"
- "traefik.http.routers.pusher-ssl.rule=PathPrefix(`/pusher`)"
- "traefik.http.routers.pusher-ssl.middlewares=strip-pusher-prefix@docker"
- "traefik.http.routers.pusher-ssl.entryPoints=websecure"
- "traefik.http.routers.pusher-ssl.tls=true"
- "traefik.http.routers.pusher-ssl.service=pusher"
maps:
image: thecodingmachine/nodejs:12-apache
environment:
DEBUG_MODE: "$DEBUG_MODE"
HOST: "0.0.0.0"
NODE_ENV: development
#APACHE_DOCUMENT_ROOT: dist/
#APACHE_EXTENSIONS: headers
#APACHE_EXTENSION_HEADERS: 1
STARTUP_COMMAND_0: sudo a2enmod headers
STARTUP_COMMAND_1: yarn install
STARTUP_COMMAND_2: yarn run dev &
volumes:
- ./maps:/var/www/html
labels:
- "traefik.http.middlewares.strip-maps-prefix.stripprefix.prefixes=/maps"
- "traefik.http.routers.maps.rule=PathPrefix(`/maps`)"
- "traefik.http.routers.maps.middlewares=strip-maps-prefix@docker"
- "traefik.http.routers.maps.entryPoints=web,traefik"
- "traefik.http.services.maps.loadbalancer.server.port=80"
- "traefik.http.routers.maps-ssl.rule=PathPrefix(`/maps`)"
- "traefik.http.routers.maps-ssl.middlewares=strip-maps-prefix@docker"
- "traefik.http.routers.maps-ssl.entryPoints=websecure"
- "traefik.http.routers.maps-ssl.tls=true"
- "traefik.http.routers.maps-ssl.service=maps"
back:
image: thecodingmachine/nodejs:12
command: yarn dev
#command: yarn run profile
environment:
DEBUG: "*"
STARTUP_COMMAND_1: yarn install
SECRET_KEY: yourSecretKey
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
ALLOW_ARTILLERY: "true"
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
volumes:
- ./back:/usr/src/app
labels:
- "traefik.http.middlewares.strip-api-prefix.stripprefix.prefixes=/api"
- "traefik.http.routers.back.rule=PathPrefix(`/api`)"
- "traefik.http.routers.back.middlewares=strip-api-prefix@docker"
- "traefik.http.routers.back.entryPoints=web"
- "traefik.http.services.back.loadbalancer.server.port=8080"
- "traefik.http.routers.back-ssl.rule=PathPrefix(`/api`)"
- "traefik.http.routers.back-ssl.middlewares=strip-api-prefix@docker"
- "traefik.http.routers.back-ssl.entryPoints=websecure"
- "traefik.http.routers.back-ssl.tls=true"
- "traefik.http.routers.back-ssl.service=back"
uploader:
image: thecodingmachine/nodejs:12
command: yarn dev
#command: yarn run profile
environment:
DEBUG: "*"
STARTUP_COMMAND_1: yarn install
volumes:
- ./uploader:/usr/src/app
labels:
- "traefik.http.middlewares.strip-uploader-prefix.stripprefix.prefixes=/uploader"
- "traefik.http.routers.uploader.rule=PathPrefix(`/uploader`)"
- "traefik.http.routers.uploader.middlewares=strip-uploader-prefix@docker"
- "traefik.http.routers.uploader.entryPoints=web"
- "traefik.http.services.uploader.loadbalancer.server.port=8080"
- "traefik.http.routers.uploader-ssl.rule=PathPrefix(`/uploader`)"
- "traefik.http.routers.uploader-ssl.middlewares=strip-uploader-prefix@docker"
- "traefik.http.routers.uploader-ssl.entryPoints=websecure"
- "traefik.http.routers.uploader-ssl.tls=true"
- "traefik.http.routers.uploader-ssl.service=uploader"
website:
image: thecodingmachine/nodejs:12-apache
environment:
STARTUP_COMMAND_1: npm install
STARTUP_COMMAND_2: npm run watch &
APACHE_DOCUMENT_ROOT: dist/
volumes:
- ./website:/var/www/html
labels:
- "traefik.http.routers.website.rule=Host(`workadventure.localhost`)"
- "traefik.http.routers.website.entryPoints=web"
- "traefik.http.services.website.loadbalancer.server.port=80"
- "traefik.http.routers.website-ssl.rule=Host(`workadventure.localhost`)"
- "traefik.http.routers.website-ssl.entryPoints=websecure"
- "traefik.http.routers.website-ssl.tls=true"
- "traefik.http.routers.website-ssl.service=website"
messages:
#image: thecodingmachine/nodejs:14
image: thecodingmachine/workadventure-back-base:latest
environment:
#STARTUP_COMMAND_0: sudo apt-get install -y inotify-tools
STARTUP_COMMAND_1: yarn install
STARTUP_COMMAND_2: yarn run proto:watch
volumes:
- ./messages:/usr/src/app
- ./back:/usr/src/back
- ./front:/usr/src/front
- ./pusher:/usr/src/pusher
# coturn:
# image: coturn/coturn:4.5.2
# command:
# - turnserver
# #- -c=/etc/coturn/turnserver.conf
# - --log-file=stdout
# - --external-ip=$$(detect-external-ip)
# - --listening-port=3478
# - --min-port=10000
# - --max-port=10010
# - --tls-listening-port=5349
# - --listening-ip=0.0.0.0
# - --realm=localhost
# - --server-name=localhost
# - --lt-cred-mech
# # Enable Coturn "REST API" to validate temporary passwords.
# #- --use-auth-secret
# #- --static-auth-secret=SomeStaticAuthSecret
# #- --userdb=/var/lib/turn/turndb
# - --user=workadventure:WorkAdventure123
# # use real-valid certificate/privatekey files
# #- --cert=/root/letsencrypt/fullchain.pem
# #- --pkey=/root/letsencrypt/privkey.pem
# network_mode: host

View File

@ -26,9 +26,9 @@ services:
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
HOST: "0.0.0.0" HOST: "0.0.0.0"
NODE_ENV: development NODE_ENV: development
API_URL: pusher.workadventure.localhost PUSHER_URL: //pusher.workadventure.localhost
UPLOADER_URL: uploader.workadventure.localhost UPLOADER_URL: //uploader.workadventure.localhost
ADMIN_URL: workadventure.localhost ADMIN_URL: //workadventure.localhost
STARTUP_COMMAND_1: ./templater.sh STARTUP_COMMAND_1: ./templater.sh
STARTUP_COMMAND_2: yarn install STARTUP_COMMAND_2: yarn install
STUN_SERVER: "stun:stun.l.google.com:19302" STUN_SERVER: "stun:stun.l.google.com:19302"
@ -43,7 +43,7 @@ services:
- ./front:/usr/src/app - ./front:/usr/src/app
labels: labels:
- "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)" - "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)"
- "traefik.http.routers.front.entryPoints=web,traefik" - "traefik.http.routers.front.entryPoints=web"
- "traefik.http.services.front.loadbalancer.server.port=8080" - "traefik.http.services.front.loadbalancer.server.port=8080"
- "traefik.http.routers.front-ssl.rule=Host(`play.workadventure.localhost`)" - "traefik.http.routers.front-ssl.rule=Host(`play.workadventure.localhost`)"
- "traefik.http.routers.front-ssl.entryPoints=websecure" - "traefik.http.routers.front-ssl.entryPoints=websecure"

View File

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

View File

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

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

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

View File

@ -76,11 +76,11 @@
</aside> </aside>
<main id="cowebsite-main"> <main id="cowebsite-main">
</main> </main>
<button class="top-right-btn" id="cowebsite-fullscreen"> <button class="top-right-btn" id="cowebsite-fullscreen" alt="fullscreen mode">
<img id="cowebsite-fullscreen-open" src="resources/logos/monitor.svg"/> <img id="cowebsite-fullscreen-open" src="resources/logos/fullscreen.svg"/>
<img id="cowebsite-fullscreen-close" style="display: none;" src="resources/logos/monitor-close.svg"/> <img id="cowebsite-fullscreen-close" style="display: none;" src="resources/logos/fullscreen-exit.svg"/>
</button> </button>
<button class="top-right-btn" id="cowebsite-close"> <button class="top-right-btn" id="cowebsite-close" alt="close the iframe">
<img src="resources/logos/close.svg"/> <img src="resources/logos/close.svg"/>
</button> </button>
</div> </div>

View File

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

After

Width:  |  Height:  |  Size: 329 B

View File

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

After

Width:  |  Height:  |  Size: 322 B

View File

@ -35,9 +35,8 @@
cursor: url('/resources/logos/cursor_pointer.png'), pointer; cursor: url('/resources/logos/cursor_pointer.png'), pointer;
img { img {
height: 20px; height: 25px;
background-color: rgba(0,0.0,0,0.3); padding: 4px;
padding: 5px;
border-radius: 3px; border-radius: 3px;
} }
@ -78,31 +77,13 @@
} }
.top-right-btn{ .top-right-btn{
top: 10px; left: -6px;
right: -100px; &#cowebsite-close {
animation: right .2s ease; top: 0px;
}
img { &#cowebsite-fullscreen {
right: 15px; top: 25px;
} }
}
#cowebsite-close {
right: -140px;
}
#cowebsite-fullscreen {
right: -100px;
}
}
#cowebsite:hover {
#cowebsite-close{
right: 10px;
}
#cowebsite-fullscreen{
right: 45px;
} }
} }
} }
@ -139,32 +120,13 @@
} }
} }
.top-right-btn{ .top-right-btn {
top: 10px; &#cowebsite-close {
right: -100px; right: 0px;
animation: right .2s ease; }
&#cowebsite-fullscreen {
img { right: 25px;
right: 15px;
} }
}
#cowebsite-close {
right: -140px;
}
#cowebsite-fullscreen {
right: -100px;
}
}
#cowebsite:hover {
#cowebsite-close{
right: 10px;
}
#cowebsite-fullscreen{
right: 45px;
} }
} }
} }

View File

@ -1123,6 +1123,31 @@ div.action p.action-body{
margin-left: calc(50% - 75px); margin-left: calc(50% - 75px);
border-radius: 15px; border-radius: 15px;
} }
.popUpElement{
font-family: 'Press Start 2P';
text-align: left;
color: white;
}
.popUpElement div {
font-family: 'Press Start 2P';
font-size: 10px;
background-color: #727678;
}
.popUpElement button {
position: relative;
font-size: 10px;
border-image-repeat: revert;
margin-right: 5px;
}
.popUpElement .buttonContainer {
float: right;
background-color: inherit;
}
@keyframes mymove { @keyframes mymove {
0% {bottom: 40px;} 0% {bottom: 40px;}
50% {bottom: 30px;} 50% {bottom: 30px;}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import Axios from "axios"; import Axios from "axios";
import {API_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable"; import {PUSHER_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "./RoomConnection"; import {RoomConnection} from "./RoomConnection";
import {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels"; import {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
import {GameConnexionTypes, urlManager} from "../Url/UrlManager"; import {GameConnexionTypes, urlManager} from "../Url/UrlManager";
@ -34,7 +34,7 @@ class ConnectionManager {
this.connexionType = connexionType; this.connexionType = connexionType;
if(connexionType === GameConnexionTypes.register) { if(connexionType === GameConnexionTypes.register) {
const organizationMemberToken = urlManager.getOrganizationToken(); const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${API_URL}/register`, {organizationMemberToken}).then(res => res.data); const data = await Axios.post(`${PUSHER_URL}/register`, {organizationMemberToken}).then(res => res.data);
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures); this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);
@ -42,7 +42,7 @@ class ConnectionManager {
const worldSlug = data.worldSlug; const worldSlug = data.worldSlug;
const roomSlug = data.roomSlug; const roomSlug = data.roomSlug;
const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.hash); const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.search + window.location.hash);
urlManager.pushRoomIdToUrl(room); urlManager.pushRoomIdToUrl(room);
return Promise.resolve(room); return Promise.resolve(room);
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) { } else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
@ -64,20 +64,20 @@ class ConnectionManager {
if (connexionType === GameConnexionTypes.empty) { if (connexionType === GameConnexionTypes.empty) {
roomId = START_ROOM_URL; roomId = START_ROOM_URL;
} else { } else {
roomId = window.location.pathname + window.location.hash; roomId = window.location.pathname + window.location.search + window.location.hash;
} }
return Promise.resolve(new Room(roomId)); return Promise.resolve(new Room(roomId));
} }
return Promise.reject('Invalid URL'); return Promise.reject(new Error('Invalid URL'));
} }
private async verifyToken(token: string): Promise<void> { private async verifyToken(token: string): Promise<void> {
await Axios.get(`${API_URL}/verify`, {params: {token}}); await Axios.get(`${PUSHER_URL}/verify`, {params: {token}});
} }
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> { public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${API_URL}/anonymLogin`).then(res => res.data); const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then(res => res.data);
this.localUser = new LocalUser(data.userUuid, data.authToken, []); this.localUser = new LocalUser(data.userUuid, data.authToken, []);
if (!isBenchmark) { // In benchmark, we don't have a local storage. if (!isBenchmark) { // In benchmark, we don't have a local storage.
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);

View File

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

View File

@ -1,4 +1,4 @@
import {API_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import Axios from "axios"; import Axios from "axios";
import { import {
BatchMessage, BatchMessage,
@ -67,8 +67,12 @@ export class RoomConnection implements RoomConnection {
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]" * @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]"
*/ */
public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) { public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) {
let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://'); let url = new URL(PUSHER_URL, window.location.toString()).toString();
url += '/room'; url = url.replace('http://', 'ws://').replace('https://', 'wss://');
if (!url.endsWith('/')) {
url += '/';
}
url += 'room';
url += '?roomId='+(roomId ?encodeURIComponent(roomId):''); url += '?roomId='+(roomId ?encodeURIComponent(roomId):'');
url += '&token='+(token ?encodeURIComponent(token):''); url += '&token='+(token ?encodeURIComponent(token):'');
url += '&name='+encodeURIComponent(name); url += '&name='+encodeURIComponent(name);
@ -183,6 +187,8 @@ export class RoomConnection implements RoomConnection {
adminMessagesService.onSendusermessage(message.getSendusermessage() as BanUserMessage); adminMessagesService.onSendusermessage(message.getSendusermessage() as BanUserMessage);
} else if (message.hasWorldfullwarningmessage()) { } else if (message.hasWorldfullwarningmessage()) {
worldFullWarningStream.onMessage(); worldFullWarningStream.onMessage();
} else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed.
} else { } else {
throw new Error('Unknown message received'); throw new Error('Unknown message received');
} }

View File

@ -1,8 +1,9 @@
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 = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json';
const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "pusher.workadventure.localhost"); // For compatibility reasons with older versions, API_URL is the old host name of PUSHER_URL
const UPLOADER_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.UPLOADER_URL || 'uploader.workadventure.localhost'); const PUSHER_URL = process.env.PUSHER_URL || (process.env.API_URL ? '//'+process.env.API_URL : "//pusher.workadventure.localhost");
const ADMIN_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.ADMIN_URL || "workadventure.localhost"); const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost';
const ADMIN_URL = process.env.ADMIN_URL || "//workadventure.localhost";
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302"; const 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 TURN_USER: string = process.env.TURN_USER || ''; const TURN_USER: string = process.env.TURN_USER || '';
@ -17,7 +18,7 @@ const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new
export { export {
DEBUG_MODE, DEBUG_MODE,
START_ROOM_URL, START_ROOM_URL,
API_URL, PUSHER_URL,
UPLOADER_URL, UPLOADER_URL,
ADMIN_URL, ADMIN_URL,
RESOLUTION, RESOLUTION,

View File

@ -59,11 +59,14 @@ import {TextureError} from "../../Exception/TextureError";
import {addLoader} from "../Components/Loader"; import {addLoader} from "../Components/Loader";
import {ErrorSceneName} from "../Reconnecting/ErrorScene"; import {ErrorSceneName} from "../Reconnecting/ErrorScene";
import {localUserStore} from "../../Connexion/LocalUserStore"; import {localUserStore} from "../../Connexion/LocalUserStore";
import {iframeListener} from "../../Api/IframeListener";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import Texture = Phaser.Textures.Texture; import Texture = Phaser.Textures.Texture;
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import CanvasTexture = Phaser.Textures.CanvasTexture; import CanvasTexture = Phaser.Textures.CanvasTexture;
import GameObject = Phaser.GameObjects.GameObject; import GameObject = Phaser.GameObjects.GameObject;
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import DOMElement = Phaser.GameObjects.DOMElement;
import {Subscription} from "rxjs"; import {Subscription} from "rxjs";
import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream";
@ -157,6 +160,7 @@ export class GameScene extends ResizableScene implements CenterListener {
private playerName!: string; private playerName!: string;
private characterLayers!: string[]; private characterLayers!: string[];
private messageSubscription: Subscription|null = null; private messageSubscription: Subscription|null = null;
private popUpElements : Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>();
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
super({ super({
@ -190,6 +194,15 @@ export class GameScene extends ResizableScene implements CenterListener {
this.load.image(openChatIconName, 'resources/objects/talk.png'); this.load.image(openChatIconName, 'resources/objects/talk.png');
this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => {
// If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments)
if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:')) {
this.MapUrlFile = this.MapUrlFile.replace('http://', 'https://');
this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile);
this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => {
this.onMapLoad(data);
});
return;
}
this.scene.start(ErrorSceneName, { this.scene.start(ErrorSceneName, {
title: 'Network error', title: 'Network error',
subTitle: 'An error occurred while loading resource:', subTitle: 'An error occurred while loading resource:',
@ -263,7 +276,8 @@ export class GameScene extends ResizableScene implements CenterListener {
break; break;
} }
default: default:
throw new Error('Unsupported object type: "'+ itemType +'"'); continue;
//throw new Error('Unsupported object type: "'+ itemType +'"');
} }
itemFactory.preload(this.load); itemFactory.preload(this.load);
@ -289,6 +303,12 @@ export class GameScene extends ResizableScene implements CenterListener {
}); });
}); });
} }
// Now, let's load the script, if any
const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) {
iframeListener.registerScript(script);
}
} }
//hook initialisation //hook initialisation
@ -373,19 +393,21 @@ export class GameScene extends ResizableScene implements CenterListener {
this.initCirclesCanvas(); this.initCirclesCanvas();
// Let's pause the scene if the connection is not established yet // Let's pause the scene if the connection is not established yet
if (this.isReconnecting) { if (!this.room.isDisconnected()) {
setTimeout(() => { if (this.isReconnecting) {
this.scene.sleep(); setTimeout(() => {
this.scene.launch(ReconnectingSceneName);
}, 0);
} else if (this.connection === undefined) {
// Let's wait 1 second before printing the "connecting" screen to avoid blinking
setTimeout(() => {
if (this.connection === undefined) {
this.scene.sleep(); this.scene.sleep();
this.scene.launch(ReconnectingSceneName); this.scene.launch(ReconnectingSceneName);
} }, 0);
}, 1000); } else if (this.connection === undefined) {
// Let's wait 1 second before printing the "connecting" screen to avoid blinking
setTimeout(() => {
if (this.connection === undefined) {
this.scene.sleep();
this.scene.launch(ReconnectingSceneName);
}
}, 1000);
}
} }
this.createPromiseResolve(); this.createPromiseResolve();
@ -410,7 +432,18 @@ export class GameScene extends ResizableScene implements CenterListener {
// From now, this game scene will be notified of reposition events // From now, this game scene will be notified of reposition events
layoutManager.setListener(this); layoutManager.setListener(this);
this.triggerOnMapLayerPropertyChange(); this.triggerOnMapLayerPropertyChange();
this.listenToIframeEvents();
if (!this.room.isDisconnected()) {
this.connect();
}
}
/**
* Initializes the connection to Pusher.
*/
private connect(): void {
const camera = this.cameras.main; const camera = this.cameras.main;
connectionManager.connectToRoomSocket( connectionManager.connectToRoomSocket(
@ -548,7 +581,6 @@ export class GameScene extends ResizableScene implements CenterListener {
}); });
} }
//todo: into dedicated classes //todo: into dedicated classes
private initCirclesCanvas(): void { private initCirclesCanvas(): void {
// Let's generate the circle for the group delimiter // Let's generate the circle for the group delimiter
@ -577,7 +609,7 @@ export class GameScene extends ResizableScene implements CenterListener {
const contextRed = this.circleRedTexture.context; const contextRed = this.circleRedTexture.context;
contextRed.beginPath(); contextRed.beginPath();
contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false);
// context.lineWidth = 5; //context.lineWidth = 5;
contextRed.strokeStyle = '#ff0000'; contextRed.strokeStyle = '#ff0000';
contextRed.stroke(); contextRed.stroke();
this.circleRedTexture.refresh(); this.circleRedTexture.refresh();
@ -606,7 +638,7 @@ export class GameScene extends ResizableScene implements CenterListener {
coWebsiteManager.closeCoWebsite(); coWebsiteManager.closeCoWebsite();
}else{ }else{
const openWebsiteFunction = () => { const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsitePolicy') as string | undefined); coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined);
layoutManager.removeActionButton('openWebsite', this.userInputManager); layoutManager.removeActionButton('openWebsite', this.userInputManager);
}; };
@ -672,6 +704,103 @@ export class GameScene extends ResizableScene implements CenterListener {
this.gameMap.onPropertyChange('playAudioLoop', (newValue, oldValue) => { this.gameMap.onPropertyChange('playAudioLoop', (newValue, oldValue) => {
newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true); newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true);
}); });
this.gameMap.onPropertyChange('zone', (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === '') {
iframeListener.sendLeaveEvent(oldValue as string);
} else {
iframeListener.sendEnterEvent(newValue as string);
}
});
}
private listenToIframeEvents(): void {
iframeListener.openPopupStream.subscribe((openPopupEvent) => {
let objectLayerSquare : ITiledMapObject;
const targetObjectData = this.getObjectLayerData(openPopupEvent.targetObject);
if (targetObjectData !== undefined){
objectLayerSquare = targetObjectData;
} else {
console.error("Error while opening a popup. Cannot find an object on the map with name '" + openPopupEvent.targetObject + "'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map.");
return;
}
const escapedMessage = HtmlUtils.escapeHtml(openPopupEvent.message);
let html = `<div id="container"><div class="nes-container with-title is-centered">
${escapedMessage}
</div> </div>`;
const buttonContainer = `<div class="buttonContainer"</div>`;
html += buttonContainer;
let id = 0;
for (const button of openPopupEvent.buttons) {
html += `<button type="button" class="nes-btn is-${HtmlUtils.escapeHtml(button.className ?? '')}" id="popup-${openPopupEvent.popupId}-${id}">${HtmlUtils.escapeHtml(button.label)}</button>`;
id++;
}
const domElement = this.add.dom(objectLayerSquare.x + objectLayerSquare.width/2 ,
objectLayerSquare.y + objectLayerSquare.height/2).createFromHTML(html);
const container : HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement;
container.style.width = objectLayerSquare.width + "px";
domElement.scale = 0;
domElement.setClassName('popUpElement');
id = 0;
for (const button of openPopupEvent.buttons) {
const button = HtmlUtils.getElementByIdOrFail<HTMLButtonElement>(`popup-${openPopupEvent.popupId}-${id}`);
const btnId = id;
button.onclick = () => {
iframeListener.sendButtonClickedEvent(openPopupEvent.popupId, btnId);
}
id++;
}
this.tweens.add({
targets : domElement ,
scale : 1,
ease : "EaseOut",
duration : 400,
});
this.popUpElements.set(openPopupEvent.popupId, domElement);
});
iframeListener.closePopupStream.subscribe((closePopupEvent) => {
const popUpElement = this.popUpElements.get(closePopupEvent.popupId);
if (popUpElement === undefined) {
console.error('Could not close popup with ID ', closePopupEvent.popupId,'. Maybe it has already been closed?');
}
this.tweens.add({
targets : popUpElement ,
scale : 0,
ease : "EaseOut",
duration : 400,
onComplete : () => {
popUpElement?.destroy();
this.popUpElements.delete(closePopupEvent.popupId);
},
});
});
iframeListener.disablePlayerControlStream.subscribe(()=>{
this.userInputManager.disableControls();
})
iframeListener.enablePlayerControlStream.subscribe(()=>{
this.userInputManager.restoreControls();
})
let scriptedBubbleSprite : Sprite;
iframeListener.displayBubbleStream.subscribe(()=>{
scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white');
scriptedBubbleSprite.setDisplayOrigin(48, 48);
this.add.existing(scriptedBubbleSprite);
})
iframeListener.removeBubbleStream.subscribe(()=>{
scriptedBubbleSprite.destroy();
})
} }
private getMapDirUrl(): string { private getMapDirUrl(): string {
@ -702,6 +831,12 @@ export class GameScene extends ResizableScene implements CenterListener {
public cleanupClosingScene(): void { public cleanupClosingScene(): void {
// stop playing audio, close any open website, stop any open Jitsi // stop playing audio, close any open website, stop any open Jitsi
coWebsiteManager.closeCoWebsite(); coWebsiteManager.closeCoWebsite();
// Stop the script, if any
const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) {
iframeListener.unregisterScript(script);
}
this.stopJitsi(); this.stopJitsi();
audioManager.unloadAudio(); audioManager.unloadAudio();
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
@ -785,8 +920,12 @@ export class GameScene extends ResizableScene implements CenterListener {
return this.getProperty(layer, "startLayer") == true; return this.getProperty(layer, "startLayer") == true;
} }
private getProperty(layer: ITiledMapLayer, name: string): string|boolean|number|undefined { private getScriptUrls(map: ITiledMap): string[] {
const properties = layer.properties; return (this.getProperties(map, "script") as string[]).map((script) => (new URL(script, this.MapUrlFile)).toString());
}
private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined {
const properties: ITiledMapLayerProperty[] = layer.properties;
if (!properties) { if (!properties) {
return undefined; return undefined;
} }
@ -797,6 +936,14 @@ export class GameScene extends ResizableScene implements CenterListener {
return obj.value; return obj.value;
} }
private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] {
const properties: ITiledMapLayerProperty[] = layer.properties;
if (!properties) {
return [];
}
return properties.filter((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()).map((property) => property.value);
}
//todo: push that into the gameManager //todo: push that into the gameManager
private async loadNextGame(exitSceneIdentifier: string){ private async loadNextGame(exitSceneIdentifier: string){
const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
@ -1176,7 +1323,19 @@ export class GameScene extends ResizableScene implements CenterListener {
bottom: camera.scrollY + camera.height, bottom: camera.scrollY + camera.height,
}); });
} }
private getObjectLayerData(objectName : string) : ITiledMapObject| undefined{
for (const layer of this.mapFile.layers) {
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
for (const object of layer.objects) {
if (object.name === objectName) {
return object;
}
}
}
}
return undefined;
}
private reposition(): void { private reposition(): void {
this.presentationModeSprite.setY(this.game.renderer.height - 2); this.presentationModeSprite.setY(this.game.renderer.height - 2);
this.chatModeSprite.setY(this.game.renderer.height - 2); this.chatModeSprite.setY(this.game.renderer.height - 2);
@ -1233,7 +1392,7 @@ export class GameScene extends ResizableScene implements CenterListener {
//todo: put this into an 'orchestrator' scene (EntryScene?) //todo: put this into an 'orchestrator' scene (EntryScene?)
private bannedUser(){ private bannedUser(){
this.cleanupClosingScene(); this.cleanupClosingScene();
this.userInputManager.clearAllKeys(); this.userInputManager.disableControls();
this.scene.start(ErrorSceneName, { this.scene.start(ErrorSceneName, {
title: 'Banned', title: 'Banned',
subTitle: 'You were banned from WorkAdventure', subTitle: 'You were banned from WorkAdventure',
@ -1245,7 +1404,7 @@ export class GameScene extends ResizableScene implements CenterListener {
private showWorldFullError(): void { private showWorldFullError(): void {
this.cleanupClosingScene(); this.cleanupClosingScene();
this.scene.stop(ReconnectingSceneName); this.scene.stop(ReconnectingSceneName);
this.userInputManager.clearAllKeys(); this.userInputManager.disableControls();
this.scene.start(ErrorSceneName, { this.scene.start(ErrorSceneName, {
title: 'Connection rejected', title: 'Connection rejected',
subTitle: 'The world you are trying to join is full. Try again later.', subTitle: 'The world you are trying to join is full. Try again later.',

View File

@ -14,7 +14,7 @@ export interface ITiledMap {
* Map orientation (orthogonal) * Map orientation (orthogonal)
*/ */
orientation: string; orientation: string;
properties: {[key: string]: string}; properties: ITiledMapLayerProperty[];
/** /**
* Render order (right-down) * Render order (right-down)

View File

@ -61,7 +61,7 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
this.opened = true; this.opened = true;
gameManager.getCurrentGameScene(this.scene).userInputManager.clearAllKeys(); gameManager.getCurrentGameScene(this.scene).userInputManager.disableControls();
this.scene.tweens.add({ this.scene.tweens.add({
targets: this, targets: this,

View File

@ -31,10 +31,11 @@ export class ActiveEventList {
export class UserInputManager { export class UserInputManager {
private KeysCode!: UserInputManagerDatum[]; private KeysCode!: UserInputManagerDatum[];
private Scene: GameScene; private Scene: GameScene;
private isInputDisabled : boolean;
constructor(Scene : GameScene) { constructor(Scene : GameScene) {
this.Scene = Scene; this.Scene = Scene;
this.initKeyBoardEvent(); this.initKeyBoardEvent();
this.isInputDisabled = false;
} }
initKeyBoardEvent(){ initKeyBoardEvent(){
@ -63,16 +64,25 @@ export class UserInputManager {
this.Scene.input.keyboard.removeAllListeners(); this.Scene.input.keyboard.removeAllListeners();
} }
clearAllKeys(){ disableControls(){
this.Scene.input.keyboard.removeAllKeys(); this.Scene.input.keyboard.removeAllKeys();
this.isInputDisabled = true;
} }
restoreControls(){
this.initKeyBoardEvent();
this.isInputDisabled = false;
}
getEventListForGameTick(): ActiveEventList { getEventListForGameTick(): ActiveEventList {
const eventsMap = new ActiveEventList(); const eventsMap = new ActiveEventList();
if (this.isInputDisabled) {
return eventsMap;
}
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);
} }
}); });
return eventsMap; return eventsMap;
} }

View File

@ -35,7 +35,8 @@ class UrlManager {
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;
history.pushState({}, 'WorkAdventure', room.id+hash); const search = room.search.toString();
history.pushState({}, 'WorkAdventure', room.id+(search?'?'+search:'')+hash);
} }
public getStartLayerNameFromUrl(): string|null { public getStartLayerNameFromUrl(): string|null {

View File

@ -1,5 +1,6 @@
import {HtmlUtils} from "./HtmlUtils"; import {HtmlUtils} from "./HtmlUtils";
import {Subject} from "rxjs"; import {Subject} from "rxjs";
import {iframeListener} from "../Api/IframeListener";
enum iframeStates { enum iframeStates {
closed = 1, closed = 1,
@ -126,7 +127,7 @@ class CoWebsiteManager {
return iframe; return iframe;
} }
public loadCoWebsite(url: string, base: string, allowPolicy?: string): void { public loadCoWebsite(url: string, base: string, allowApi?: boolean, allowPolicy?: string): void {
this.load(); this.load();
this.cowebsiteMainDom.innerHTML = ``; this.cowebsiteMainDom.innerHTML = ``;
@ -139,6 +140,9 @@ class CoWebsiteManager {
const onloadPromise = new Promise((resolve) => { const onloadPromise = new Promise((resolve) => {
iframe.onload = () => resolve(); iframe.onload = () => resolve();
}); });
if (allowApi) {
iframeListener.registerIframe(iframe);
}
this.cowebsiteMainDom.appendChild(iframe); this.cowebsiteMainDom.appendChild(iframe);
const onTimeoutPromise = new Promise((resolve) => { const onTimeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(), 2000); setTimeout(() => resolve(), 2000);
@ -170,6 +174,10 @@ class CoWebsiteManager {
if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example
this.close(); this.close();
this.fire(); this.fire();
const iframe = this.cowebsiteDiv.querySelector('iframe');
if (iframe) {
iframeListener.unregisterIframe(iframe);
}
setTimeout(() => { setTimeout(() => {
this.cowebsiteMainDom.innerHTML = ``; this.cowebsiteMainDom.innerHTML = ``;
resolve(); resolve();

View File

@ -3,6 +3,7 @@ import {mediaManager, ReportCallback, ShowReportCallBack} from "./MediaManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {connectionManager} from "../Connexion/ConnectionManager"; import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager"; import {GameConnexionTypes} from "../Url/UrlManager";
import {iframeListener} from "../Api/IframeListener";
export type SendMessageCallback = (message:string) => void; export type SendMessageCallback = (message:string) => void;
@ -25,6 +26,14 @@ export class DiscussionManager {
constructor() { constructor() {
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container'); this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
this.createDiscussPart(''); //todo: why do we always use empty string? this.createDiscussPart(''); //todo: why do we always use empty string?
iframeListener.chatStream.subscribe((chatEvent) => {
this.addMessage(chatEvent.author, chatEvent.message, false);
this.showDiscussion();
});
this.onSendMessageCallback('iframe_listener', (message) => {
iframeListener.sendUserInputChat(message);
})
} }
private createDiscussPart(name: string) { private createDiscussPart(name: string) {
@ -61,12 +70,12 @@ export class DiscussionManager {
const inputMessage: HTMLInputElement = document.createElement('input'); const inputMessage: HTMLInputElement = document.createElement('input');
inputMessage.onfocus = () => { inputMessage.onfocus = () => {
if(this.userInputManager) { if(this.userInputManager) {
this.userInputManager.clearAllKeys(); this.userInputManager.disableControls();
} }
} }
inputMessage.onblur = () => { inputMessage.onblur = () => {
if(this.userInputManager) { if(this.userInputManager) {
this.userInputManager.initKeyBoardEvent(); this.userInputManager.restoreControls();
} }
} }
inputMessage.type = "text"; inputMessage.type = "text";

View File

@ -24,7 +24,7 @@ export class HtmlUtils {
throw new Error("Cannot find HTML element with id '"+id+"'"); throw new Error("Cannot find HTML element with id '"+id+"'");
} }
private static escapeHtml(html: string): string { public static escapeHtml(html: string): string {
const text = document.createTextNode(html); const text = document.createTextNode(html);
const p = document.createElement('p'); const p = document.createElement('p');
p.appendChild(text); p.appendChild(text);

232
front/src/iframe_api.ts Normal file
View File

@ -0,0 +1,232 @@
import {ChatEvent, isChatEvent} from "./Api/Events/ChatEvent";
import {isIframeEventWrapper} from "./Api/Events/IframeEvent";
import {isUserInputChatEvent, UserInputChatEvent} from "./Api/Events/UserInputChatEvent";
import {Subject} from "rxjs";
import {EnterLeaveEvent, isEnterLeaveEvent} from "./Api/Events/EnterLeaveEvent";
import {OpenPopupEvent} from "./Api/Events/OpenPopupEvent";
import {isButtonClickedEvent} from "./Api/Events/ButtonClickedEvent";
import {ClosePopupEvent} from "./Api/Events/ClosePopupEvent";
import {OpenTabEvent} from "./Api/Events/OpenTabEvent";
import {GoToPageEvent} from "./Api/Events/GoToPageEvent";
import {OpenCoWebSiteEvent} from "./Api/Events/OpenCoWebSiteEvent";
interface WorkAdventureApi {
sendChatMessage(message: string, author: string): void;
onChatMessage(callback: (message: string) => void): void;
onEnterZone(name: string, callback: () => void): void;
onLeaveZone(name: string, callback: () => void): void;
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup;
openTab(url : string): void;
goToPage(url : string): void;
openCoWebSite(url : string): void;
closeCoWebSite(): void;
disablePlayerControl() : void;
restorePlayerControl() : void;
displayBubble() : void;
removeBubble() : void;
}
declare global {
// eslint-disable-next-line no-var
var WA: WorkAdventureApi
}
type ChatMessageCallback = (message: string) => void;
type ButtonClickedCallback = (popup: Popup) => void;
const userInputChatStream: Subject<UserInputChatEvent> = new Subject();
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const popups: Map<number, Popup> = new Map<number, Popup>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
let popupId = 0;
interface ButtonDescriptor {
/**
* The label of the button
*/
label: string,
/**
* The type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled"
*/
className?: "normal"|"primary"|"success"|"warning"|"error"|"disabled",
/**
* Callback called if the button is pressed
*/
callback: ButtonClickedCallback,
}
class Popup {
constructor(private id: number) {
}
/**
* Closes the popup
*/
public close(): void {
window.parent.postMessage({
'type': 'closePopup',
'data': {
'popupId': this.id,
} as ClosePopupEvent
}, '*');
}
}
window.WA = {
/**
* Send a message in the chat.
* Only the local user will receive this message.
*/
sendChatMessage(message: string, author: string) {
window.parent.postMessage({
'type': 'chat',
'data': {
'message': message,
'author': author
} as ChatEvent
}, '*');
},
disablePlayerControl() : void {
window.parent.postMessage({'type' : 'disablePlayerControl'},'*');
},
restorePlayerControl() : void {
window.parent.postMessage({'type' : 'restorePlayerControl'},'*');
},
displayBubble() : void {
window.parent.postMessage({'type' : 'displayBubble'},'*');
},
removeBubble() : void {
window.parent.postMessage({'type' : 'removeBubble'},'*');
},
openTab(url : string) : void{
window.parent.postMessage({
"type" : 'openTab',
"data" : {
url
} as OpenTabEvent
},'*');
},
goToPage(url : string) : void{
window.parent.postMessage({
"type" : 'goToPage',
"data" : {
url
} as GoToPageEvent
},'*');
},
openCoWebSite(url : string) : void{
window.parent.postMessage({
"type" : 'openCoWebSite',
"data" : {
url
} as OpenCoWebSiteEvent
},'*');
},
closeCoWebSite() : void{
window.parent.postMessage({
"type" : 'closeCoWebSite'
},'*');
},
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
popupId++;
const popup = new Popup(popupId);
const btnMap = new Map<number, () => void>();
popupCallbacks.set(popupId, btnMap);
let id = 0;
for (const button of buttons) {
const callback = button.callback;
if (callback) {
btnMap.set(id, () => {
callback(popup);
});
}
id++;
}
window.parent.postMessage({
'type': 'openPopup',
'data': {
popupId,
targetObject,
message,
buttons: buttons.map((button) => {
return {
label: button.label,
className: button.className
};
})
} as OpenPopupEvent
}, '*');
popups.set(popupId, popup)
return popup;
},
/**
* Listen to messages sent by the local user, in the chat.
*/
onChatMessage(callback: ChatMessageCallback): void {
userInputChatStream.subscribe((userInputChatEvent) => {
callback(userInputChatEvent.message);
});
},
onEnterZone(name: string, callback: () => void): void {
let subject = enterStreams.get(name);
if (subject === undefined) {
subject = new Subject<EnterLeaveEvent>();
enterStreams.set(name, subject);
}
subject.subscribe(callback);
},
onLeaveZone(name: string, callback: () => void): void {
let subject = leaveStreams.get(name);
if (subject === undefined) {
subject = new Subject<EnterLeaveEvent>();
leaveStreams.set(name, subject);
}
subject.subscribe(callback);
},
}
window.addEventListener('message', message => {
if (message.source !== window.parent) {
return; // Skip message in this event listener
}
const payload = message.data;
console.log(payload);
if (isIframeEventWrapper(payload)) {
const payloadData = payload.data;
if (payload.type === 'userInputChat' && isUserInputChatEvent(payloadData)) {
userInputChatStream.next(payloadData);
} else if (payload.type === 'enterEvent' && isEnterLeaveEvent(payloadData)) {
enterStreams.get(payloadData.name)?.next();
} else if (payload.type === 'leaveEvent' && isEnterLeaveEvent(payloadData)) {
leaveStreams.get(payloadData.name)?.next();
} else if (payload.type === 'buttonClickedEvent' && isButtonClickedEvent(payloadData)) {
const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId);
const popup = popups.get(payloadData.popupId);
if (popup === undefined) {
throw new Error('Could not find popup with ID "'+payloadData.popupId+'"');
}
if (callback) {
callback(popup);
}
}
}
// ...
});

View File

@ -15,6 +15,8 @@ import {MenuScene} from "./Phaser/Menu/MenuScene";
import {HelpCameraSettingsScene} from "./Phaser/Menu/HelpCameraSettingsScene"; import {HelpCameraSettingsScene} from "./Phaser/Menu/HelpCameraSettingsScene";
import {localUserStore} from "./Connexion/LocalUserStore"; import {localUserStore} from "./Connexion/LocalUserStore";
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene"; import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
import {iframeListener} from "./Api/IframeListener";
import {discussionManager} from "./WebRtc/DiscussionManager";
const {width, height} = coWebsiteManager.getGameSize(); const {width, height} = coWebsiteManager.getGameSize();
@ -119,3 +121,5 @@ coWebsiteManager.onResize.subscribe(() => {
const {width, height} = coWebsiteManager.getGameSize(); const {width, height} = coWebsiteManager.getGameSize();
game.scale.resize(width / RESOLUTION, height / RESOLUTION); game.scale.resize(width / RESOLUTION, height / RESOLUTION);
}); });
iframeListener.init();

View File

@ -4,11 +4,15 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = { module.exports = {
entry: './src/index.ts', entry: {
'main': './src/index.ts',
'iframe_api': './src/iframe_api.ts'
},
devtool: 'inline-source-map', devtool: 'inline-source-map',
devServer: { devServer: {
contentBase: './dist', contentBase: './dist',
host: '0.0.0.0', host: '0.0.0.0',
sockPort: 80,
disableHostCheck: true, disableHostCheck: true,
historyApiFallback: { historyApiFallback: {
rewrites: [ rewrites: [
@ -34,7 +38,11 @@ module.exports = {
extensions: [ '.tsx', '.ts', '.js' ], extensions: [ '.tsx', '.ts', '.js' ],
}, },
output: { output: {
filename: '[name].[contenthash].js', filename: (pathData) => {
// Add a content hash only for the main bundle.
// We want the iframe_api.js file to keep its name as it will be referenced from outside iframes.
return pathData.chunk.name === 'main' ? 'js/[name].[contenthash].js': '[name].js';
},
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
publicPath: '/' publicPath: '/'
}, },
@ -54,25 +62,27 @@ module.exports = {
removeScriptTypeAttributes: true, removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true, removeStyleLinkTypeAttributes: true,
useShortDoctype: true useShortDoctype: true
} },
chunks: ['main']
} }
), ),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
Phaser: 'phaser' Phaser: 'phaser'
}), }),
new webpack.EnvironmentPlugin([ new webpack.EnvironmentPlugin({
'API_URL', 'API_URL': null,
'UPLOADER_URL', 'PUSHER_URL': undefined,
'ADMIN_URL', 'UPLOADER_URL': null,
'DEBUG_MODE', 'ADMIN_URL': null,
'STUN_SERVER', 'DEBUG_MODE': null,
'TURN_SERVER', 'STUN_SERVER': null,
'TURN_USER', 'TURN_SERVER': null,
'TURN_PASSWORD', 'TURN_USER': null,
'JITSI_URL', 'TURN_PASSWORD': null,
'JITSI_PRIVATE_MODE', 'JITSI_URL': null,
'START_ROOM_URL' 'JITSI_PRIVATE_MODE': null,
]) 'START_ROOM_URL': null
})
], ],
}; };

View File

@ -5362,9 +5362,9 @@ xtend@^4.0.0, xtend@~4.0.1:
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
y18n@^4.0.0: y18n@^4.0.0:
version "4.0.0" version "4.0.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
yallist@^3.0.2: yallist@^3.0.2:
version "3.1.1" version "3.1.1"

View File

@ -1,4 +1,3 @@
# we are rebuilding on each deploy to cope with the API_URL environment URL
FROM thecodingmachine/nodejs:12-apache FROM thecodingmachine/nodejs:12-apache
COPY --chown=docker:docker . . COPY --chown=docker:docker . .

View File

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

BIN
maps/Tuto/Male 13-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
maps/Tuto/fantasy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

74
maps/Tuto/scriptTuto.js Normal file
View File

@ -0,0 +1,74 @@
var isFirstTimeTuto = false;
var textFirstPopup = 'Hey ! This is how to start a discussion with someone ! You can be 4 max in a bubble.';
var textSecondPopup = 'You can also use the chat to communicate ! ';
var targetObjectTutoBubble ='Tutobubble';
var targetObjectTutoChat ='tutoChat';
var targetObjectTutoExplanation ='tutoExplanation';
var popUpExplanation = undefined;
function launchTuto (){
WA.openPopup(targetObjectTutoBubble, textFirstPopup, [
{
label: "Next",
className: "popUpElement",
callback: (popup) => {
popup.close();
WA.openPopup(targetObjectTutoChat, textSecondPopup, [
{
label: "Open Chat",
className: "popUpElement",
callback: (popup1) => {
WA.sendChatMessage("Hey you can talk here too!", 'WA Guide');
popup1.close();
WA.openPopup("TutoFinal","You are good to go! You can meet the dev team and discover the features in the next room!",[
{
label: "Got it!",
className : "success",callback:(popup2 => {
popup2.close();
WA.restorePlayerControl();
})
}
])
}
}
])
}
}
]);
WA.disablePlayerControl();
}
WA.onEnterZone('popupZone', () => {
WA.displayBubble();
if (!isFirstTimeTuto) {
isFirstTimeTuto = true;
launchTuto();
}
else {
popUpExplanation = WA.openPopup(targetObjectTutoExplanation, 'Do you want to review the explanation?', [
{
label: "No",
className: "error",
callback: (popup) => {
popup.close();
}
},
{
label: "Yes",
className: "success",
callback: (popup) => {
popup.close();
launchTuto();
}
}
])
}
});
WA.onLeaveZone('popupZone', () => {
if (popUpExplanation !== undefined) popUpExplanation.close();
WA.removeBubble();
})

BIN
maps/Tuto/shift.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
maps/Tuto/textTuto3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,4 @@
LPC Atlas :
https://opengameart.org/content/lpc-tile-atlas
LPC Atlas 2 :
https://opengameart.org/content/lpc-tile-atlas2

View File

@ -0,0 +1,139 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
- See the file: cc-by-sa-3.0.txt
GNU GPL 3.0:
- http://www.gnu.org/licenses/gpl-3.0.html
- See the file: gpl-3.0.txt
Note the file is based on the LCP contest readme so don't expect the exact little pieces used like the base one.
*Additional license information.
Assets from:
LPC participants:
----------------
Casper Nilsson
*GNU GPL 3.0 or later
email: casper.nilsson@gmail.com
Freenode: CasperN
OpenGameArt.org: C.Nilsson
- LPC C.Nilsson (2D art)
Daniel Eddeland
*GNU GPL 3.0 or later
- Tilesets of plants, props, food and environments, suitable for farming / fishing sims and other games.
- Includes wheat, grass, sand tilesets, fence tilesets and plants such as corn and tomato.
Johann CHARLOT
*GNU LGPL Version 3.
*Later versions are permitted.
Homepage http://poufpoufproduction.fr
Email johannc@poufpoufproduction.fr
- Shoot'em up graphic kit
Skyler Robert Colladay
- FeralFantom's Entry (2D art)
BASE assets:
------------
Lanea Zimmerman (AKA Sharm)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- barrel.png
- brackish.png
- buckets.png
- bridges.png
- cabinets.png
- cement.png
- cementstair.png
- chests.png
- country.png
- cup.png
- dirt2.png
- dirt.png
- dungeon.png
- grassalt.png
- grass.png
- holek.png
- holemid.png
- hole.png
- house.png
- inside.png
- kitchen.png
- lava.png
- lavarock.png
- mountains.png
- rock.png
- shadow.png
- signs.png
- stairs.png
- treetop.png
- trunk.png
- waterfall.png
- watergrass.png
- water.png
- princess.png and princess.xcf
Stephen Challener (AKA Redshrike)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- female_walkcycle.png
- female_hurt.png
- female_slash.png
- female_spellcast.png
- male_walkcycle.png
- male_hurt.png
- male_slash.png
- male_spellcast.png
- male_pants.png
- male_hurt_pants.png
- male_fall_down_pants.png
- male_slash_pants.png
Charles Sanchez (AKA CharlesGabriel)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- bat.png
- bee.png
- big_worm.png
- eyeball.png
- ghost.png
- man_eater_flower.png
- pumpking.png
- slime.png
- small_worm.png
- snake.png
Manuel Riecke (AKA MrBeast)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- hairfemale.png and hairfemale.xcf
- hairmale.png and hairmale.xcf
- soldier.png
- soldier_altcolor.png
Daniel Armstrong (AKA HughSpectrum)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Castle work:
- castlewalls.png
- castlefloors.png
- castle_outside.png
- castlefloors_outside.png
- castle_lightsources.png

View File

@ -0,0 +1,166 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
- See the file: cc-by-sa-3.0.txt
GNU GPL 3.0:
- http://www.gnu.org/licenses/gpl-3.0.html
- See the file: gpl-3.0.txt
Note only some files from the entries are selected.
*Additional license information.
Assets from:
LPC participants:
----------------
Barbara Rivera
- tree,tombstone
Casper Nilsson
*GNU GPL 3.0 or later
email: casper.nilsson@gmail.com
Freenode: CasperN
OpenGameArt.org: C.Nilsson
- LPC C.Nilsson (2D art)
Chris Phillips
- tree
Daniel Eddeland
*GNU GPL 3.0 or later
- Tilesets of plants, props, food and environments, suitable for farming / fishing sims and other games.
- Includes wheat, grass, sand tilesets, fence tilesets and plants such as corn and tomato.
- Also includes village/marketplace objects like sacks, food, some smithing equipment, tables and stalls.
Anamaris and Krusmira (aka? Emilio J Sanchez)
- Sierra__Steampun-a-fy (with concept art)
Jonas Klinger
- Skorpio's SciFi Sprite Pack
Joshua Taylor
- Fruit and Veggie Inventory
Leo Villeveygoux
- Limestone Wall
Mark Weyer
- signpost+shadow
Matthew Nash
- Public Toilet Tileset
Skyler Robert Colladay
- FeralFantom's Entry
BASE assets:
------------
Lanea Zimmerman (AKA Sharm)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- barrel.png
- brackish.png
- buckets.png
- bridges.png
- cabinets.png
- cement.png
- cementstair.png
- chests.png
- country.png
- cup.png
- dirt2.png
- dirt.png
- dungeon.png
- grassalt.png
- grass.png
- holek.png
- holemid.png
- hole.png
- house.png
- inside.png
- kitchen.png
- lava.png
- lavarock.png
- mountains.png
- rock.png
- shadow.png
- signs.png
- stairs.png
- treetop.png
- trunk.png
- waterfall.png
- watergrass.png
- water.png
- princess.png and princess.xcf
Stephen Challener (AKA Redshrike)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- female_walkcycle.png
- female_hurt.png
- female_slash.png
- female_spellcast.png
- male_walkcycle.png
- male_hurt.png
- male_slash.png
- male_spellcast.png
- male_pants.png
- male_hurt_pants.png
- male_fall_down_pants.png
- male_slash_pants.png
Charles Sanchez (AKA CharlesGabriel)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- bat.png
- bee.png
- big_worm.png
- eyeball.png
- ghost.png
- man_eater_flower.png
- pumpking.png
- slime.png
- small_worm.png
- snake.png
Manuel Riecke (AKA MrBeast)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- hairfemale.png and hairfemale.xcf
- hairmale.png and hairmale.xcf
- soldier.png
- soldier_altcolor.png
Daniel Armstrong (AKA HughSpectrum)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Castle work:
- castlewalls.png
- castlefloors.png
- castle_outside.png
- castlefloors_outside.png
- castle_lightsources.png

View File

@ -0,0 +1,2 @@
https://opengameart.org/content/lpc-interior-castle-tiles
credit Lanea Zimmerman

View File

@ -0,0 +1,55 @@
= Wooden floor(CC-BY-SA) =
* Horizontal wooden floor by Lanea Zimmerman (AKA Sharm)
* Horizontal wooden floor with hole by Lanea Zimmerman (AKA Sharm) and Tuomo Untinen
* Vertical wooden floor by Tuomo Untinen
* Vertical wooden floor with hole by Tuomo Untinen
= Wooden wall topping (CC-BY-SA) =
* Tuomo Untinen
= Pile of barrels =
* Based LPC base tiles by Lanea Zimmerman (AKA Sharm)
= Decorational stuff (CC-BY-SA) =
* Green bottle
* Wine glass and bottle
* Hanging sacks
* Wall mounted rope and ropes
* Wall mounted swords
* Wall mounted kite shield
* Wall hole
By Tuomo Untinen
* Small sack from LPC farming tileset by Daniel Eddeland (http://opengameart.org/content/lpc- farming-tilesets-magic-animations-and-ui-elements)
* Purple bottles and gray lantern from Hyptosis Mage city
* Green and blue bottle by Tuomo Untinen
= Wall clock (CC-BY-SA) =
* Lanea Zimmerman AKA Sharm
* Tuomo Untinen (Scaled down and animation)
= Stone floor (CC-BY-SA)=
* Tuomo Untinen
= Cobble stone floor (CC-BY-SA)=
* Based on LPC base tileset by Lanea Zimmerman (AKA Sharm)
= Cabinets and kitchen stuff including metal stove(CC-BY-SA) =
* Based on LPC base tileset by Lanea Zimmerman (AKA Sharm)
* Cutboard is made by Hyptosis
* Sacks based on LPC farming tileset by Daniel Eddeland (http://opengameart.org/content/lpc- farming-tilesets-magic-animations-and-ui-elements)
* Spears by Tuomo Untinen
* Vertical chest by Tuomo Untinen based on LPC base tiles Lanea Zimmerman (AKA Sharm)
Manuel Riecke (AKA MrBeast)
= Skull (CC-BY-SA) =
* http://opengameart.org/content/lpc-dungeon-elements
* Graphical artist Lanea Zimmerman AKA Sharm
* Contributor William Thompson
= pile sacks =
* LPC farming tileset by Daniel Eddeland (http://opengameart.org/content/lpc- farming-tilesets-magic-animations-and-ui-elements)
= Pile of papers(CC-BY-SA) =
* Based on caeles papers
= Armor shelves(CC-BY-SA) =
* Based on LPC base tileset by Lanea Zimmerman (AKA Sharm)
* Armors by: Adapted by Matthew Krohn from art created by Johannes Sjölund
= Table lamp =
* Tuomo Untinen
= Distiller =
* Table is from LPC base tileset by Lanea Zimmerman (AKA Sharm)
* Distiller by Tuomo Untinen
= Fireplace =
* Tuomo Untinen
* Inspired by Lanea Zimmerman (AKA Sharm) Fireplace

View File

@ -0,0 +1,28 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
GNU GPL 3.0:
- http://www.gnu.org/licenses/gpl-3.0.html
If you need to figure out exactly who made what please see the Liberated Pixel Cup entries.
Liberated Pixel Cup Assets:
http://opengameart.org/lpc-art-entries
LPC participants:
----------------
Johann CHARLOT
Homepage http://poufpoufproduction.fr
Email johannc@poufpoufproduction.fr
- Shoot'em up graphic kit
Recolored Leaves
William Thompson
Email: william.thompsonj@gmail.com
OpenGameArt.org: williamthompsonj

View File

@ -0,0 +1 @@
https://opengameart.org/content/lpc-leaf-recolor

View File

@ -0,0 +1 @@
https://opengameart.org/content/lpc-submissions-merged

View File

@ -0,0 +1 @@
https://opengameart.org/content/lpc-mountains

View File

@ -0,0 +1,344 @@
License
-------
CC-BY-SA 3.0:
http://creativecommons.org/licenses/by-sa/3.0/
GNU GPL 3.0:
http://www.gnu.org/licenses/gpl-3.0.html
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Terrain and Outside:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm) - CC-BY-3.0 / GPL 3.0 / GPL 2.0 / OGA-BY-3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- barrels
- darkishgreen water
- buckets
- bridges
- cement
- cement stairs
- chests
- cup
- light dirt
- mid dirt
- dungeon
- grass1 (leftmost)
- grass2 (Middle grass)
- hole1 (left hole near lava)
- hole2 (middle hole)
- hole3 (black whole next to transparent water)
- lava
- lavarock (black dirt)
- mountains ridge (right of the water tiles)
- white rocks
- waterfall
- water/grass
- water (transparent water beside black hole)
Daniel Eddeland CC-BY-SA-3.0 / GPL 3.0
https://opengameart.org/content/lpc-farming-tilesets-magic-animations-and-ui-elements
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Plowed Ground
- Water reeds
- Sand
- Sand with water
- Tall grass
- Wheat
- Young wheet (green wheat left of tall grass)
William Thompsonj
https://opengameart.org/content/lpc-sandrock-alt-colors
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- sand (near the wheat)
- sand with water
- grey dirt left of the lava)
Matthew Krohn (Makrohn)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Cave / Mountain steps originals from Lanea Zimmerman
Matthew Nash
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- stone tile (purplish color beside the dirt path bottom right corner)
Nushio
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Ice tiles
- Snow
- snow/ice
- Snow water
Casper Nilson
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- grass with flowers
- stone pattern below cement
- tree stumps
- lily pads
MISSING:
Bricks / Paths above lillypads and left of barrels
The recoloing of the rocks on the left
The bigger stump
Bottom right tiles
Outside stone head and columns
Green water
Ladders
Brown path
Sewer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Outside Objects:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm) CC-BY-3.0 / GPL 3.0 / GPL 2.0 / OGA-BY-3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Tree trunks (for evergreen and oak)
- Tree Tops (for evergreen and oak)
Daniel Eddeland
(https://opengameart.org/content/lpc-farming-tilesets-magic-animations-and-ui-elements)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Farming stuff including food / crops / sack of grains
- Logs / Anvils
- Fish / boats / pier
- Bazaar / Merchant displays
- Wooden Fences
Caeless
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Shadowless veggies (originally made by Daniel Eddeland)
William Thompsonj
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- fallen leaves
Casper Nilsson
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Metal Fence
- Wheelbarrows
- tent
- Gravestones
- harbor (stone platform in water)
- long boat
Barbara Rivera / C Phillips
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Leafless tree
Skorpio
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Trash / Barrel ( Top right corner)
MISSING:
Bricks bottom left
- Mushrooms need attributions
Bricks/tiles above pier
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Exterior:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm) CC-BY-3.0 / GPL 3.0 / GPL 2.0 / OGA-BY-3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- house (light red brick wall on far left and purple roof below it)
- house white door frame / brown door and white windows.
- signs under gold and drak brown brick house wall
Daniel Armstrong (AKA HughSpectrum)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- castle walls
- castle floors
- castle outside
- castle floors outside
Xenodora CC-BY-3.0 / GPL 3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- water well
Tuomo Untinen CC-BY-3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- stone bridge
- silver windows
Casper Nilsson
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Asain themed shrine including red lantern
- foodog statue
- Toro
- Cherry blossom tree
- Torii
- Grey Roof
- Flag poles
- Fancy white and black entrance
- white door
- green and white walls / roof / windows
- shrub plant in white square pot
- flower box
Steampun-a-fy: Amaris / Krusimira
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- dark purple brick near purple roof
- bronze and wood house (gold and dark brick)
- dark wooden stairs
- gold and dark chimney
- grey door
- gears
- pipes
- dark wooden windows
Leo Villeveygoux
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- white bricks (limestone wall)
Skyler Robert Collady
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Grey home assets
MISSING attributions:
Graves bottom right
Water filled boat
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Interior
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm) CC-BY-3.0 / GPL 3.0 / GPL 2.0 / OGA-BY-3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- bookshelves (to the left of light brown chairs)
- cabinents above bookshelves
- counters (to the right of the kitchen table (between the white bathroom tiles) and bottom left by the rug.
- blue wallpaper
- kitchen furnace, pots and sink.
- country inside (blue bed and light brown chairs)
- red royal bed with white pillows.
- Mahogany kitchen table (near the dungeon bed with a black blanket)
- Yellow curtains
- Flower pot with tall pink flower
- Empty flower pots
- Chairs with gold seats (between the fireplaces)
- Fireplaces
- White and red mahogany stairs
- Double Rounded doors
- Flower vases
- Purple/Blue Tiles near white brick wall
- White brick wall
- White Columns
- Black candle-holder stand with candles
- Royal rug beside cabinents
- White stairs with runway / platform
- Grandfather clock
- Blue wallpaper with woodem trim
- Wood tiles
- Long painting
- Royal chairs ( gold seats)
- Rounded white windows
- Portrait painting
- small end / round tables
- Royal bed with red pillows
- white china
By Sharm but Commissioned by William Thompsonj
- campfire
- skeletons
- dungeon beds
- wood chairs and tables between the calderon and the fancy door rounded door
- calderon
- cobwebs
- dungeon prison wall and door/gate (beside fancy red and gold rugs)
- dirt by the dungeon beds
- rat
Daniel Armstrong (Aka HughSpectrum)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- castle light Sources (torches)
- red carpets
- grey brick top of walls
Matthew Nash
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Public Toilets
- Bathroom tiles (white and black)
Tuomo Untinen
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Pots with cover (based from Sharm's pots)
- Yellow stone floor
- Short paintings
- royal chair modification
- cupboards based on Sharm's (with china)
- small footchair
- piano
MISSING attributions:
Some bottomleft tiles
Banners
Sideways table
Stacked barrels
Stacked chess
Things to left of the pots
Some of the furniture below the beds
Some of the single beds
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*Interior 2
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- brown stairs with blue
- Fountain
- Pool
- FLoor tiles
- Everything between top row (between toilet stalls and bookcases) down to the floor tiles
Xenodora
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Steel floor
Tuomo Untinen
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Armor and sheilds
- Cheese and bread
- Ship
Janna - CC0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Beds
- Dressers / book shelves
- Wardrobe
Casper Nilsson
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Red and Blue stairs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Extensions Folder:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
https://opengameart.org/content/lpc-adobe-building-set
https://opengameart.org/content/lpc-arabic-elements
- Adobe / Arabic - Commissioned by William Thompsonj

View File

@ -0,0 +1,12 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
Author :
Jacques-Olivier Farcy
https://interstices.ouvaton.org
https://twitter.com/JO_Interstices

View File

@ -0,0 +1,106 @@
## Flowers / Plants / Fungi / Wood
"[LPC] Flowers / Plants / Fungi / Wood," by bluecarrot16, Guido Bos, Ivan Voirol (Silver IV), SpiderDave, William.Thompsonj, Yar, Stephen Challener and the Open Surge team (http://opensnc.sourceforge.net), Gaurav Munjal, Johann Charlot, Casper Nilsson, Jetrel, Zabin, Hyptosis, Surt, Lanea Zimmerman, George Bailey, ansimuz, Buch, and the Open Pixel Project contributors (OpenPixelProject.com).
CC-BY-SA 3.0.
Based on:
[LPC] Guido Bos entries cut up
Guido Bos
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-guido-bos-entries-cut-up
Basic map 32x32 by Silver IV
Ivan Voirol (Silver IV)
CC-BY 3.0 / GPL 3.0 / GPL 2.0
https://opengameart.org/content/basic-map-32x32-by-silver-iv
Flowers
SpiderDave
CC0
https://opengameart.org/content/flowers
[LPC] Leaf Recolor
William.Thompsonj
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-leaf-recolor
Isometric 64x64 Outside Tileset
Yar
CC-BY 3.0
https://opengameart.org/content/isometric-64x64-outside-tileset
32x32 (and 16x16) RPG Tiles--Forest and some Interior Tiles
Stephen Challener and the Open Surge team (http://opensnc.sourceforge.net)commissioned by Gaurav Munjal
CC-BY 3.0
https://opengameart.org/content/32x32-and-16x16-rpg-tiles-forest-and-some-interior-tiles
Lots of Hyptosis' tiles organized!
Hyptosis
CC-BY 3.0
https://opengameart.org/content/lots-of-hyptosis-tiles-organized
Generic Platformer Tiles
surt
CC0
http://opengameart.org/content/generic-platformer-tiles
old frogatto tile art
Guido Bos
CC0
https://opengameart.org/content/old-frogatto-tile-art
LPC: Interior Castle Tiles
Lanea Zimmerman
CC-BY-3.0 / GPL 3.0
http://opengameart.org/content/lpc-interior-castle-tiles
RPG item set
Jetrel
CC0
https://opengameart.org/content/rpg-item-set
Shoot'em up graphic kit
Johann Charlot
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/shootem-up-graphic-kit
LPC C.Nilsson
Casper Nilsson
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-cnilsson
Lots of trees and plants from OGA (DB32) tilesets pack 1
Jetrel, Zabin, Hyptosis, Surt
CC0
https://opengameart.org/content/lots-of-trees-and-plants-from-oga-db32-tilesets-pack-1
Trees & Bushes
ansimuz
CC0
https://opengameart.org/content/trees-bushes
Outdoor tiles, again
Buch <https://opengameart.org/users/buch>
CC-BY 2.0
https://opengameart.org/content/outdoor-tiles-again
16x16 Game Assets
George Bailey
CC-BY 4.0
https://opengameart.org/content/16x16-game-assets
Tuxemon tileset
Buch
CC-BY-SA 3.0
https://opengameart.org/content/tuxemon-tileset
Orthographic outdoor tiles
Buch
CC0
https://opengameart.org/content/orthographic-outdoor-tiles
OPP2017 - Jungle and temple set
OpenPixelProject.com
CC0
https://opengameart.org/content/opp2017-jungle-and-temple-set

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

View File

@ -0,0 +1,101 @@
## Medieval
[LPC] Hanging signs
Reemax
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-hanging-signs
Liberated Pixel Cup (LPC) Base Assets
Lanea Zimmerman (Sharm)
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles
[LPC] City outside
Reemax (Tuomo Untinen), Xenodora, Sharm, Johann C, Johannes Sjölund
CC-BY-SA 3.0 / GPL 3.0 / GPL 2.0
https://opengameart.org/content/lpc-city-outside
[LPC] Cavern and ruin tiles
CC-BY-SA 3.0 / GPL 3.0 / GPL 2.0
Reemax, Sharm, Hyptosis, Johann C, HughSpectrum, Redshrike, William.Thompsonj, wulax,
https://opengameart.org/node/33913
Statues & Fountains Collection
Casper Nilsson, Daniel Cook, Rayane Félix (RayaneFLX), Wolthera van Hövell tot Westerflier (TheraHedwig), Hyptosis, mold, Zachariah Husiar (Zabin), & Clint Bellanger
CC-BY-SA 3.0
https://opengameart.org/content/statues-fountains-collection
LPC C.Nilsson
Casper Nilsson
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-cnilsson
LPC Style Well
CC-BY 3.0 / GPL 3.0
Xenodora, Sharm
https://opengameart.org/content/lpc-style-well
RPG item set
Jetrel
CC0
https://opengameart.org/content/rpg-item-set
[LPC] Guido Bos entries cut up
Guido Bos
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-guido-bos-entries-cut-up
LPC Sign Post
Nemisys
CC-BY 3.0 / CC-BY-SA 3.0 / GPL 3.0 / OGA-BY 3.0
https://opengameart.org/content/lpc-sign-post
[LPC] Signposts, graves, line cloths and scare crow
Reemax
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-signposts-graves-line-cloths-and-scare-crow
[LPC] Hanging signs
Reemax
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-hanging-signs
Hyptosis
Mage City Arcanos
CC0
https://opengameart.org/content/mage-city-arcanos
[LPC] Street Lamp
Curt
CC-BY 3.0
https://opengameart.org/content/lpc-street-lamp
[LPC] Misc
Lanea Zimmerman (Sharm), William.Thompsonj
CC-BY 3.0 / GPL 3.0 / GPL 2.0 / OGA-BY 3.0
https://opengameart.org/content/lpc-misc
RPG Tiles: Cobble stone paths & town objects
https://opengameart.org/content/rpg-tiles-cobble-stone-paths-town-objects
Zabin, Daneeklu, Jetrel, Hyptosis, Redshrike, Bertram.
CC-BY-SA 3.0
[LPC] Farming tilesets, magic animations and UI elements
https://opengameart.org/content/lpc-farming-tilesets-magic-animations-and-ui-elements
Daniel Eddeland (daneeklu)
CC-BY-SA 3.0 / GPL 3.0
RPG item set
Jetrel
CC0
https://opengameart.org/content/rpg-item-set
RPG Indoor Tileset: Expansion 1
Redshrike
CC-BY 3.0 / GPL 3.0 / GPL 2.0 / OGA-BY 3.0
https://opengameart.org/content/rpg-indoor-tileset-expansion-1
[LPC] Dungeon Elements
Lanea Zimmerman (Sharm), William.Thompsonj
CC-BY 3.0 / GPL 3.0 / GPL 2.0 / OGA-BY 3.0
https://opengameart.org/content/lpc-dungeon-elements

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

706
maps/Tuto/tutoV3.json Normal file

File diff suppressed because one or more lines are too long

99
maps/tests/goToPage.json Normal file
View File

@ -0,0 +1,99 @@
{ "compressionlevel":-1,
"height":20,
"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, 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, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":20,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":20,
"id":4,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 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, 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, 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, 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":20,
"id":3,
"name":"popupZone",
"opacity":1,
"properties":[
{
"name":"zone",
"type":"string",
"value":"popUpGoToPageZone"
}],
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":5,
"name":"floorLayer",
"objects":[
{
"height":59,
"id":1,
"name":"popUp",
"rotation":0,
"type":"",
"visible":true,
"width":152,
"x":247,
"y":11
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":6,
"nextobjectid":2,
"orientation":"orthogonal",
"properties":[
{
"name":"script",
"type":"string",
"value":"goToPageScript.js"
}],
"renderorder":"right-down",
"tiledversion":"1.5.0",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":20
}

View File

@ -0,0 +1,49 @@
var zoneName = "popUpGoToPageZone";
var urlPricing = "https://workadventu.re/pricing";
var urlGettingStarted = "https://workadventu.re/getting-started";
var isCoWebSiteOpened = false;
WA.onChatMessage((message => {
WA.sendChatMessage('Poly Parrot says: "'+message+'"', 'Poly Parrot');
}));
WA.onEnterZone(zoneName, () => {
WA.openPopup("popUp","Open Links",[
{
label: "Open Tab",
className: "popUpElement",
callback: (popup => {
WA.openTab(urlPricing);
popup.close();
})
},
{
label: "Go To Page", className : "popUpElement",
callback:(popup => {
WA.goToPage(urlPricing);
popup.close();
})
}
,
{
label: "openCoWebSite", className : "popUpElement",
callback:(popup => {
WA.openCoWebSite(urlPricing);
isCoWebSiteOpened = true;
popup.close();
})
}]);
})
WA.onLeaveZone(zoneName, () => {
if (isCoWebSiteOpened) {
WA.closeCoWebSite();
isCoWebSiteOpened = false;
}
})
WA.onLeaveZone('popupZone', () => {
})

25
maps/tests/iframe.html Normal file
View File

@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<script src="http://play.workadventure.localhost/iframe_api.js"></script>
<script>
</script>
</head>
<body>
<button id="sendchat">Send chat message</button>
<script>
document.getElementById('sendchat').onclick = () => {
WA.sendChatMessage('Hello world!', 'Mr Robot');
}
</script>
<div id="chatSent"></div>
<script>
WA.onChatMessage((message => {
const chatDiv = document.createElement('p');
chatDiv.innerText = message;
document.getElementById('chatSent').append(chatDiv);
}));
</script>
</body>
</html>

View File

@ -0,0 +1,94 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":5,
"name":"iframe_api",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"iframe.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":6,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.3.3",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.2,
"width":10
}

79
maps/tests/script.js Normal file
View File

@ -0,0 +1,79 @@
console.log('SCRIPT LAUNCHED');
//WA.sendChatMessage('Hi, my name is Poly and I repeat what you say!', 'Poly Parrot');
var isFirstTimeTuto = false;
var textFirstPopup = 'Hey ! This is how to open start a discussion with someone ! You can be 4 max in a booble';
var textSecondPopup = 'You can also use the chat to communicate ! ';
var targetObjectTutoBubble ='tutoBobble';
var targetObjectTutoChat ='tutoChat';
var popUpExplanation = undefined;
function launchTuto (){
WA.openPopup(targetObjectTutoBubble, textFirstPopup, [
{
label: "Next",
className: "popUpElement",
callback: (popup) => {
popup.close();
WA.openPopup(targetObjectTutoChat, textSecondPopup, [
{
label: "Open Chat",
className: "popUpElement",
callback: (popup1) => {
WA.sendChatMessage("Hey you can talk here too ! ", 'WA Guide');
popup1.close();
WA.restorePlayerControl();
}
}
])
}
}
]);
WA.disablePlayerControl();
}
WA.onChatMessage((message => {
console.log('CHAT MESSAGE RECEIVED BY SCRIPT');
WA.sendChatMessage('Poly Parrot says: "'+message+'"', 'Poly Parrot');
}));
WA.onEnterZone('myTrigger', () => {
WA.sendChatMessage("Don't step on my carpet!", 'Poly Parrot');
})
WA.onLeaveZone('popupZone', () => {
})
WA.onEnterZone('notExist', () => {
WA.sendChatMessage("YOU SHOULD NEVER SEE THIS", 'Poly Parrot');
})
WA.onEnterZone('popupZone', () => {
WA.displayBubble();
if (!isFirstTimeTuto) {
isFirstTimeTuto = true;
launchTuto();
}
else popUpExplanation = WA.openPopup(targetObjectTutoChat,'Do you want to review the explanation ? ', [
{
label: "No",
className: "popUpElementReviewexplanation",
callback: (popup) => {
popup.close();
}
},
{
label: "Yes",
className: "popUpElementReviewexplanation",
callback: (popup) => {
popup.close();
launchTuto();
}
}
])
});
WA.onLeaveZone('popupZone', () => {
if (popUpExplanation !== undefined) popUpExplanation.close();
WA.removeBubble();
})

135
maps/tests/script_api.json Normal file
View File

@ -0,0 +1,135 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 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":6,
"name":"triggerZone",
"opacity":1,
"properties":[
{
"name":"zone",
"type":"string",
"value":"myTrigger"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 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":7,
"name":"popupZone",
"opacity":1,
"properties":[
{
"name":"zone",
"type":"string",
"value":"popupZone"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":147.135497146101,
"id":1,
"name":"myPopup2",
"rotation":0,
"type":"",
"visible":true,
"width":104.442827410047,
"x":142.817125079855,
"y":147.448134926559
},
{
"height":132.434722966794,
"id":2,
"name":"myPopup1",
"rotation":0,
"type":"",
"visible":true,
"width":125.735549178518,
"x":13.649632619596,
"y":50.8502491249093
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":3,
"orientation":"orthogonal",
"properties":[
{
"name":"script",
"type":"string",
"value":"script.js"
}],
"renderorder":"right-down",
"tiledversion":"1.4.3",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.4,
"width":10
}

View File

@ -202,6 +202,13 @@ message WorldFullWarningMessage{
message WorldFullWarningToRoomMessage{ message WorldFullWarningToRoomMessage{
string roomId = 1; string roomId = 1;
} }
message RefreshRoomPromptMessage{
string roomId = 1;
}
message RefreshRoomMessage{
string roomId = 1;
int32 versionNumber = 2;
}
message WorldFullMessage{ message WorldFullMessage{
} }
@ -229,6 +236,7 @@ message ServerToClientMessage {
AdminRoomMessage adminRoomMessage = 14; AdminRoomMessage adminRoomMessage = 14;
WorldFullWarningMessage worldFullWarningMessage = 15; WorldFullWarningMessage worldFullWarningMessage = 15;
WorldFullMessage worldFullMessage = 16; WorldFullMessage worldFullMessage = 16;
RefreshRoomMessage refreshRoomMessage = 17;
} }
} }
@ -395,4 +403,5 @@ service RoomManager {
rpc ban(BanMessage) returns (EmptyMessage); rpc ban(BanMessage) returns (EmptyMessage);
rpc sendAdminMessageToRoom(AdminRoomMessage) returns (EmptyMessage); rpc sendAdminMessageToRoom(AdminRoomMessage) returns (EmptyMessage);
rpc sendWorldFullWarningToRoom(WorldFullWarningToRoomMessage) returns (EmptyMessage); rpc sendWorldFullWarningToRoom(WorldFullWarningToRoomMessage) returns (EmptyMessage);
rpc sendRefreshRoomPrompt(RefreshRoomPromptMessage) returns (EmptyMessage);
} }

View File

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

View File

@ -2,7 +2,7 @@ import {BaseController} from "./BaseController";
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
import {apiClientRepository} from "../Services/ApiClientRepository"; import {apiClientRepository} from "../Services/ApiClientRepository";
import {AdminRoomMessage, WorldFullWarningToRoomMessage} from "../Messages/generated/messages_pb"; import {AdminRoomMessage, WorldFullWarningToRoomMessage, RefreshRoomPromptMessage} from "../Messages/generated/messages_pb";
export class AdminController extends BaseController{ export class AdminController extends BaseController{
@ -11,6 +11,56 @@ export class AdminController extends BaseController{
super(); super();
this.App = App; this.App = App;
this.receiveGlobalMessagePrompt(); this.receiveGlobalMessagePrompt();
this.receiveRoomEditionPrompt();
}
receiveRoomEditionPrompt() {
this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
res.end();
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn('/message request was aborted');
})
const token = req.getHeader('admin-token');
const body = await res.json();
if (token !== ADMIN_API_TOKEN) {
console.error('Admin access refused for token: '+token)
res.writeStatus("401 Unauthorized").end('Incorrect token');
return;
}
try {
if (typeof body.roomId !== 'string') {
throw 'Incorrect roomId parameter'
}
const roomId: string = body.roomId;
await apiClientRepository.getClient(roomId).then((roomClient) =>{
return new Promise((res, rej) => {
const roomMessage = new RefreshRoomPromptMessage();
roomMessage.setRoomid(roomId);
roomClient.sendRefreshRoomPrompt(roomMessage, (err) => {
err ? rej(err) : res();
});
});
});
} catch (err) {
this.errorToResponse(err, res);
return;
}
res.writeStatus("200");
res.end('ok');
});
} }
receiveGlobalMessagePrompt() { receiveGlobalMessagePrompt() {

View File

@ -13,8 +13,20 @@ export class BaseController {
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
protected errorToResponse(e: any, res: HttpResponse): void { protected errorToResponse(e: any, res: HttpResponse): void {
console.error(e.message || "An error happened.", e?.config.url); if (e && e.message) {
console.error(e.stack || 'no stack defined.'); let url = e?.config?.url;
if (url !== undefined) {
url = ' for URL: '+url;
} else {
url = '';
}
console.error('ERROR: '+e.message+url);
} else if (typeof(e) === 'string') {
console.error(e);
}
if (e.stack) {
console.error(e.stack);
}
if (e.response) { if (e.response) {
res.writeStatus(e.response.status+" "+e.response.statusText); res.writeStatus(e.response.status+" "+e.response.statusText);
this.addCorsHeaders(res); this.addCorsHeaders(res);

View File

@ -190,10 +190,10 @@ export class IoSocketController {
memberMessages = userData.messages; memberMessages = userData.messages;
memberTags = userData.tags; memberTags = userData.tags;
memberTextures = userData.textures; memberTextures = userData.textures;
if (!room.anonymous && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && (userData.anonymous === true || !room.canAccess(memberTags))) { if (!room.public && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && (userData.anonymous === true || !room.canAccess(memberTags))) {
throw new Error('No correct tags') throw new Error('No correct tags')
} }
if (!room.anonymous && room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && userData.anonymous === true) { if (!room.public && room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && userData.anonymous === true) {
throw new Error('No correct member') throw new Error('No correct member')
} }
} catch (e) { } catch (e) {

View File

@ -13,21 +13,22 @@ export enum GameRoomPolicyTypes {
export class PusherRoom { export class PusherRoom {
private readonly positionNotifier: PositionDispatcher; private readonly positionNotifier: PositionDispatcher;
public readonly anonymous: boolean; public readonly public: boolean;
public tags: string[]; public tags: string[];
public policyType: GameRoomPolicyTypes; public policyType: GameRoomPolicyTypes;
public readonly roomSlug: string; public readonly roomSlug: string;
public readonly worldSlug: string = ''; public readonly worldSlug: string = '';
public readonly organizationSlug: string = ''; public readonly organizationSlug: string = '';
private versionNumber: number = 1;
constructor(public readonly roomId: string, constructor(public readonly roomId: string,
private socketListener: ZoneEventListener) private socketListener: ZoneEventListener)
{ {
this.anonymous = isRoomAnonymous(roomId); this.public = isRoomAnonymous(roomId);
this.tags = []; this.tags = [];
this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY; this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY;
if (this.anonymous) { if (this.public) {
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
} else { } else {
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId);
@ -55,4 +56,13 @@ export class PusherRoom {
public isEmpty(): boolean { public isEmpty(): boolean {
return this.positionNotifier.isEmpty(); return this.positionNotifier.isEmpty();
} }
public needsUpdate(versionNumber: number): boolean {
if (this.versionNumber < versionNumber) {
this.versionNumber = versionNumber;
return true;
} else {
return false;
}
}
} }

View File

@ -1,5 +1,6 @@
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
import Axios from "axios"; import Axios from "axios";
import {GameRoomPolicyTypes} from "_Model/PusherRoom";
export interface AdminApiData { export interface AdminApiData {
organizationSlug: string organizationSlug: string
@ -13,6 +14,13 @@ export interface AdminApiData {
textures: CharacterTexture[] textures: CharacterTexture[]
} }
export interface MapDetailsData {
roomSlug: string,
mapUrl: string,
policy_type: GameRoomPolicyTypes,
tags: string[],
}
export interface AdminBannedData { export interface AdminBannedData {
is_banned: boolean, is_banned: boolean,
message: string message: string
@ -35,9 +43,9 @@ export interface FetchMemberDataByUuidResponse {
class AdminApi { class AdminApi {
async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise<AdminApiData> { async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise<MapDetailsData> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!'); return Promise.reject(new Error('No admin backoffice set!'));
} }
const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = {
@ -60,7 +68,7 @@ class AdminApi {
async fetchMemberDataByUuid(uuid: string, roomId: string): Promise<FetchMemberDataByUuidResponse> { async fetchMemberDataByUuid(uuid: string, roomId: string): Promise<FetchMemberDataByUuidResponse> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject('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}, headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } { params: {uuid, roomId}, headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
@ -70,7 +78,7 @@ class AdminApi {
async fetchMemberDataByToken(organizationMemberToken: string): Promise<AdminApiData> { async fetchMemberDataByToken(organizationMemberToken: string): Promise<AdminApiData> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!'); return Promise.reject(new Error('No admin backoffice set!'));
} }
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken,
@ -81,7 +89,7 @@ class AdminApi {
async fetchCheckUserByToken(organizationMemberToken: string): Promise<AdminApiData> { async fetchCheckUserByToken(organizationMemberToken: string): Promise<AdminApiData> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!'); return Promise.reject(new Error('No admin backoffice set!'));
} }
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken, const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken,
@ -104,7 +112,7 @@ class AdminApi {
async verifyBanUser(organizationMemberToken: string, ipAddress: string, organization: string, world: string): Promise<AdminBannedData> { async verifyBanUser(organizationMemberToken: string, ipAddress: string, organization: string, world: string): Promise<AdminBannedData> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!'); return Promise.reject(new Error('No admin backoffice set!'));
} }
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
return Axios.get(ADMIN_API_URL + '/api/check-moderate-user/'+organization+'/'+world+'?ipAddress='+ipAddress+'&token='+organizationMemberToken, return Axios.get(ADMIN_API_URL + '/api/check-moderate-user/'+organization+'/'+world+'?ipAddress='+ipAddress+'&token='+organizationMemberToken,

View File

@ -22,7 +22,7 @@ import {
WorldFullMessage, WorldFullMessage,
AdminPusherToBackMessage, AdminPusherToBackMessage,
ServerToAdminClientMessage, ServerToAdminClientMessage,
UserJoinedRoomMessage, UserLeftRoomMessage, AdminMessage, BanMessage UserJoinedRoomMessage, UserLeftRoomMessage, AdminMessage, BanMessage, RefreshRoomMessage
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable";
@ -54,7 +54,7 @@ export interface AdminSocketData {
export class SocketManager implements ZoneEventListener { export class SocketManager implements ZoneEventListener {
private Worlds: Map<string, PusherRoom> = new Map<string, PusherRoom>(); private rooms: Map<string, PusherRoom> = new Map<string, PusherRoom>();
private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>(); private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>();
constructor() { constructor() {
@ -181,6 +181,11 @@ export class SocketManager implements ZoneEventListener {
this.handleViewport(client, viewport); this.handleViewport(client, viewport);
} }
if (message.hasRefreshroommessage()) {
const refreshMessage:RefreshRoomMessage = message.getRefreshroommessage() as unknown as RefreshRoomMessage;
this.refreshRoomData(refreshMessage.getRoomid(), refreshMessage.getVersionnumber())
}
// Let's pass data over from the back to the client. // Let's pass data over from the back to the client.
if (!client.disconnecting) { if (!client.disconnecting) {
client.send(message.serializeBinary().buffer, true); client.send(message.serializeBinary().buffer, true);
@ -219,7 +224,7 @@ export class SocketManager implements ZoneEventListener {
try { try {
client.viewport = viewport; client.viewport = viewport;
const world = this.Worlds.get(client.roomId); const world = this.rooms.get(client.roomId);
if (!world) { if (!world) {
console.error("In SET_VIEWPORT, could not find world with id '", client.roomId, "'"); console.error("In SET_VIEWPORT, could not find world with id '", client.roomId, "'");
return; return;
@ -310,12 +315,12 @@ export class SocketManager implements ZoneEventListener {
if (socket.roomId) { if (socket.roomId) {
try { try {
//user leaves room //user leaves room
const room: PusherRoom | undefined = this.Worlds.get(socket.roomId); const room: PusherRoom | undefined = this.rooms.get(socket.roomId);
if (room) { if (room) {
debug('Leaving room %s.', socket.roomId); debug('Leaving room %s.', socket.roomId);
room.leave(socket); room.leave(socket);
if (room.isEmpty()) { if (room.isEmpty()) {
this.Worlds.delete(socket.roomId); this.rooms.delete(socket.roomId);
debug('Room %s is empty. Deleting.', socket.roomId); debug('Room %s is empty. Deleting.', socket.roomId);
} }
} else { } else {
@ -339,19 +344,23 @@ export class SocketManager implements ZoneEventListener {
async getOrCreateRoom(roomId: string): Promise<PusherRoom> { async getOrCreateRoom(roomId: string): Promise<PusherRoom> {
//check and create new world for a room //check and create new world for a room
let world = this.Worlds.get(roomId) let world = this.rooms.get(roomId)
if(world === undefined){ if(world === undefined){
world = new PusherRoom(roomId, this); world = new PusherRoom(roomId, this);
if (!world.anonymous) { if (!world.public) {
const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) await this.updateRoomWithAdminData(world);
world.tags = data.tags
world.policyType = Number(data.policy_type)
} }
this.Worlds.set(roomId, world); this.rooms.set(roomId, world);
} }
return Promise.resolve(world) return Promise.resolve(world)
} }
public async updateRoomWithAdminData(world: PusherRoom): Promise<void> {
const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug)
world.tags = data.tags;
world.policyType = Number(data.policy_type);
}
emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) { emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) {
const pusherToBackMessage = new PusherToBackMessage(); const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setPlayglobalmessage(playglobalmessage); pusherToBackMessage.setPlayglobalmessage(playglobalmessage);
@ -360,7 +369,7 @@ export class SocketManager implements ZoneEventListener {
} }
public getWorlds(): Map<string, PusherRoom> { public getWorlds(): Map<string, PusherRoom> {
return this.Worlds; return this.rooms;
} }
searchClientByUuid(uuid: string): ExSocketInterface | null { searchClientByUuid(uuid: string): ExSocketInterface | null {
@ -544,6 +553,14 @@ export class SocketManager implements ZoneEventListener {
client.send(serverToClientMessage.serializeBinary().buffer, true); client.send(serverToClientMessage.serializeBinary().buffer, true);
} }
private refreshRoomData(roomId: string, versionNumber: number): void {
const room = this.rooms.get(roomId);
//this function is run for every users connected to the room, so we need to make sure the room wasn't already refreshed.
if (!room || !room.needsUpdate(versionNumber)) return;
this.updateRoomWithAdminData(room);
}
} }
export const socketManager = new SocketManager(); export const socketManager = new SocketManager();

View File

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