Merge branch 'develop' of github.com:thecodingmachine/workadventure into MenuSvelte
This commit is contained in:
commit
fe585297fc
@ -19,3 +19,6 @@ ACME_EMAIL=
|
||||
MAX_PER_GROUP=4
|
||||
MAX_USERNAME_LENGTH=8
|
||||
|
||||
OPID_CLIENT_ID=
|
||||
OPID_CLIENT_SECRET=
|
||||
OPID_CLIENT_ISSUER=
|
||||
|
3
.github/workflows/continuous_integration.yml
vendored
3
.github/workflows/continuous_integration.yml
vendored
@ -50,6 +50,7 @@ jobs:
|
||||
run: yarn run build
|
||||
env:
|
||||
PUSHER_URL: "//localhost:8080"
|
||||
ADMIN_URL: "//localhost:80"
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Svelte check"
|
||||
@ -81,7 +82,7 @@ jobs:
|
||||
- name: "Setup NodeJS"
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
node-version: '14.x'
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
|
1
.github/workflows/push-to-npm.yml
vendored
1
.github/workflows/push-to-npm.yml
vendored
@ -47,6 +47,7 @@ jobs:
|
||||
run: yarn run build-typings
|
||||
env:
|
||||
PUSHER_URL: "//localhost:8080"
|
||||
ADMIN_URL: "//localhost:80"
|
||||
working-directory: "front"
|
||||
|
||||
# We build the front to generate the typings of iframe_api, then we copy those typings in a separate package.
|
||||
|
34
CHANGELOG.md
34
CHANGELOG.md
@ -1,4 +1,13 @@
|
||||
## Version 1.4.x-dev
|
||||
## Version develop
|
||||
|
||||
### Updates
|
||||
- New scripting API features :
|
||||
- Use `WA.room.loadTileset(url: string) : Promise<number>` to load a tileset from a JSON file.
|
||||
- Rewrote the way authentification works: the auth jwt token can now contains an email instead of an uuid
|
||||
- Added an OpenId login flow than can be plugged to any OIDC provider.
|
||||
- You can send a message to all rooms of your world from the console global message (user with tag admin only).
|
||||
|
||||
## Version 1.4.11
|
||||
|
||||
### Updates
|
||||
|
||||
@ -8,20 +17,35 @@
|
||||
- Migrated the admin console to Svelte, and redesigned the console #1211
|
||||
- Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1)
|
||||
- New scripting API features :
|
||||
- Use `WA.onInit(): Promise<void>` to wait for scripting API initialization
|
||||
- Use `WA.room.showLayer(): void` to show a layer
|
||||
- Use `WA.room.hideLayer(): void` to hide a layer
|
||||
- Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer
|
||||
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
|
||||
- Use `WA.room.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player
|
||||
- Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started
|
||||
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
||||
- Use `WA.player.id: string|undefined` to get the ID of the current player
|
||||
- Use `WA.player.name: string` to get the name of the current player
|
||||
- Use `WA.player.tags: string[]` to get the tags of the current player
|
||||
- Use `WA.room.id: string` to get the ID of the room
|
||||
- Use `WA.room.mapURL: string` to get the URL of the map
|
||||
- Use `WA.room.mapURL: string` to get the URL of the map
|
||||
- Use `WA.room.getMap(): Promise<ITiledMap>` to get the JSON map file
|
||||
- Use `WA.room.setTiles(): void` to add, delete or change an array of tiles
|
||||
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
||||
- Use `WA.state.loadVariable(key: string): unknown` to retrieve a variable
|
||||
- Use `WA.state.saveVariable(key: string, value: unknown): Promise<void>` to set a variable (across the room, for all users)
|
||||
- Use `WA.state.onVariableChange(key: string): Observable<unknown>` to track a variable
|
||||
- Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`)
|
||||
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
|
||||
- The text chat was redesigned to be prettier and to use more features :
|
||||
- The chat is now persistent bewteen discussions and always accesible
|
||||
- The chat is now persistent between discussions and always accessible
|
||||
- The chat now tracks incoming and outcoming users in your conversation
|
||||
- The chat allows your to see the visit card of users
|
||||
- You can close the chat window with the escape key
|
||||
- Added a 'Enable notifications' button in the menu.
|
||||
- The exchange format between Pusher and Admin servers has changed. If you have your own implementation of an admin server, these endpoints signatures have changed:
|
||||
- `/api/map`: now accepts a complete room URL instead of organization/world/room slugs
|
||||
- `/api/ban`: new endpoint to report users
|
||||
- as a side effect, the "routing" is now completely stored on the admin side, so by implementing your own admin server, you can develop completely custom routing
|
||||
|
||||
## Version 1.4.3 - 1.4.4 - 1.4.5
|
||||
|
||||
|
@ -40,6 +40,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||
"dependencies": {
|
||||
"@workadventure/tiled-map-type-guard": "^1.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"busboy": "^0.3.1",
|
||||
"circular-json": "^0.5.9",
|
||||
@ -47,10 +48,12 @@
|
||||
"generic-type-guard": "^3.2.0",
|
||||
"google-protobuf": "^3.13.0",
|
||||
"grpc": "^1.24.4",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"prom-client": "^12.0.0",
|
||||
"query-string": "^6.13.3",
|
||||
"redis": "^3.1.2",
|
||||
"systeminformation": "^4.31.1",
|
||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||
"uuidv4": "^6.0.7"
|
||||
@ -64,6 +67,7 @@
|
||||
"@types/jasmine": "^3.5.10",
|
||||
"@types/jsonwebtoken": "^8.3.8",
|
||||
"@types/mkdirp": "^1.0.1",
|
||||
"@types/redis": "^2.8.31",
|
||||
"@types/uuidv4": "^5.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||
"@typescript-eslint/parser": "^2.26.0",
|
||||
|
@ -12,6 +12,9 @@ const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051;
|
||||
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
|
||||
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
||||
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
|
||||
export const REDIS_HOST = process.env.REDIS_HOST || undefined;
|
||||
export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379;
|
||||
export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;
|
||||
|
||||
export {
|
||||
MINIMUM_DISTANCE,
|
||||
|
@ -5,41 +5,64 @@ import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
||||
import { PositionNotifier } from "./PositionNotifier";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
|
||||
import { arrayIntersect } from "../Services/ArrayHelper";
|
||||
import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb";
|
||||
import {
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
EmoteEventMessage,
|
||||
ErrorMessage,
|
||||
JoinRoomMessage,
|
||||
SubToPusherRoomMessage,
|
||||
VariableMessage,
|
||||
VariableWithTagMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
import { ZoneSocket } from "src/RoomManager";
|
||||
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
||||
import { Admin } from "../Model/Admin";
|
||||
import { adminApi } from "../Services/AdminApi";
|
||||
import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
|
||||
import { ITiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||
import { mapFetcher } from "../Services/MapFetcher";
|
||||
import { VariablesManager } from "../Services/VariablesManager";
|
||||
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import { LocalUrlError } from "../Services/LocalUrlError";
|
||||
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
|
||||
|
||||
export type ConnectCallback = (user: User, group: Group) => void;
|
||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||
|
||||
export class GameRoom {
|
||||
private readonly minDistance: number;
|
||||
private readonly groupRadius: number;
|
||||
|
||||
// Users, sorted by ID
|
||||
private readonly users: Map<number, User>;
|
||||
private readonly usersByUuid: Map<string, User>;
|
||||
private readonly groups: Set<Group>;
|
||||
private readonly admins: Set<Admin>;
|
||||
private readonly users = new Map<number, User>();
|
||||
private readonly usersByUuid = new Map<string, User>();
|
||||
private readonly groups = new Set<Group>();
|
||||
private readonly admins = new Set<Admin>();
|
||||
|
||||
private readonly connectCallback: ConnectCallback;
|
||||
private readonly disconnectCallback: DisconnectCallback;
|
||||
|
||||
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
||||
private itemsState = new Map<number, unknown>();
|
||||
|
||||
private readonly positionNotifier: PositionNotifier;
|
||||
public readonly roomId: string;
|
||||
public readonly roomSlug: string;
|
||||
public readonly worldSlug: string = "";
|
||||
public readonly organizationSlug: string = "";
|
||||
private versionNumber: number = 1;
|
||||
private nextUserId: number = 1;
|
||||
|
||||
constructor(
|
||||
roomId: string,
|
||||
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
||||
|
||||
private constructor(
|
||||
public readonly roomUrl: string,
|
||||
private mapUrl: string,
|
||||
private readonly connectCallback: ConnectCallback,
|
||||
private readonly disconnectCallback: DisconnectCallback,
|
||||
private readonly minDistance: number,
|
||||
private readonly groupRadius: number,
|
||||
onEnters: EntersCallback,
|
||||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback
|
||||
) {
|
||||
// A zone is 10 sprites wide.
|
||||
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
|
||||
}
|
||||
|
||||
public static async create(
|
||||
roomUrl: string,
|
||||
connectCallback: ConnectCallback,
|
||||
disconnectCallback: DisconnectCallback,
|
||||
minDistance: number,
|
||||
@ -48,28 +71,23 @@ export class GameRoom {
|
||||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback
|
||||
) {
|
||||
this.roomId = roomId;
|
||||
): Promise<GameRoom> {
|
||||
const mapDetails = await GameRoom.getMapDetails(roomUrl);
|
||||
|
||||
if (isRoomAnonymous(roomId)) {
|
||||
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
||||
} else {
|
||||
const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
|
||||
this.roomSlug = roomSlug;
|
||||
this.organizationSlug = organizationSlug;
|
||||
this.worldSlug = worldSlug;
|
||||
}
|
||||
const gameRoom = new GameRoom(
|
||||
roomUrl,
|
||||
mapDetails.mapUrl,
|
||||
connectCallback,
|
||||
disconnectCallback,
|
||||
minDistance,
|
||||
groupRadius,
|
||||
onEnters,
|
||||
onMoves,
|
||||
onLeaves,
|
||||
onEmote
|
||||
);
|
||||
|
||||
this.users = new Map<number, User>();
|
||||
this.usersByUuid = new Map<string, User>();
|
||||
this.admins = new Set<Admin>();
|
||||
this.groups = new Set<Group>();
|
||||
this.connectCallback = connectCallback;
|
||||
this.disconnectCallback = disconnectCallback;
|
||||
this.minDistance = minDistance;
|
||||
this.groupRadius = groupRadius;
|
||||
// A zone is 10 sprites wide.
|
||||
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
|
||||
return gameRoom;
|
||||
}
|
||||
|
||||
public getGroups(): Group[] {
|
||||
@ -86,6 +104,15 @@ export class GameRoom {
|
||||
public getUserById(id: number): User | undefined {
|
||||
return this.users.get(id);
|
||||
}
|
||||
public getUsersByUuid(uuid: string): User[] {
|
||||
const userList: User[] = [];
|
||||
for (const user of this.users.values()) {
|
||||
if (user.uuid === uuid) {
|
||||
userList.push(user);
|
||||
}
|
||||
}
|
||||
return userList;
|
||||
}
|
||||
|
||||
public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User {
|
||||
const positionMessage = joinRoomMessage.getPositionmessage();
|
||||
@ -177,7 +204,7 @@ export class GameRoom {
|
||||
} else {
|
||||
const closestUser: User = closestItem;
|
||||
const group: Group = new Group(
|
||||
this.roomId,
|
||||
this.roomUrl,
|
||||
[user, closestUser],
|
||||
this.connectCallback,
|
||||
this.disconnectCallback,
|
||||
@ -303,6 +330,37 @@ export class GameRoom {
|
||||
return this.itemsState;
|
||||
}
|
||||
|
||||
public async setVariable(name: string, value: string, user: User): Promise<void> {
|
||||
// First, let's check if "user" is allowed to modify the variable.
|
||||
const variableManager = await this.getVariableManager();
|
||||
|
||||
const readableBy = variableManager.setVariable(name, value, user);
|
||||
|
||||
// If the variable was not changed, let's not dispatch anything.
|
||||
if (readableBy === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: should we batch those every 100ms?
|
||||
const variableMessage = new VariableWithTagMessage();
|
||||
variableMessage.setName(name);
|
||||
variableMessage.setValue(value);
|
||||
if (readableBy) {
|
||||
variableMessage.setReadableby(readableBy);
|
||||
}
|
||||
|
||||
const subMessage = new SubToPusherRoomMessage();
|
||||
subMessage.setVariablemessage(variableMessage);
|
||||
|
||||
const batchMessage = new BatchToPusherRoomMessage();
|
||||
batchMessage.addPayload(subMessage);
|
||||
|
||||
// Dispatch the message on the room listeners
|
||||
for (const socket of this.roomListeners) {
|
||||
socket.write(batchMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
||||
return this.positionNotifier.addZoneListener(call, x, y);
|
||||
}
|
||||
@ -332,4 +390,98 @@ export class GameRoom {
|
||||
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
|
||||
this.positionNotifier.emitEmoteEvent(user, emoteEventMessage);
|
||||
}
|
||||
|
||||
public addRoomListener(socket: RoomSocket) {
|
||||
this.roomListeners.add(socket);
|
||||
}
|
||||
|
||||
public removeRoomListener(socket: RoomSocket) {
|
||||
this.roomListeners.delete(socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the admin server to fetch map details.
|
||||
* If there is no admin server, the map details are generated by analysing the map URL (that must be in the form: /_/instance/map_url)
|
||||
*/
|
||||
private static async getMapDetails(roomUrl: string): Promise<MapDetailsData> {
|
||||
if (!ADMIN_API_URL) {
|
||||
const roomUrlObj = new URL(roomUrl);
|
||||
|
||||
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrlObj.pathname);
|
||||
if (!match) {
|
||||
console.error("Unexpected room URL", roomUrl);
|
||||
throw new Error('Unexpected room URL "' + roomUrl + '"');
|
||||
}
|
||||
|
||||
const mapUrl = roomUrlObj.protocol + "//" + match[1];
|
||||
|
||||
return {
|
||||
mapUrl,
|
||||
policy_type: 1,
|
||||
textures: [],
|
||||
tags: [],
|
||||
};
|
||||
}
|
||||
|
||||
const result = await adminApi.fetchMapDetails(roomUrl);
|
||||
if (!isMapDetailsData(result)) {
|
||||
console.error("Unexpected room details received from server", result);
|
||||
throw new Error("Unexpected room details received from server");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private mapPromise: Promise<ITiledMap> | undefined;
|
||||
|
||||
/**
|
||||
* Returns a promise to the map file.
|
||||
* @throws LocalUrlError if the map we are trying to load is hosted on a local network
|
||||
* @throws Error
|
||||
*/
|
||||
private getMap(): Promise<ITiledMap> {
|
||||
if (!this.mapPromise) {
|
||||
this.mapPromise = mapFetcher.fetchMap(this.mapUrl);
|
||||
}
|
||||
|
||||
return this.mapPromise;
|
||||
}
|
||||
|
||||
private variableManagerPromise: Promise<VariablesManager> | undefined;
|
||||
|
||||
private getVariableManager(): Promise<VariablesManager> {
|
||||
if (!this.variableManagerPromise) {
|
||||
this.variableManagerPromise = this.getMap()
|
||||
.then((map) => {
|
||||
const variablesManager = new VariablesManager(this.roomUrl, map);
|
||||
return variablesManager.init();
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e instanceof LocalUrlError) {
|
||||
// If we are trying to load a local URL, we are probably in test mode.
|
||||
// In this case, let's bypass the server-side checks completely.
|
||||
|
||||
// Note: we run this message inside a setTimeout so that the room listeners can have time to connect.
|
||||
setTimeout(() => {
|
||||
for (const roomListener of this.roomListeners) {
|
||||
emitErrorOnRoomSocket(
|
||||
roomListener,
|
||||
"You are loading a local map. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled."
|
||||
);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
const variablesManager = new VariablesManager(this.roomUrl, null);
|
||||
return variablesManager.init();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.variableManagerPromise;
|
||||
}
|
||||
|
||||
public async getVariablesForTags(tags: string[]): Promise<Map<string, string>> {
|
||||
const variablesManager = await this.getVariableManager();
|
||||
return variablesManager.getVariablesForTags(tags);
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ interface ZoneDescriptor {
|
||||
}
|
||||
|
||||
export class PositionNotifier {
|
||||
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
|
||||
// TODO: we need a way to clean the zones if no one is in the zone and no one listening (to free memory!)
|
||||
|
||||
private zones: Zone[][] = [];
|
||||
|
||||
|
@ -1,30 +0,0 @@
|
||||
//helper functions to parse room IDs
|
||||
|
||||
export const isRoomAnonymous = (roomID: string): boolean => {
|
||||
if (roomID.startsWith("_/")) {
|
||||
return true;
|
||||
} else if (roomID.startsWith("@/")) {
|
||||
return false;
|
||||
} else {
|
||||
throw new Error("Incorrect room ID: " + roomID);
|
||||
}
|
||||
};
|
||||
|
||||
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
|
||||
const idParts = roomId.split("/");
|
||||
if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId);
|
||||
return idParts.slice(2).join("/");
|
||||
};
|
||||
export interface extractDataFromPrivateRoomIdResponse {
|
||||
organizationSlug: string;
|
||||
worldSlug: string;
|
||||
roomSlug: string;
|
||||
}
|
||||
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
|
||||
const idParts = roomId.split("/");
|
||||
if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId);
|
||||
const organizationSlug = idParts[1];
|
||||
const worldSlug = idParts[2];
|
||||
const roomSlug = idParts[3];
|
||||
return { organizationSlug, worldSlug, roomSlug };
|
||||
};
|
@ -5,6 +5,8 @@ import {
|
||||
AdminPusherToBackMessage,
|
||||
AdminRoomMessage,
|
||||
BanMessage,
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
EmotePromptMessage,
|
||||
EmptyMessage,
|
||||
ItemEventMessage,
|
||||
@ -13,17 +15,18 @@ import {
|
||||
PusherToBackMessage,
|
||||
QueryJitsiJwtMessage,
|
||||
RefreshRoomPromptMessage,
|
||||
RoomMessage,
|
||||
ServerToAdminClientMessage,
|
||||
ServerToClientMessage,
|
||||
SilentMessage,
|
||||
UserMovesMessage,
|
||||
VariableMessage,
|
||||
WebRtcSignalToServerMessage,
|
||||
WorldFullWarningToRoomMessage,
|
||||
ZoneMessage,
|
||||
} from "./Messages/generated/messages_pb";
|
||||
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
|
||||
import { socketManager } from "./Services/SocketManager";
|
||||
import { emitError } from "./Services/MessageHelpers";
|
||||
import { emitError, emitErrorOnRoomSocket, emitErrorOnZoneSocket } from "./Services/MessageHelpers";
|
||||
import { User, UserSocket } from "./Model/User";
|
||||
import { GameRoom } from "./Model/GameRoom";
|
||||
import Debug from "debug";
|
||||
@ -32,7 +35,8 @@ import { Admin } from "./Model/Admin";
|
||||
const debug = Debug("roommanager");
|
||||
|
||||
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
||||
export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>;
|
||||
export type ZoneSocket = ServerWritableStream<ZoneMessage, BatchToPusherMessage>;
|
||||
export type RoomSocket = ServerWritableStream<RoomMessage, BatchToPusherRoomMessage>;
|
||||
|
||||
const roomManager: IRoomManagerServer = {
|
||||
joinRoom: (call: UserSocket): void => {
|
||||
@ -42,6 +46,7 @@ const roomManager: IRoomManagerServer = {
|
||||
let user: User | null = null;
|
||||
|
||||
call.on("data", (message: PusherToBackMessage) => {
|
||||
(async () => {
|
||||
try {
|
||||
if (room === null || user === null) {
|
||||
if (message.hasJoinroommessage()) {
|
||||
@ -52,10 +57,11 @@ const roomManager: IRoomManagerServer = {
|
||||
room = gameRoom;
|
||||
user = myUser;
|
||||
} else {
|
||||
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
|
||||
//Connection may have been closed before the init was finished, so we have to manually disconnect the user.
|
||||
socketManager.leaveRoom(gameRoom, myUser);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((e) => emitError(call, e));
|
||||
} else {
|
||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||
}
|
||||
@ -71,7 +77,17 @@ const roomManager: IRoomManagerServer = {
|
||||
} else if (message.hasSilentmessage()) {
|
||||
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
||||
} else if (message.hasItemeventmessage()) {
|
||||
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
|
||||
socketManager.handleItemEvent(
|
||||
room,
|
||||
user,
|
||||
message.getItemeventmessage() as ItemEventMessage
|
||||
);
|
||||
} else if (message.hasVariablemessage()) {
|
||||
await socketManager.handleVariableEvent(
|
||||
room,
|
||||
user,
|
||||
message.getVariablemessage() as VariableMessage
|
||||
);
|
||||
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||
socketManager.emitVideo(
|
||||
room,
|
||||
@ -85,7 +101,10 @@ const roomManager: IRoomManagerServer = {
|
||||
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||
);
|
||||
} else if (message.hasPlayglobalmessage()) {
|
||||
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
|
||||
socketManager.emitPlayGlobalMessage(
|
||||
room,
|
||||
message.getPlayglobalmessage() as PlayGlobalMessage
|
||||
);
|
||||
} else if (message.hasQueryjitsijwtmessage()) {
|
||||
socketManager.handleQueryJitsiJwtMessage(
|
||||
user,
|
||||
@ -112,9 +131,11 @@ const roomManager: IRoomManagerServer = {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
emitError(call, e);
|
||||
call.end();
|
||||
}
|
||||
})().catch((e) => console.error(e));
|
||||
});
|
||||
|
||||
call.on("end", () => {
|
||||
@ -136,20 +157,54 @@ const roomManager: IRoomManagerServer = {
|
||||
debug("listenZone called");
|
||||
const zoneMessage = call.request;
|
||||
|
||||
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||
socketManager
|
||||
.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||
.catch((e) => {
|
||||
emitErrorOnZoneSocket(call, e.toString());
|
||||
});
|
||||
|
||||
call.on("cancelled", () => {
|
||||
debug("listenZone cancelled");
|
||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||
socketManager
|
||||
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||
.catch((e) => console.error(e));
|
||||
call.end();
|
||||
});
|
||||
|
||||
call.on("close", () => {
|
||||
debug("listenZone connection closed");
|
||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||
socketManager
|
||||
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||
.catch((e) => console.error(e));
|
||||
}).on("error", (e) => {
|
||||
console.error("An error occurred in listenZone stream:", e);
|
||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||
socketManager
|
||||
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||
.catch((e) => console.error(e));
|
||||
call.end();
|
||||
});
|
||||
},
|
||||
|
||||
listenRoom(call: RoomSocket): void {
|
||||
debug("listenRoom called");
|
||||
const roomMessage = call.request;
|
||||
|
||||
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => {
|
||||
emitErrorOnRoomSocket(call, e.toString());
|
||||
});
|
||||
|
||||
call.on("cancelled", () => {
|
||||
debug("listenRoom cancelled");
|
||||
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||
call.end();
|
||||
});
|
||||
|
||||
call.on("close", () => {
|
||||
debug("listenRoom connection closed");
|
||||
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||
}).on("error", (e) => {
|
||||
console.error("An error occurred in listenRoom stream:", e);
|
||||
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||
call.end();
|
||||
});
|
||||
},
|
||||
@ -165,9 +220,12 @@ const roomManager: IRoomManagerServer = {
|
||||
if (room === null) {
|
||||
if (message.hasSubscribetoroom()) {
|
||||
const roomId = message.getSubscribetoroom();
|
||||
socketManager.handleJoinAdminRoom(admin, roomId).then((gameRoom: GameRoom) => {
|
||||
socketManager
|
||||
.handleJoinAdminRoom(admin, roomId)
|
||||
.then((gameRoom: GameRoom) => {
|
||||
room = gameRoom;
|
||||
});
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
} else {
|
||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||
}
|
||||
@ -192,11 +250,9 @@ const roomManager: IRoomManagerServer = {
|
||||
});
|
||||
},
|
||||
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||
socketManager.sendAdminMessage(
|
||||
call.request.getRoomid(),
|
||||
call.request.getRecipientuuid(),
|
||||
call.request.getMessage()
|
||||
);
|
||||
socketManager
|
||||
.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||
.catch((e) => console.error(e));
|
||||
|
||||
callback(null, new EmptyMessage());
|
||||
},
|
||||
@ -207,26 +263,33 @@ const roomManager: IRoomManagerServer = {
|
||||
},
|
||||
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||
// FIXME Work in progress
|
||||
socketManager.banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage());
|
||||
socketManager
|
||||
.banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||
.catch((e) => console.error(e));
|
||||
|
||||
callback(null, new EmptyMessage());
|
||||
},
|
||||
sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||
socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage());
|
||||
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||
socketManager
|
||||
.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage(), call.request.getType())
|
||||
.catch((e) => console.error(e));
|
||||
callback(null, new EmptyMessage());
|
||||
},
|
||||
sendWorldFullWarningToRoom(
|
||||
call: ServerUnaryCall<WorldFullWarningToRoomMessage>,
|
||||
callback: sendUnaryData<EmptyMessage>
|
||||
): void {
|
||||
socketManager.dispatchWorlFullWarning(call.request.getRoomid());
|
||||
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||
socketManager.dispatchWorldFullWarning(call.request.getRoomid()).catch((e) => console.error(e));
|
||||
callback(null, new EmptyMessage());
|
||||
},
|
||||
sendRefreshRoomPrompt(
|
||||
call: ServerUnaryCall<RefreshRoomPromptMessage>,
|
||||
callback: sendUnaryData<EmptyMessage>
|
||||
): void {
|
||||
socketManager.dispatchRoomRefresh(call.request.getRoomid());
|
||||
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||
socketManager.dispatchRoomRefresh(call.request.getRoomid()).catch((e) => console.error(e));
|
||||
callback(null, new EmptyMessage());
|
||||
},
|
||||
};
|
||||
|
24
back/src/Services/AdminApi.ts
Normal file
24
back/src/Services/AdminApi.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import Axios from "axios";
|
||||
import { MapDetailsData } from "./AdminApi/MapDetailsData";
|
||||
import { RoomRedirect } from "./AdminApi/RoomRedirect";
|
||||
|
||||
class AdminApi {
|
||||
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
||||
if (!ADMIN_API_URL) {
|
||||
return Promise.reject(new Error("No admin backoffice set!"));
|
||||
}
|
||||
|
||||
const params: { playUri: string } = {
|
||||
playUri,
|
||||
};
|
||||
|
||||
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||
params,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const adminApi = new AdminApi();
|
11
back/src/Services/AdminApi/CharacterTexture.ts
Normal file
11
back/src/Services/AdminApi/CharacterTexture.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isCharacterTexture = new tg.IsInterface()
|
||||
.withProperties({
|
||||
id: tg.isNumber,
|
||||
level: tg.isNumber,
|
||||
url: tg.isString,
|
||||
rights: tg.isString,
|
||||
})
|
||||
.get();
|
||||
export type CharacterTexture = tg.GuardedType<typeof isCharacterTexture>;
|
21
back/src/Services/AdminApi/MapDetailsData.ts
Normal file
21
back/src/Services/AdminApi/MapDetailsData.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
import { isCharacterTexture } from "./CharacterTexture";
|
||||
import { isAny, isNumber } from "generic-type-guard";
|
||||
|
||||
/*const isNumericEnum =
|
||||
<T extends { [n: number]: string }>(vs: T) =>
|
||||
(v: any): v is T =>
|
||||
typeof v === "number" && v in vs;*/
|
||||
|
||||
export const isMapDetailsData = new tg.IsInterface()
|
||||
.withProperties({
|
||||
mapUrl: tg.isString,
|
||||
policy_type: isNumber, //isNumericEnum(GameRoomPolicyTypes),
|
||||
tags: tg.isArray(tg.isString),
|
||||
textures: tg.isArray(isCharacterTexture),
|
||||
})
|
||||
.withOptionalProperties({
|
||||
roomSlug: tg.isUnion(tg.isString, tg.isNull), // deprecated
|
||||
})
|
||||
.get();
|
||||
export type MapDetailsData = tg.GuardedType<typeof isMapDetailsData>;
|
8
back/src/Services/AdminApi/RoomRedirect.ts
Normal file
8
back/src/Services/AdminApi/RoomRedirect.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isRoomRedirect = new tg.IsInterface()
|
||||
.withProperties({
|
||||
redirectUrl: tg.isString,
|
||||
})
|
||||
.get();
|
||||
export type RoomRedirect = tg.GuardedType<typeof isRoomRedirect>;
|
1
back/src/Services/LocalUrlError.ts
Normal file
1
back/src/Services/LocalUrlError.ts
Normal file
@ -0,0 +1 @@
|
||||
export class LocalUrlError extends Error {}
|
69
back/src/Services/MapFetcher.ts
Normal file
69
back/src/Services/MapFetcher.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import Axios from "axios";
|
||||
import ipaddr from "ipaddr.js";
|
||||
import { Resolver } from "dns";
|
||||
import { promisify } from "util";
|
||||
import { LocalUrlError } from "./LocalUrlError";
|
||||
import { ITiledMap } from "@workadventure/tiled-map-type-guard";
|
||||
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||
|
||||
class MapFetcher {
|
||||
async fetchMap(mapUrl: string): Promise<ITiledMap> {
|
||||
// Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map)
|
||||
|
||||
if (await this.isLocalUrl(mapUrl)) {
|
||||
throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map');
|
||||
}
|
||||
|
||||
// Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that
|
||||
// returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially
|
||||
// target to different servers (and one could trick Axios.get into loading resources on the internal network
|
||||
// despite isLocalUrl checking that.
|
||||
// We can deem this problem not that important because:
|
||||
// - We make sure we are only passing "GET" requests
|
||||
// - The result of the query is never displayed to the end user
|
||||
const res = await Axios.get(mapUrl, {
|
||||
maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger
|
||||
timeout: 10000, // Timeout after 10 seconds
|
||||
});
|
||||
|
||||
if (!isTiledMap(res.data)) {
|
||||
//TODO fixme
|
||||
//throw new Error("Invalid map format for map " + mapUrl);
|
||||
console.error("Invalid map format for map " + mapUrl);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the domain name is localhost of *.localhost
|
||||
* Returns true if the domain name resolves to an IP address that is "private" (like 10.x.x.x or 192.168.x.x)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async isLocalUrl(url: string): Promise<boolean> {
|
||||
const urlObj = new URL(url);
|
||||
if (urlObj.hostname === "localhost" || urlObj.hostname.endsWith(".localhost")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let addresses = [];
|
||||
if (!ipaddr.isValid(urlObj.hostname)) {
|
||||
const resolver = new Resolver();
|
||||
addresses = await promisify(resolver.resolve).bind(resolver)(urlObj.hostname);
|
||||
} else {
|
||||
addresses = [urlObj.hostname];
|
||||
}
|
||||
|
||||
for (const address of addresses) {
|
||||
const addr = ipaddr.parse(address);
|
||||
if (addr.range() !== "unicast") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const mapFetcher = new MapFetcher();
|
@ -1,5 +1,14 @@
|
||||
import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb";
|
||||
import {
|
||||
BatchMessage,
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
ErrorMessage,
|
||||
ServerToClientMessage,
|
||||
SubToPusherMessage,
|
||||
SubToPusherRoomMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { UserSocket } from "_Model/User";
|
||||
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
||||
|
||||
export function emitError(Client: UserSocket, message: string): void {
|
||||
const errorMessage = new ErrorMessage();
|
||||
@ -13,3 +22,39 @@ export function emitError(Client: UserSocket, message: string): void {
|
||||
//}
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
export function emitErrorOnRoomSocket(Client: RoomSocket, message: string): void {
|
||||
console.error(message);
|
||||
|
||||
const errorMessage = new ErrorMessage();
|
||||
errorMessage.setMessage(message);
|
||||
|
||||
const subToPusherRoomMessage = new SubToPusherRoomMessage();
|
||||
subToPusherRoomMessage.setErrormessage(errorMessage);
|
||||
|
||||
const batchToPusherMessage = new BatchToPusherRoomMessage();
|
||||
batchToPusherMessage.addPayload(subToPusherRoomMessage);
|
||||
|
||||
//if (!Client.disconnecting) {
|
||||
Client.write(batchToPusherMessage);
|
||||
//}
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
export function emitErrorOnZoneSocket(Client: ZoneSocket, message: string): void {
|
||||
console.error(message);
|
||||
|
||||
const errorMessage = new ErrorMessage();
|
||||
errorMessage.setMessage(message);
|
||||
|
||||
const subToPusherMessage = new SubToPusherMessage();
|
||||
subToPusherMessage.setErrormessage(errorMessage);
|
||||
|
||||
const batchToPusherMessage = new BatchToPusherMessage();
|
||||
batchToPusherMessage.addPayload(subToPusherMessage);
|
||||
|
||||
//if (!Client.disconnecting) {
|
||||
Client.write(batchToPusherMessage);
|
||||
//}
|
||||
console.warn(message);
|
||||
}
|
||||
|
23
back/src/Services/RedisClient.ts
Normal file
23
back/src/Services/RedisClient.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ClientOpts, createClient, RedisClient } from "redis";
|
||||
import { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } from "../Enum/EnvironmentVariable";
|
||||
|
||||
let redisClient: RedisClient | null = null;
|
||||
|
||||
if (REDIS_HOST !== undefined) {
|
||||
const config: ClientOpts = {
|
||||
host: REDIS_HOST,
|
||||
port: REDIS_PORT,
|
||||
};
|
||||
|
||||
if (REDIS_PASSWORD) {
|
||||
config.password = REDIS_PASSWORD;
|
||||
}
|
||||
|
||||
redisClient = createClient(config);
|
||||
|
||||
redisClient.on("error", (err) => {
|
||||
console.error("Error connecting to Redis:", err);
|
||||
});
|
||||
}
|
||||
|
||||
export { redisClient };
|
43
back/src/Services/Repository/RedisVariablesRepository.ts
Normal file
43
back/src/Services/Repository/RedisVariablesRepository.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { promisify } from "util";
|
||||
import { RedisClient } from "redis";
|
||||
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||
|
||||
/**
|
||||
* Class in charge of saving/loading variables from the data store
|
||||
*/
|
||||
export class RedisVariablesRepository implements VariablesRepositoryInterface {
|
||||
private readonly hgetall: OmitThisParameter<(arg1: string) => Promise<{ [p: string]: string }>>;
|
||||
private readonly hset: OmitThisParameter<(arg1: [string, ...string[]]) => Promise<number>>;
|
||||
private readonly hdel: OmitThisParameter<(arg1: string, arg2: string) => Promise<number>>;
|
||||
|
||||
constructor(private redisClient: RedisClient) {
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
this.hgetall = promisify(redisClient.hgetall).bind(redisClient);
|
||||
this.hset = promisify(redisClient.hset).bind(redisClient);
|
||||
this.hdel = promisify(redisClient.hdel).bind(redisClient);
|
||||
/* eslint-enable @typescript-eslint/unbound-method */
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all variables for a room.
|
||||
*
|
||||
* Note: in Redis, variables are stored in a hashmap and the key is the roomUrl
|
||||
*/
|
||||
async loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
|
||||
return this.hgetall(roomUrl);
|
||||
}
|
||||
|
||||
async saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
|
||||
// The value is passed to JSON.stringify client side. If value is "undefined", JSON.stringify returns "undefined"
|
||||
// which is translated to empty string when fetching the value in the pusher.
|
||||
// Therefore, empty string server side == undefined client side.
|
||||
if (value === "") {
|
||||
return this.hdel(roomUrl, key);
|
||||
}
|
||||
|
||||
// TODO: SLOW WRITING EVERY 2 SECONDS WITH A TIMEOUT
|
||||
|
||||
// @ts-ignore See https://stackoverflow.com/questions/63539317/how-do-i-use-hmset-with-node-promisify
|
||||
return this.hset(roomUrl, key, value);
|
||||
}
|
||||
}
|
14
back/src/Services/Repository/VariablesRepository.ts
Normal file
14
back/src/Services/Repository/VariablesRepository.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { RedisVariablesRepository } from "./RedisVariablesRepository";
|
||||
import { redisClient } from "../RedisClient";
|
||||
import { VoidVariablesRepository } from "./VoidVariablesRepository";
|
||||
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||
|
||||
let variablesRepository: VariablesRepositoryInterface;
|
||||
if (!redisClient) {
|
||||
console.warn("WARNING: Redis isnot configured. No variables will be persisted.");
|
||||
variablesRepository = new VoidVariablesRepository();
|
||||
} else {
|
||||
variablesRepository = new RedisVariablesRepository(redisClient);
|
||||
}
|
||||
|
||||
export { variablesRepository };
|
10
back/src/Services/Repository/VariablesRepositoryInterface.ts
Normal file
10
back/src/Services/Repository/VariablesRepositoryInterface.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface VariablesRepositoryInterface {
|
||||
/**
|
||||
* Load all variables for a room.
|
||||
*
|
||||
* Note: in Redis, variables are stored in a hashmap and the key is the roomUrl
|
||||
*/
|
||||
loadVariables(roomUrl: string): Promise<{ [key: string]: string }>;
|
||||
|
||||
saveVariable(roomUrl: string, key: string, value: string): Promise<number>;
|
||||
}
|
14
back/src/Services/Repository/VoidVariablesRepository.ts
Normal file
14
back/src/Services/Repository/VoidVariablesRepository.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||
|
||||
/**
|
||||
* Mock class in charge of NOT saving/loading variables from the data store
|
||||
*/
|
||||
export class VoidVariablesRepository implements VariablesRepositoryInterface {
|
||||
loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
}
|
@ -30,6 +30,9 @@ import {
|
||||
BanUserMessage,
|
||||
RefreshRoomMessage,
|
||||
EmotePromptMessage,
|
||||
VariableMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
SubToPusherRoomMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { User, UserSocket } from "../Model/User";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
@ -48,7 +51,7 @@ import Jwt from "jsonwebtoken";
|
||||
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
||||
import { clientEventsEmitter } from "./ClientEventsEmitter";
|
||||
import { gaugeManager } from "./GaugeManager";
|
||||
import { ZoneSocket } from "../RoomManager";
|
||||
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
||||
import { Zone } from "_Model/Zone";
|
||||
import Debug from "debug";
|
||||
import { Admin } from "_Model/Admin";
|
||||
@ -65,7 +68,9 @@ function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): vo
|
||||
}
|
||||
|
||||
export class SocketManager {
|
||||
private rooms: Map<string, GameRoom> = new Map<string, GameRoom>();
|
||||
//private rooms = new Map<string, GameRoom>();
|
||||
// List of rooms in process of loading.
|
||||
private roomsPromises = new Map<string, PromiseLike<GameRoom>>();
|
||||
|
||||
constructor() {
|
||||
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
||||
@ -101,6 +106,16 @@ export class SocketManager {
|
||||
roomJoinedMessage.addItem(itemStateMessage);
|
||||
}
|
||||
|
||||
const variables = await room.getVariablesForTags(user.tags);
|
||||
|
||||
for (const [name, value] of variables.entries()) {
|
||||
const variableMessage = new VariableMessage();
|
||||
variableMessage.setName(name);
|
||||
variableMessage.setValue(value);
|
||||
|
||||
roomJoinedMessage.addVariable(variableMessage);
|
||||
}
|
||||
|
||||
roomJoinedMessage.setCurrentuserid(user.id);
|
||||
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
@ -114,7 +129,6 @@ export class SocketManager {
|
||||
}
|
||||
|
||||
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
|
||||
try {
|
||||
const userMoves = userMovesMessage.toObject();
|
||||
const position = userMovesMessage.getPosition();
|
||||
|
||||
@ -134,10 +148,6 @@ export class SocketManager {
|
||||
// update position in the world
|
||||
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
|
||||
//room.setViewport(client, client.viewport);
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "user_position" event');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Useless now, will be useful again if we allow editing details in game
|
||||
@ -156,18 +166,12 @@ export class SocketManager {
|
||||
}*/
|
||||
|
||||
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
|
||||
try {
|
||||
room.setSilent(user, silentMessage.getSilent());
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "handleSilentMessage"');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) {
|
||||
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
|
||||
|
||||
try {
|
||||
const subMessage = new SubMessage();
|
||||
subMessage.setItemeventmessage(itemEventMessage);
|
||||
|
||||
@ -178,10 +182,10 @@ export class SocketManager {
|
||||
}
|
||||
|
||||
room.setItemState(itemEvent.itemId, itemEvent.state);
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "item_event"');
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage): Promise<void> {
|
||||
return room.setVariable(variableMessage.getName(), variableMessage.getValue(), user);
|
||||
}
|
||||
|
||||
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
||||
@ -250,21 +254,21 @@ export class SocketManager {
|
||||
//user leave previous world
|
||||
room.leave(user);
|
||||
if (room.isEmpty()) {
|
||||
this.rooms.delete(room.roomId);
|
||||
this.roomsPromises.delete(room.roomUrl);
|
||||
gaugeManager.decNbRoomGauge();
|
||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||
}
|
||||
} finally {
|
||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
|
||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl);
|
||||
console.log("A user left");
|
||||
}
|
||||
}
|
||||
|
||||
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
||||
//check and create new world for a room
|
||||
let world = this.rooms.get(roomId);
|
||||
if (world === undefined) {
|
||||
world = new GameRoom(
|
||||
//check and create new room
|
||||
let roomPromise = this.roomsPromises.get(roomId);
|
||||
if (roomPromise === undefined) {
|
||||
roomPromise = GameRoom.create(
|
||||
roomId,
|
||||
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
||||
(user: User, group: Group) => this.disConnectedUser(user, group),
|
||||
@ -278,11 +282,18 @@ export class SocketManager {
|
||||
this.onClientLeave(thing, newZone, listener),
|
||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||
this.onEmote(emoteEventMessage, listener)
|
||||
);
|
||||
)
|
||||
.then((gameRoom) => {
|
||||
gaugeManager.incNbRoomGauge();
|
||||
this.rooms.set(roomId, world);
|
||||
return gameRoom;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.roomsPromises.delete(roomId);
|
||||
throw e;
|
||||
});
|
||||
this.roomsPromises.set(roomId, roomPromise);
|
||||
}
|
||||
return Promise.resolve(world);
|
||||
return roomPromise;
|
||||
}
|
||||
|
||||
private async joinRoom(
|
||||
@ -508,21 +519,16 @@ export class SocketManager {
|
||||
}
|
||||
|
||||
emitPlayGlobalMessage(room: GameRoom, playGlobalMessage: PlayGlobalMessage) {
|
||||
try {
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
serverToClientMessage.setPlayglobalmessage(playGlobalMessage);
|
||||
|
||||
for (const [id, user] of room.getUsers().entries()) {
|
||||
user.socket.write(serverToClientMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "emitPlayGlobalMessage" event');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public getWorlds(): Map<string, GameRoom> {
|
||||
return this.rooms;
|
||||
public getWorlds(): Map<string, PromiseLike<GameRoom>> {
|
||||
return this.roomsPromises;
|
||||
}
|
||||
|
||||
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
|
||||
@ -592,11 +598,10 @@ export class SocketManager {
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
public async addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error("In addZoneListener, could not find room with id '" + roomId + "'");
|
||||
return;
|
||||
throw new Error("In addZoneListener, could not find room with id '" + roomId + "'");
|
||||
}
|
||||
|
||||
const things = room.addZoneListener(call, x, y);
|
||||
@ -637,16 +642,37 @@ export class SocketManager {
|
||||
call.write(batchMessage);
|
||||
}
|
||||
|
||||
removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) {
|
||||
const room = this.rooms.get(roomId);
|
||||
async removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
||||
return;
|
||||
throw new Error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
||||
}
|
||||
|
||||
room.removeZoneListener(call, x, y);
|
||||
}
|
||||
|
||||
async addRoomListener(call: RoomSocket, roomId: string) {
|
||||
const room = await this.getOrCreateRoom(roomId);
|
||||
if (!room) {
|
||||
throw new Error("In addRoomListener, could not find room with id '" + roomId + "'");
|
||||
}
|
||||
|
||||
room.addRoomListener(call);
|
||||
|
||||
const batchMessage = new BatchToPusherRoomMessage();
|
||||
|
||||
call.write(batchMessage);
|
||||
}
|
||||
|
||||
async removeRoomListener(call: RoomSocket, roomId: string) {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
throw new Error("In removeRoomListener, could not find room with id '" + roomId + "'");
|
||||
}
|
||||
|
||||
room.removeRoomListener(call);
|
||||
}
|
||||
|
||||
public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise<GameRoom> {
|
||||
const room = await socketManager.getOrCreateRoom(roomId);
|
||||
|
||||
@ -658,14 +684,14 @@ export class SocketManager {
|
||||
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
||||
room.adminLeave(admin);
|
||||
if (room.isEmpty()) {
|
||||
this.rooms.delete(room.roomId);
|
||||
this.roomsPromises.delete(room.roomUrl);
|
||||
gaugeManager.decNbRoomGauge();
|
||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error(
|
||||
"In sendAdminMessage, could not find room with id '" +
|
||||
@ -675,8 +701,8 @@ export class SocketManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipient = room.getUserByUuid(recipientUuid);
|
||||
if (recipient === undefined) {
|
||||
const recipients = room.getUsersByUuid(recipientUuid);
|
||||
if (recipients.length === 0) {
|
||||
console.error(
|
||||
"In sendAdminMessage, could not find user with id '" +
|
||||
recipientUuid +
|
||||
@ -685,6 +711,7 @@ export class SocketManager {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const recipient of recipients) {
|
||||
const sendUserMessage = new SendUserMessage();
|
||||
sendUserMessage.setMessage(message);
|
||||
sendUserMessage.setType("ban"); //todo: is the type correct?
|
||||
@ -694,9 +721,10 @@ export class SocketManager {
|
||||
|
||||
recipient.socket.write(serverToClientMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public banUser(roomId: string, recipientUuid: string, message: string): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
public async banUser(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error(
|
||||
"In banUser, could not find room with id '" +
|
||||
@ -706,8 +734,8 @@ export class SocketManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipient = room.getUserByUuid(recipientUuid);
|
||||
if (recipient === undefined) {
|
||||
const recipients = room.getUsersByUuid(recipientUuid);
|
||||
if (recipients.length === 0) {
|
||||
console.error(
|
||||
"In banUser, could not find user with id '" +
|
||||
recipientUuid +
|
||||
@ -716,6 +744,7 @@ export class SocketManager {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const recipient of recipients) {
|
||||
// Let's leave the room now.
|
||||
room.leave(recipient);
|
||||
|
||||
@ -730,9 +759,10 @@ export class SocketManager {
|
||||
recipient.socket.write(serverToClientMessage);
|
||||
recipient.socket.end();
|
||||
}
|
||||
}
|
||||
|
||||
sendAdminRoomMessage(roomId: string, message: string) {
|
||||
const room = this.rooms.get(roomId);
|
||||
async sendAdminRoomMessage(roomId: string, message: string, type: string) {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
//todo: this should cause the http call to return a 500
|
||||
console.error(
|
||||
@ -746,7 +776,7 @@ export class SocketManager {
|
||||
room.getUsers().forEach((recipient) => {
|
||||
const sendUserMessage = new SendUserMessage();
|
||||
sendUserMessage.setMessage(message);
|
||||
sendUserMessage.setType("message");
|
||||
sendUserMessage.setType(type);
|
||||
|
||||
const clientMessage = new ServerToClientMessage();
|
||||
clientMessage.setSendusermessage(sendUserMessage);
|
||||
@ -755,12 +785,12 @@ export class SocketManager {
|
||||
});
|
||||
}
|
||||
|
||||
dispatchWorlFullWarning(roomId: string): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
async dispatchWorldFullWarning(roomId: string): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
//todo: this should cause the http call to return a 500
|
||||
console.error(
|
||||
"In sendAdminRoomMessage, could not find room with id '" +
|
||||
"In dispatchWorldFullWarning, could not find room with id '" +
|
||||
roomId +
|
||||
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
||||
);
|
||||
@ -777,8 +807,8 @@ export class SocketManager {
|
||||
});
|
||||
}
|
||||
|
||||
dispatchRoomRefresh(roomId: string): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
async dispatchRoomRefresh(roomId: string): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
218
back/src/Services/VariablesManager.ts
Normal file
218
back/src/Services/VariablesManager.ts
Normal file
@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Handles variables shared between the scripting API and the server.
|
||||
*/
|
||||
import { ITiledMap, ITiledMapObject, ITiledMapObjectLayer } from "@workadventure/tiled-map-type-guard/dist";
|
||||
import { User } from "_Model/User";
|
||||
import { variablesRepository } from "./Repository/VariablesRepository";
|
||||
import { redisClient } from "./RedisClient";
|
||||
|
||||
interface Variable {
|
||||
defaultValue?: string;
|
||||
persist?: boolean;
|
||||
readableBy?: string;
|
||||
writableBy?: string;
|
||||
}
|
||||
|
||||
export class VariablesManager {
|
||||
/**
|
||||
* The actual values of the variables for the current room
|
||||
*/
|
||||
private _variables = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* The list of variables that are allowed
|
||||
*/
|
||||
private variableObjects: Map<string, Variable> | undefined;
|
||||
|
||||
/**
|
||||
* @param map The map can be "null" if it is hosted on a private network. In this case, we assume this is a test setup and bypass any server-side checks.
|
||||
*/
|
||||
constructor(private roomUrl: string, private map: ITiledMap | null) {
|
||||
// We initialize the list of variable object at room start. The objects cannot be edited later
|
||||
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
|
||||
if (map) {
|
||||
this.variableObjects = VariablesManager.findVariablesInMap(map);
|
||||
|
||||
// Let's initialize default values
|
||||
for (const [name, variableObject] of this.variableObjects.entries()) {
|
||||
if (variableObject.defaultValue !== undefined) {
|
||||
this._variables.set(name, variableObject.defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Let's load data from the Redis backend.
|
||||
*/
|
||||
public async init(): Promise<VariablesManager> {
|
||||
if (!this.shouldPersist()) {
|
||||
return this;
|
||||
}
|
||||
const variables = await variablesRepository.loadVariables(this.roomUrl);
|
||||
for (const key in variables) {
|
||||
// Let's only set variables if they are in the map (if the map has changed, maybe stored variables do not exist anymore)
|
||||
if (this.variableObjects) {
|
||||
const variableObject = this.variableObjects.get(key);
|
||||
if (variableObject === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (!variableObject.persist) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
this._variables.set(key, variables[key]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if saving should be enabled, and false otherwise.
|
||||
*
|
||||
* Saving is enabled if REDIS_HOST is set
|
||||
* unless we are editing a local map
|
||||
* unless we are in dev mode in which case it is ok to save
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private shouldPersist(): boolean {
|
||||
return redisClient !== null && (this.map !== null || process.env.NODE_ENV === "development");
|
||||
}
|
||||
|
||||
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
|
||||
const objects = new Map<string, Variable>();
|
||||
for (const layer of map.layers) {
|
||||
if (layer.type === "objectgroup") {
|
||||
for (const object of (layer as ITiledMapObjectLayer).objects) {
|
||||
if (object.type === "variable") {
|
||||
if (object.template) {
|
||||
console.warn(
|
||||
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// We store a copy of the object (to make it immutable)
|
||||
objects.set(object.name, this.iTiledObjectToVariable(object));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
|
||||
const variable: Variable = {};
|
||||
|
||||
if (object.properties) {
|
||||
for (const property of object.properties) {
|
||||
const value = property.value;
|
||||
switch (property.name) {
|
||||
case "default":
|
||||
variable.defaultValue = JSON.stringify(value);
|
||||
break;
|
||||
case "persist":
|
||||
if (typeof value !== "boolean") {
|
||||
throw new Error('The persist property of variable "' + object.name + '" must be a boolean');
|
||||
}
|
||||
variable.persist = value;
|
||||
break;
|
||||
case "writableBy":
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(
|
||||
'The writableBy property of variable "' + object.name + '" must be a string'
|
||||
);
|
||||
}
|
||||
if (value) {
|
||||
variable.writableBy = value;
|
||||
}
|
||||
break;
|
||||
case "readableBy":
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(
|
||||
'The readableBy property of variable "' + object.name + '" must be a string'
|
||||
);
|
||||
}
|
||||
if (value) {
|
||||
variable.readableBy = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return variable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the variable.
|
||||
*
|
||||
* Returns who is allowed to read the variable (the readableby property) or "undefined" if anyone can read it.
|
||||
* Also, returns "false" if the variable was not modified (because we set it to the value it already has)
|
||||
*
|
||||
* @param name
|
||||
* @param value
|
||||
* @param user
|
||||
*/
|
||||
setVariable(name: string, value: string, user: User): string | undefined | false {
|
||||
let readableBy: string | undefined;
|
||||
let variableObject: Variable | undefined;
|
||||
if (this.variableObjects) {
|
||||
variableObject = this.variableObjects.get(name);
|
||||
if (variableObject === undefined) {
|
||||
throw new Error('Trying to set a variable "' + name + '" that is not defined as an object in the map.');
|
||||
}
|
||||
|
||||
if (variableObject.writableBy && !user.tags.includes(variableObject.writableBy)) {
|
||||
throw new Error(
|
||||
'Trying to set a variable "' +
|
||||
name +
|
||||
'". User "' +
|
||||
user.name +
|
||||
'" does not have sufficient permission. Required tag: "' +
|
||||
variableObject.writableBy +
|
||||
'". User tags: ' +
|
||||
user.tags.join(", ") +
|
||||
"."
|
||||
);
|
||||
}
|
||||
|
||||
readableBy = variableObject.readableBy;
|
||||
}
|
||||
|
||||
// If the value is not modified, return false
|
||||
if (this._variables.get(name) === value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._variables.set(name, value);
|
||||
|
||||
if (variableObject !== undefined && variableObject.persist) {
|
||||
variablesRepository
|
||||
.saveVariable(this.roomUrl, name, value)
|
||||
.catch((e) => console.error("Error while saving variable in Redis:", e));
|
||||
}
|
||||
|
||||
return readableBy;
|
||||
}
|
||||
|
||||
public getVariablesForTags(tags: string[]): Map<string, string> {
|
||||
if (this.variableObjects === undefined) {
|
||||
return this._variables;
|
||||
}
|
||||
|
||||
const readableVariables = new Map<string, string>();
|
||||
|
||||
for (const [key, value] of this._variables.entries()) {
|
||||
const variableObject = this.variableObjects.get(key);
|
||||
if (variableObject === undefined) {
|
||||
throw new Error('Unexpected variable "' + key + '" found has no associated variableObject.');
|
||||
}
|
||||
if (!variableObject.readableBy || tags.includes(variableObject.readableBy)) {
|
||||
readableVariables.set(key, value);
|
||||
}
|
||||
}
|
||||
return readableVariables;
|
||||
}
|
||||
}
|
@ -1,59 +1,62 @@
|
||||
import "jasmine";
|
||||
import {ConnectCallback, DisconnectCallback, GameRoom} from "../src/Model/GameRoom";
|
||||
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
||||
import {Group} from "../src/Model/Group";
|
||||
import {User, UserSocket} from "_Model/User";
|
||||
import {JoinRoomMessage, PositionMessage} from "../src/Messages/generated/messages_pb";
|
||||
import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom";
|
||||
import { Point } from "../src/Model/Websocket/MessageUserPosition";
|
||||
import { Group } from "../src/Model/Group";
|
||||
import { User, UserSocket } from "_Model/User";
|
||||
import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import {EmoteCallback} from "_Model/Zone";
|
||||
import { EmoteCallback } from "_Model/Zone";
|
||||
|
||||
function createMockUser(userId: number): User {
|
||||
return {
|
||||
userId
|
||||
userId,
|
||||
} as unknown as User;
|
||||
}
|
||||
|
||||
function createMockUserSocket(): UserSocket {
|
||||
return {
|
||||
} as unknown as UserSocket;
|
||||
return {} as unknown as UserSocket;
|
||||
}
|
||||
|
||||
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage
|
||||
{
|
||||
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage {
|
||||
const positionMessage = new PositionMessage();
|
||||
positionMessage.setX(x);
|
||||
positionMessage.setY(y);
|
||||
positionMessage.setDirection(Direction.DOWN);
|
||||
positionMessage.setMoving(false);
|
||||
const joinRoomMessage = new JoinRoomMessage();
|
||||
joinRoomMessage.setUseruuid('1');
|
||||
joinRoomMessage.setIpaddress('10.0.0.2');
|
||||
joinRoomMessage.setName('foo');
|
||||
joinRoomMessage.setRoomid('_/global/test.json');
|
||||
joinRoomMessage.setUseruuid("1");
|
||||
joinRoomMessage.setIpaddress("10.0.0.2");
|
||||
joinRoomMessage.setName("foo");
|
||||
joinRoomMessage.setRoomid("_/global/test.json");
|
||||
joinRoomMessage.setPositionmessage(positionMessage);
|
||||
return joinRoomMessage;
|
||||
}
|
||||
|
||||
const emote: EmoteCallback = (emoteEventMessage, listener): void => {}
|
||||
const emote: EmoteCallback = (emoteEventMessage, listener): void => {};
|
||||
|
||||
describe("GameRoom", () => {
|
||||
it("should connect user1 and user2", () => {
|
||||
it("should connect user1 and user2", async () => {
|
||||
let connectCalledNumber: number = 0;
|
||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||
connectCalledNumber++;
|
||||
}
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||
};
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
|
||||
|
||||
}
|
||||
const world = await GameRoom.create(
|
||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||
connect,
|
||||
disconnect,
|
||||
160,
|
||||
160,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
);
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
|
||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
||||
|
||||
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 500, 100));
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 500, 100));
|
||||
|
||||
world.updatePosition(user2, new Point(261, 100));
|
||||
|
||||
@ -67,26 +70,34 @@ describe("GameRoom", () => {
|
||||
expect(connectCalledNumber).toBe(2);
|
||||
});
|
||||
|
||||
it("should connect 3 users", () => {
|
||||
it("should connect 3 users", async () => {
|
||||
let connectCalled: boolean = false;
|
||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||
connectCalled = true;
|
||||
}
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||
};
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
|
||||
|
||||
}
|
||||
const world = await GameRoom.create(
|
||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||
connect,
|
||||
disconnect,
|
||||
160,
|
||||
160,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
);
|
||||
|
||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 200, 100));
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 200, 100));
|
||||
|
||||
expect(connectCalled).toBe(true);
|
||||
connectCalled = false;
|
||||
|
||||
// baz joins at the outer limit of the group
|
||||
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 311, 100));
|
||||
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 311, 100));
|
||||
|
||||
expect(connectCalled).toBe(false);
|
||||
|
||||
@ -95,31 +106,40 @@ describe("GameRoom", () => {
|
||||
expect(connectCalled).toBe(true);
|
||||
});
|
||||
|
||||
it("should disconnect user1 and user2", () => {
|
||||
it("should disconnect user1 and user2", async () => {
|
||||
let connectCalled: boolean = false;
|
||||
let disconnectCallNumber: number = 0;
|
||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||
connectCalled = true;
|
||||
}
|
||||
};
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||
disconnectCallNumber++;
|
||||
}
|
||||
};
|
||||
|
||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
||||
const world = await GameRoom.create(
|
||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||
connect,
|
||||
disconnect,
|
||||
160,
|
||||
160,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
);
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 259, 100));
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 259, 100));
|
||||
|
||||
expect(connectCalled).toBe(true);
|
||||
expect(disconnectCallNumber).toBe(0);
|
||||
|
||||
world.updatePosition(user2, new Point(100+160+160+1, 100));
|
||||
world.updatePosition(user2, new Point(100 + 160 + 160 + 1, 100));
|
||||
|
||||
expect(disconnectCallNumber).toBe(2);
|
||||
|
||||
world.updatePosition(user2, new Point(262, 100));
|
||||
expect(disconnectCallNumber).toBe(2);
|
||||
});
|
||||
|
||||
})
|
||||
});
|
||||
|
32
back/tests/MapFetcherTest.ts
Normal file
32
back/tests/MapFetcherTest.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { arrayIntersect } from "../src/Services/ArrayHelper";
|
||||
import { mapFetcher } from "../src/Services/MapFetcher";
|
||||
|
||||
describe("MapFetcher", () => {
|
||||
it("should return true on localhost ending URLs", async () => {
|
||||
expect(await mapFetcher.isLocalUrl("https://localhost")).toBeTrue();
|
||||
expect(await mapFetcher.isLocalUrl("https://foo.localhost")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return true on DNS resolving to a local domain", async () => {
|
||||
expect(await mapFetcher.isLocalUrl("https://127.0.0.1.nip.io")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return true on an IP resolving to a local domain", async () => {
|
||||
expect(await mapFetcher.isLocalUrl("https://127.0.0.1")).toBeTrue();
|
||||
expect(await mapFetcher.isLocalUrl("https://192.168.0.1")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return false on an IP resolving to a global domain", async () => {
|
||||
expect(await mapFetcher.isLocalUrl("https://51.12.42.42")).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return false on an DNS resolving to a global domain", async () => {
|
||||
expect(await mapFetcher.isLocalUrl("https://maps.workadventu.re")).toBeFalse();
|
||||
});
|
||||
|
||||
it("should throw error on invalid domain", async () => {
|
||||
await expectAsync(
|
||||
mapFetcher.isLocalUrl("https://this.domain.name.doesnotexistfoobgjkgfdjkgldf.com")
|
||||
).toBeRejected();
|
||||
});
|
||||
});
|
@ -1,19 +0,0 @@
|
||||
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier";
|
||||
|
||||
describe("RoomIdentifier", () => {
|
||||
it("should flag public id as anonymous", () => {
|
||||
expect(isRoomAnonymous('_/global/test')).toBe(true);
|
||||
});
|
||||
it("should flag public id as not anonymous", () => {
|
||||
expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false);
|
||||
});
|
||||
it("should extract roomSlug from public ID", () => {
|
||||
expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json');
|
||||
});
|
||||
it("should extract correct from private ID", () => {
|
||||
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor');
|
||||
expect(organizationSlug).toBe('afup');
|
||||
expect(worldSlug).toBe('afup2020');
|
||||
expect(roomSlug).toBe('1floor');
|
||||
});
|
||||
})
|
@ -3,7 +3,7 @@
|
||||
"experimentalDecorators": true,
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"downlevelIteration": true,
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
|
@ -122,6 +122,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||
|
||||
"@types/redis@^2.8.31":
|
||||
version "2.8.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.31.tgz#c11c1b269fec132ac2ec9eb891edf72fc549149e"
|
||||
integrity sha512-daWrrTDYaa5iSDFbgzZ9gOOzyp2AJmYK59OlG/2KGBgYWF3lfs8GDKm1c//tik5Uc93hDD36O+qLPvzDolChbA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/strip-bom@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
|
||||
@ -187,6 +194,13 @@
|
||||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@workadventure/tiled-map-type-guard@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.0.tgz#02524602ee8b2688429a1f56df1d04da3fc171ba"
|
||||
integrity sha512-Mc0SE128otQnYlScQWVaQVyu1+CkailU/FTBh09UTrVnBAhyMO+jIn9vT9+Dv244xq+uzgQDpXmiVdjgrYFQ+A==
|
||||
dependencies:
|
||||
generic-type-guard "^3.4.1"
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
@ -797,6 +811,11 @@ delegates@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||
|
||||
denque@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
|
||||
integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
|
||||
|
||||
detect-libc@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
@ -1181,6 +1200,11 @@ generic-type-guard@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1"
|
||||
integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ==
|
||||
|
||||
generic-type-guard@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.4.1.tgz#0896dc018de915c890562a34763858076e4676da"
|
||||
integrity sha512-sXce0Lz3Wfy2rR1W8O8kUemgEriTeG1x8shqSJeWGb0FwJu2qBEkB1M2qXbdSLmpgDnHcIXo0Dj/1VLNJkK/QA==
|
||||
|
||||
get-own-enumerable-property-symbols@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
|
||||
@ -1417,6 +1441,11 @@ invert-kv@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
|
||||
integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
|
||||
|
||||
ipaddr.js@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
|
||||
integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
|
||||
|
||||
is-accessor-descriptor@^0.1.6:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
|
||||
@ -2424,6 +2453,33 @@ redent@^1.0.0:
|
||||
indent-string "^2.1.0"
|
||||
strip-indent "^1.0.1"
|
||||
|
||||
redis-commands@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
|
||||
integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
|
||||
|
||||
redis-errors@^1.0.0, redis-errors@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
|
||||
integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
|
||||
|
||||
redis-parser@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
|
||||
integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
|
||||
dependencies:
|
||||
redis-errors "^1.0.0"
|
||||
|
||||
redis@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c"
|
||||
integrity sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==
|
||||
dependencies:
|
||||
denque "^1.5.0"
|
||||
redis-commands "^1.7.0"
|
||||
redis-errors "^1.2.0"
|
||||
redis-parser "^3.0.0"
|
||||
|
||||
regex-not@^1.0.0, regex-not@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
|
||||
|
@ -13,7 +13,6 @@ RoomConnection.setWebsocketFactory((url: string) => {
|
||||
});
|
||||
|
||||
async function startOneUser(): Promise<void> {
|
||||
await connectionManager.anonymousLogin(true);
|
||||
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
||||
{
|
||||
x: 783,
|
||||
@ -23,7 +22,7 @@ async function startOneUser(): Promise<void> {
|
||||
bottom: 200,
|
||||
left: 500,
|
||||
right: 800
|
||||
});
|
||||
}, null);
|
||||
|
||||
const connection = onConnect.connection;
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
"JITSI_URL": env.JITSI_URL,
|
||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||
"REDIS_HOST": "redis",
|
||||
} + (if adminUrl != null then {
|
||||
"ADMIN_API_URL": adminUrl,
|
||||
} else {})
|
||||
@ -40,6 +41,7 @@
|
||||
"JITSI_URL": env.JITSI_URL,
|
||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||
"REDIS_HOST": "redis",
|
||||
} + (if adminUrl != null then {
|
||||
"ADMIN_API_URL": adminUrl,
|
||||
} else {})
|
||||
@ -97,6 +99,9 @@
|
||||
},
|
||||
"ports": [80]
|
||||
},
|
||||
"redis": {
|
||||
"image": "redis:6",
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
k8sextension(k8sConf)::
|
||||
|
@ -53,7 +53,7 @@ services:
|
||||
- "traefik.http.routers.front-ssl.service=front"
|
||||
|
||||
pusher:
|
||||
image: thecodingmachine/nodejs:12
|
||||
image: thecodingmachine/nodejs:14
|
||||
command: yarn dev
|
||||
#command: yarn run prod
|
||||
#command: yarn run profile
|
||||
@ -66,6 +66,10 @@ services:
|
||||
API_URL: back:50051
|
||||
JITSI_URL: $JITSI_URL
|
||||
JITSI_ISS: $JITSI_ISS
|
||||
FRONT_URL: http://localhost
|
||||
OPID_CLIENT_ID: $OPID_CLIENT_ID
|
||||
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
|
||||
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
|
||||
volumes:
|
||||
- ./pusher:/usr/src/app
|
||||
labels:
|
||||
@ -120,6 +124,8 @@ services:
|
||||
JITSI_URL: $JITSI_URL
|
||||
JITSI_ISS: $JITSI_ISS
|
||||
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
||||
REDIS_HOST: redis
|
||||
NODE_ENV: development
|
||||
volumes:
|
||||
- ./back:/usr/src/app
|
||||
labels:
|
||||
@ -168,6 +174,9 @@ services:
|
||||
- ./front:/usr/src/front
|
||||
- ./pusher:/usr/src/pusher
|
||||
|
||||
redis:
|
||||
image: redis:6
|
||||
|
||||
# coturn:
|
||||
# image: coturn/coturn:4.5.2
|
||||
# command:
|
||||
|
@ -55,7 +55,7 @@ services:
|
||||
- "traefik.http.routers.front-ssl.service=front"
|
||||
|
||||
pusher:
|
||||
image: thecodingmachine/nodejs:12
|
||||
image: thecodingmachine/nodejs:14
|
||||
command: yarn dev
|
||||
environment:
|
||||
DEBUG: "socket:*"
|
||||
@ -66,6 +66,10 @@ services:
|
||||
API_URL: back:50051
|
||||
JITSI_URL: $JITSI_URL
|
||||
JITSI_ISS: $JITSI_ISS
|
||||
FRONT_URL: http://play.workadventure.localhost
|
||||
OPID_CLIENT_ID: $OPID_CLIENT_ID
|
||||
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
|
||||
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
|
||||
volumes:
|
||||
- ./pusher:/usr/src/app
|
||||
labels:
|
||||
@ -115,6 +119,8 @@ services:
|
||||
JITSI_ISS: $JITSI_ISS
|
||||
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
|
||||
MAX_PER_GROUP: "MAX_PER_GROUP"
|
||||
REDIS_HOST: redis
|
||||
NODE_ENV: development
|
||||
volumes:
|
||||
- ./back:/usr/src/app
|
||||
labels:
|
||||
@ -157,6 +163,20 @@ services:
|
||||
- ./front:/usr/src/front
|
||||
- ./pusher:/usr/src/pusher
|
||||
|
||||
redis:
|
||||
image: redis:6
|
||||
|
||||
redisinsight:
|
||||
image: redislabs/redisinsight:latest
|
||||
labels:
|
||||
- "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)"
|
||||
- "traefik.http.routers.redisinsight.entryPoints=web"
|
||||
- "traefik.http.services.redisinsight.loadbalancer.server.port=8001"
|
||||
- "traefik.http.routers.redisinsight-ssl.rule=Host(`redis.workadventure.localhost`)"
|
||||
- "traefik.http.routers.redisinsight-ssl.entryPoints=websecure"
|
||||
- "traefik.http.routers.redisinsight-ssl.tls=true"
|
||||
- "traefik.http.routers.redisinsight-ssl.service=redisinsight"
|
||||
|
||||
# coturn:
|
||||
# image: coturn/coturn:4.5.2
|
||||
# command:
|
||||
|
@ -1,6 +1,63 @@
|
||||
{.section-title.accent.text-primary}
|
||||
# API Player functions Reference
|
||||
|
||||
### Get the player name
|
||||
|
||||
```
|
||||
WA.player.name: string;
|
||||
```
|
||||
|
||||
The player name is available from the `WA.player.name` property.
|
||||
|
||||
{.alert.alert-info}
|
||||
You need to wait for the end of the initialization before accessing `WA.player.name`
|
||||
|
||||
```typescript
|
||||
WA.onInit().then(() => {
|
||||
console.log('Player name: ', WA.player.name);
|
||||
})
|
||||
```
|
||||
|
||||
### Get the player ID
|
||||
|
||||
```
|
||||
WA.player.id: string|undefined;
|
||||
```
|
||||
|
||||
The player ID is available from the `WA.player.id` property.
|
||||
This is a unique identifier for a given player. Anonymous player might not have an id.
|
||||
|
||||
{.alert.alert-info}
|
||||
You need to wait for the end of the initialization before accessing `WA.player.id`
|
||||
|
||||
```typescript
|
||||
WA.onInit().then(() => {
|
||||
console.log('Player ID: ', WA.player.id);
|
||||
})
|
||||
```
|
||||
|
||||
### Get the tags of the player
|
||||
|
||||
```
|
||||
WA.player.tags: string[];
|
||||
```
|
||||
|
||||
The player tags are available from the `WA.player.tags` property.
|
||||
They represent a set of rights the player acquires after login in.
|
||||
|
||||
{.alert.alert-warn}
|
||||
Tags attributed to a user depend on the authentication system you are using. For the hosted version
|
||||
of WorkAdventure, you can define tags related to the user in the [administration panel](https://workadventu.re/admin-guide/manage-members).
|
||||
|
||||
{.alert.alert-info}
|
||||
You need to wait for the end of the initialization before accessing `WA.player.tags`
|
||||
|
||||
```typescript
|
||||
WA.onInit().then(() => {
|
||||
console.log('Tags: ', WA.player.tags);
|
||||
})
|
||||
```
|
||||
|
||||
### Listen to player movement
|
||||
```
|
||||
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
|
||||
|
@ -1,9 +1,11 @@
|
||||
{.section-title.accent.text-primary}
|
||||
# API Reference
|
||||
|
||||
- [Start / Init functions](api-start.md)
|
||||
- [Navigation functions](api-nav.md)
|
||||
- [Chat functions](api-chat.md)
|
||||
- [Room functions](api-room.md)
|
||||
- [State related functions](api-state.md)
|
||||
- [Player functions](api-player.md)
|
||||
- [UI functions](api-ui.md)
|
||||
- [Sound functions](api-sound.md)
|
||||
|
@ -79,44 +79,58 @@ Example :
|
||||
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
|
||||
```
|
||||
|
||||
### Getting information on the current room
|
||||
```
|
||||
WA.room.getCurrentRoom(): Promise<Room>
|
||||
```
|
||||
Return a promise that resolves to a `Room` object with the following attributes :
|
||||
* **id (string) :** ID of the current room
|
||||
* **map (ITiledMap) :** contains the JSON map file with the properties that were set by the script if `setProperty` was called.
|
||||
* **mapUrl (string) :** Url of the JSON map file
|
||||
* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer
|
||||
### Get the room id
|
||||
|
||||
Example :
|
||||
```javascript
|
||||
WA.room.getCurrentRoom((room) => {
|
||||
if (room.id === '42') {
|
||||
console.log(room.map);
|
||||
window.open(room.mapUrl, '_blank');
|
||||
}
|
||||
```
|
||||
WA.room.id: string;
|
||||
```
|
||||
|
||||
The ID of the current room is available from the `WA.room.id` property.
|
||||
|
||||
{.alert.alert-info}
|
||||
You need to wait for the end of the initialization before accessing `WA.room.id`
|
||||
|
||||
```typescript
|
||||
WA.onInit().then(() => {
|
||||
console.log('Room id: ', WA.room.id);
|
||||
// Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json"
|
||||
})
|
||||
```
|
||||
|
||||
### Getting information on the current user
|
||||
```
|
||||
WA.player.getCurrentUser(): Promise<User>
|
||||
```
|
||||
Return a promise that resolves to a `User` object with the following attributes :
|
||||
* **id (string) :** ID of the current user
|
||||
* **nickName (string) :** name displayed above the current user
|
||||
* **tags (string[]) :** list of all the tags of the current user
|
||||
### Get the map URL
|
||||
|
||||
Example :
|
||||
```javascript
|
||||
WA.room.getCurrentUser().then((user) => {
|
||||
if (user.nickName === 'ABC') {
|
||||
console.log(user.tags);
|
||||
}
|
||||
```
|
||||
WA.room.mapURL: string;
|
||||
```
|
||||
|
||||
The URL of the map is available from the `WA.room.mapURL` property.
|
||||
|
||||
{.alert.alert-info}
|
||||
You need to wait for the end of the initialization before accessing `WA.room.mapURL`
|
||||
|
||||
```typescript
|
||||
WA.onInit().then(() => {
|
||||
console.log('Map URL: ', WA.room.mapURL);
|
||||
// Will output something like: 'https://mymap.org/map.json"
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Getting map data
|
||||
```
|
||||
WA.room.getTiledMap(): Promise<ITiledMap>
|
||||
```
|
||||
|
||||
Returns a promise that resolves to the JSON map file.
|
||||
|
||||
```javascript
|
||||
const map = await WA.room.getTiledMap();
|
||||
console.log("Map generated with Tiled version ", map.tiledversion);
|
||||
```
|
||||
|
||||
Check the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/).
|
||||
|
||||
### Changing tiles
|
||||
```
|
||||
WA.room.setTiles(tiles: TileDescriptor[]): void
|
||||
@ -149,3 +163,17 @@ WA.room.setTiles([
|
||||
{x: 9, y: 4, tile: 'blue', layer: 'setTiles'}
|
||||
]);
|
||||
```
|
||||
|
||||
### Loading a tileset
|
||||
```
|
||||
WA.room.loadTileset(url: string): Promise<number>
|
||||
```
|
||||
Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset.
|
||||
|
||||
You can create a tileset file in Tile Editor.
|
||||
|
||||
```javascript
|
||||
WA.room.loadTileset("Assets/Tileset.json").then((firstId) => {
|
||||
WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]);
|
||||
})
|
||||
```
|
30
docs/maps/api-start.md
Normal file
30
docs/maps/api-start.md
Normal file
@ -0,0 +1,30 @@
|
||||
{.section-title.accent.text-primary}
|
||||
# API start functions Reference
|
||||
|
||||
### Waiting for WorkAdventure API to be available
|
||||
|
||||
When your script / iFrame loads WorkAdventure, it takes a few milliseconds for your script / iFrame to exchange
|
||||
data with WorkAdventure. You should wait for the WorkAdventure API to be fully ready using the `WA.onInit()` method.
|
||||
|
||||
```
|
||||
WA.onInit(): Promise<void>
|
||||
```
|
||||
|
||||
Some properties (like the current user name, or the room ID) are not available until `WA.onInit` has completed.
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
WA.onInit().then(() => {
|
||||
console.log('Current player name: ', WA.player.name);
|
||||
});
|
||||
```
|
||||
|
||||
Or the same code, using await/async:
|
||||
|
||||
```typescript
|
||||
(async () => {
|
||||
await WA.onInit();
|
||||
console.log('Current player name: ', WA.player.name);
|
||||
})();
|
||||
```
|
136
docs/maps/api-state.md
Normal file
136
docs/maps/api-state.md
Normal file
@ -0,0 +1,136 @@
|
||||
{.section-title.accent.text-primary}
|
||||
# API state related functions Reference
|
||||
|
||||
## Saving / loading state
|
||||
|
||||
The `WA.state` functions allow you to easily share a common state between all the players in a given room.
|
||||
Moreover, `WA.state` functions can be used to persist this state across reloads.
|
||||
|
||||
```
|
||||
WA.state.saveVariable(key : string, data : unknown): void
|
||||
WA.state.loadVariable(key : string) : unknown
|
||||
WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription
|
||||
WA.state.[any property]: unknown
|
||||
```
|
||||
|
||||
These methods and properties can be used to save, load and track changes in variables related to the current room.
|
||||
|
||||
Variables stored in `WA.state` can be any value that is serializable in JSON.
|
||||
|
||||
Please refrain from storing large amounts of data in a room. Those functions are typically useful for saving or restoring
|
||||
configuration / metadata.
|
||||
|
||||
{.alert.alert-warning}
|
||||
We are in the process of fine-tuning variables, and we will eventually put limits on the maximum size a variable can hold. We will also put limits on the number of calls you can make to saving variables, so don't change the value of a variable every 10ms, this will fail in the future.
|
||||
|
||||
|
||||
Example :
|
||||
```javascript
|
||||
WA.state.saveVariable('config', {
|
||||
'bottomExitUrl': '/@/org/world/castle',
|
||||
'topExitUrl': '/@/org/world/tower',
|
||||
'enableBirdSound': true
|
||||
}).catch(e => console.error('Something went wrong while saving variable', e));
|
||||
//...
|
||||
let config = WA.state.loadVariable('config');
|
||||
```
|
||||
|
||||
You can use the shortcut properties to load and save variables. The code above is similar to:
|
||||
|
||||
```javascript
|
||||
WA.state.config = {
|
||||
'bottomExitUrl': '/@/org/world/castle',
|
||||
'topExitUrl': '/@/org/world/tower',
|
||||
'enableBirdSound': true
|
||||
};
|
||||
|
||||
//...
|
||||
let config = WA.state.config;
|
||||
```
|
||||
|
||||
Note: `saveVariable` returns a promise that will fail in case the variable cannot be saved. This
|
||||
can happen if your user does not have the required rights (more on that in the next chapter).
|
||||
In contrast, if you use the WA.state properties, you cannot access the promise and therefore cannot
|
||||
know for sure if your variable was properly saved.
|
||||
|
||||
If you are using Typescript, please note that the type of variables is `unknown`. This is
|
||||
for security purpose, as we don't know the type of the variable. In order to use the returned value,
|
||||
you will need to cast it to the correct type (or better, use a [Type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to actually check at runtime
|
||||
that you get the expected type).
|
||||
|
||||
{.alert.alert-warning}
|
||||
For security reasons, the list of variables you are allowed to access and modify is **restricted** (otherwise, anyone on your map could set any data).
|
||||
Variables storage is subject to an authorization process. Read below to learn more.
|
||||
|
||||
### Declaring allowed keys
|
||||
|
||||
In order to declare allowed keys related to a room, you need to add **objects** in an "object layer" of the map.
|
||||
|
||||
Each object will represent a variable.
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<img src="https://workadventu.re/img/docs/object_variable.png" class="figure-img img-fluid rounded" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
The name of the variable is the name of the object.
|
||||
The object **type** MUST be **variable**.
|
||||
|
||||
You can set a default value for the object in the `default` property.
|
||||
|
||||
### Persisting variables state
|
||||
|
||||
Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay
|
||||
in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the
|
||||
server restarts).
|
||||
|
||||
{.alert.alert-info}
|
||||
Do not use `persist` for highly dynamic values that have a short life spawn.
|
||||
|
||||
### Managing access rights to variables
|
||||
|
||||
With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string
|
||||
representing a "tag". Anyone having this "tag" can read/write in the variable.
|
||||
|
||||
{.alert.alert-warning}
|
||||
`readableBy` and `writableBy` are specific to the "online" version of WorkAdventure because the notion of tags
|
||||
is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure).
|
||||
|
||||
Finally, the `jsonSchema` property can contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable.
|
||||
Trying to set a variable to a value that is not compatible with the schema will fail.
|
||||
|
||||
|
||||
## Tracking variables changes
|
||||
|
||||
The properties of the `WA.state` object are shared in real-time between users of a same room. You can listen to modifications
|
||||
of any property of `WA.state` by using the `WA.state.onVariableChange()` method.
|
||||
|
||||
```
|
||||
WA.state.onVariableChange(name: string): Observable<unknown>
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```javascript
|
||||
WA.state.onVariableChange('config').subscribe((value) => {
|
||||
console.log('Variable "config" changed. New value: ', value);
|
||||
});
|
||||
```
|
||||
|
||||
The `WA.state.onVariableChange` method returns an [RxJS `Observable` object](https://rxjs.dev/guide/observable). This is
|
||||
an object on which you can add subscriptions using the `subscribe` method.
|
||||
|
||||
### Stopping tracking variables
|
||||
|
||||
If you want to stop tracking a variable change, the `subscribe` method returns a subscription object with an `unsubscribe` method.
|
||||
|
||||
**Example with unsubscription:**
|
||||
|
||||
```javascript
|
||||
const subscription = WA.state.onVariableChange('config').subscribe((value) => {
|
||||
console.log('Variable "config" changed. New value: ', value);
|
||||
});
|
||||
// Later:
|
||||
subscription.unsubscribe();
|
||||
```
|
@ -55,10 +55,10 @@ Start by testing this with a simple message sent to the chat.
|
||||
|
||||
**script.js**
|
||||
```javascript
|
||||
WA.sendChatMessage('Hello world', 'Mr Robot');
|
||||
WA.chat.sendChatMessage('Hello world', 'Mr Robot');
|
||||
```
|
||||
|
||||
The `WA` objects contains a number of useful methods enabling you to interact with the WorkAdventure game. For instance, `WA.sendChatMessage` opens the chat and adds a message in it.
|
||||
The `WA` objects contains a number of useful methods enabling you to interact with the WorkAdventure game. For instance, `WA.chat.sendChatMessage` opens the chat and adds a message in it.
|
||||
|
||||
In your browser console, when you open the map, the chat message should be displayed right away.
|
||||
|
||||
|
35
docs/maps/text.md
Normal file
35
docs/maps/text.md
Normal file
@ -0,0 +1,35 @@
|
||||
{.section-title.accent.text-primary}
|
||||
# Writing text on a map
|
||||
|
||||
## Solution 1: design a specific tileset (recommended)
|
||||
|
||||
If you want to write some text on a map, our recommendation is to create a tileset that contains
|
||||
your text. You will obtain the most pleasant graphical result with this result, since you will be able
|
||||
to control the fonts you use, and you will be able to disable the antialiasing of the font to get a
|
||||
"crispy" result easily.
|
||||
|
||||
## Solution 2: using a "text" object in Tiled
|
||||
|
||||
On "object" layers, Tiled has support for "Text" objects. You can use these objects to add some
|
||||
text on your map.
|
||||
|
||||
WorkAdventure will do its best to display the text properly. However, you need to know that:
|
||||
|
||||
- Tiled displays your system fonts.
|
||||
- Computers have different sets of fonts. Therefore, browsers never rely on system fonts
|
||||
- Which means if you select a font in Tiled, it is quite unlikely it will render properly in WorkAdventure
|
||||
|
||||
To circumvent this problem, in your text object in Tiled, you can add an additional property: `font-family`.
|
||||
|
||||
The `font-family` property can contain any "web-font" that can be loaded by your browser.
|
||||
|
||||
{.alert.alert-info}
|
||||
**Pro-tip:** By default, WorkAdventure uses the **'"Press Start 2P"'** font, which is a great pixelated
|
||||
font that has support for a variety of accents. It renders great when used at *8px* size.
|
||||
|
||||
<div>
|
||||
<figure class="figure">
|
||||
<img src="https://workadventu.re/img/docs/text-object.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
|
||||
<figcaption class="figure-caption">The "font-family" property</figcaption>
|
||||
</figure>
|
||||
</div>
|
@ -56,7 +56,7 @@ A few things to notice:
|
||||
|
||||
## Building walls and "collidable" areas
|
||||
|
||||
By default, the characters can traverse any tiles. If you want to prevent your characeter from going through a tile (like a wall or a desktop), you must make this tile "collidable". You can do this by settings the `collides` property on a given tile.
|
||||
By default, the characters can traverse any tiles. If you want to prevent your character from going through a tile (like a wall or a desktop), you must make this tile "collidable". You can do this by settings the `collides` property on a given tile.
|
||||
|
||||
To make a tile "collidable", you should:
|
||||
|
||||
|
18
front/dist/resources/html/warningContainer.html
vendored
18
front/dist/resources/html/warningContainer.html
vendored
@ -1,18 +0,0 @@
|
||||
<style>
|
||||
#warningMain {
|
||||
border-radius: 5px;
|
||||
height: 100px;
|
||||
width: 300px;
|
||||
background-color: red;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#warningMain h2 {
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<main id="warningMain">
|
||||
<h2>Warning!</h2>
|
||||
<p>This world is close to its limit!</p>
|
||||
</main>
|
1
front/dist/webpack.config.d.ts
vendored
Normal file
1
front/dist/webpack.config.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
8393
front/package-lock.json
generated
8393
front/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@
|
||||
"@types/mini-css-extract-plugin": "^1.4.3",
|
||||
"@types/node": "^15.3.0",
|
||||
"@types/quill": "^1.3.7",
|
||||
"@types/uuidv4": "^5.0.0",
|
||||
"@types/webpack-dev-server": "^3.11.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||
"@typescript-eslint/parser": "^4.23.0",
|
||||
@ -39,7 +40,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/press-start-2p": "^4.3.0",
|
||||
"@types/simple-peer": "^9.6.0",
|
||||
"@types/simple-peer": "^9.11.1",
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
"axios": "^0.21.1",
|
||||
"cross-env": "^7.0.3",
|
||||
@ -50,10 +51,12 @@
|
||||
"phaser3-rex-plugins": "^1.1.42",
|
||||
"queue-typescript": "^1.0.1",
|
||||
"quill": "1.3.6",
|
||||
"quill-delta-to-html": "^0.12.0",
|
||||
"rxjs": "^6.6.3",
|
||||
"simple-peer": "^9.6.2",
|
||||
"simple-peer": "^9.11.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"standardized-audio-context": "^25.2.4"
|
||||
"standardized-audio-context": "^25.2.4",
|
||||
"uuidv4": "^6.2.10"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "run-p templater serve svelte-check-watch",
|
||||
|
@ -1,92 +0,0 @@
|
||||
import {HtmlUtils} from "./../WebRtc/HtmlUtils";
|
||||
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
|
||||
import type {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
|
||||
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
|
||||
import {soundManager} from "../Phaser/Game/SoundManager";
|
||||
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
|
||||
|
||||
export class GlobalMessageManager {
|
||||
|
||||
constructor(private Connection: RoomConnection) {
|
||||
this.initialise();
|
||||
}
|
||||
|
||||
initialise(){
|
||||
//receive signal to show message
|
||||
this.Connection.receivePlayGlobalMessage((message: PlayGlobalMessageInterface) => {
|
||||
this.playMessage(message);
|
||||
});
|
||||
|
||||
//receive signal to close message
|
||||
this.Connection.receiveStopGlobalMessage((messageId: string) => {
|
||||
this.stopMessage(messageId);
|
||||
});
|
||||
|
||||
//receive signal to close message
|
||||
this.Connection.receiveTeleportMessage((map: string) => {
|
||||
console.log('map to teleport user', map);
|
||||
//TODO teleport user on map
|
||||
});
|
||||
}
|
||||
|
||||
private playMessage(message : PlayGlobalMessageInterface){
|
||||
const previousMessage = document.getElementById(this.getHtmlMessageId(message.id));
|
||||
if(previousMessage){
|
||||
previousMessage.remove();
|
||||
}
|
||||
|
||||
if(AdminMessageEventTypes.audio === message.type){
|
||||
this.playAudioMessage(message.id, message.message);
|
||||
}
|
||||
|
||||
if(AdminMessageEventTypes.admin === message.type){
|
||||
this.playTextMessage(message.id, message.message);
|
||||
}
|
||||
}
|
||||
|
||||
private playAudioMessage(messageId : string, urlMessage: string) {
|
||||
soundPlayingStore.playSound(UPLOADER_URL + urlMessage);
|
||||
}
|
||||
|
||||
private playTextMessage(messageId : string, htmlMessage: string){
|
||||
//add button to clear message
|
||||
const buttonText = document.createElement('p');
|
||||
buttonText.id = 'button-clear-message';
|
||||
buttonText.innerText = 'Clear';
|
||||
|
||||
const buttonMainConsole = document.createElement('div');
|
||||
buttonMainConsole.classList.add('clear');
|
||||
buttonMainConsole.appendChild(buttonText);
|
||||
buttonMainConsole.addEventListener('click', () => {
|
||||
messageContainer.style.top = '-80%';
|
||||
setTimeout(() => {
|
||||
messageContainer.remove();
|
||||
buttonMainConsole.remove();
|
||||
});
|
||||
});
|
||||
|
||||
//create content message
|
||||
const messageCotent = document.createElement('div');
|
||||
messageCotent.innerHTML = htmlMessage;
|
||||
messageCotent.className = "content-message";
|
||||
|
||||
//add message container
|
||||
const messageContainer = document.createElement('div');
|
||||
messageContainer.id = this.getHtmlMessageId(messageId);
|
||||
messageContainer.className = "message-container";
|
||||
messageContainer.appendChild(messageCotent);
|
||||
messageContainer.appendChild(buttonMainConsole);
|
||||
|
||||
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
|
||||
mainSectionDiv.appendChild(messageContainer);
|
||||
}
|
||||
|
||||
private stopMessage(messageId: string){
|
||||
HtmlUtils.removeElementByIdOrFail<HTMLDivElement>(this.getHtmlMessageId(messageId));
|
||||
}
|
||||
|
||||
private getHtmlMessageId(messageId: string) : string{
|
||||
return `message-${messageId}`;
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
import type {TypeMessageInterface} from "./UserMessageManager";
|
||||
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
||||
|
||||
let modalTimeOut : NodeJS.Timeout;
|
||||
|
||||
export class TypeMessageExt implements TypeMessageInterface{
|
||||
private nbSecond = 0;
|
||||
private maxNbSecond = 10;
|
||||
private titleMessage = 'IMPORTANT !';
|
||||
|
||||
showMessage(message: string, canDeleteMessage: boolean = true): void {
|
||||
//delete previous modal
|
||||
try{
|
||||
if(modalTimeOut){
|
||||
clearTimeout(modalTimeOut);
|
||||
}
|
||||
const modal = HtmlUtils.getElementByIdOrFail('report-message-user');
|
||||
modal.remove();
|
||||
}catch (err){
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
//create new modal
|
||||
const div : HTMLDivElement = document.createElement('div');
|
||||
div.classList.add('modal-report-user');
|
||||
div.id = 'report-message-user';
|
||||
div.style.backgroundColor = '#000000e0';
|
||||
|
||||
const img : HTMLImageElement = document.createElement('img');
|
||||
img.src = 'resources/logos/report.svg';
|
||||
div.appendChild(img);
|
||||
|
||||
const title : HTMLParagraphElement = document.createElement('p');
|
||||
title.id = 'title-report-user';
|
||||
title.innerText = `${this.titleMessage} (${this.maxNbSecond})`;
|
||||
div.appendChild(title);
|
||||
|
||||
const p : HTMLParagraphElement = document.createElement('p');
|
||||
p.id = 'body-report-user'
|
||||
p.innerText = message;
|
||||
div.appendChild(p);
|
||||
|
||||
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
|
||||
mainSectionDiv.appendChild(div);
|
||||
|
||||
const reportMessageAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('report-message');
|
||||
// FIXME: this will fail on iOS
|
||||
// We should move the sound playing into the GameScene and listen to the event of a report using a store
|
||||
try {
|
||||
reportMessageAudio.play();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
this.nbSecond = this.maxNbSecond;
|
||||
setTimeout((c) => {
|
||||
this.forMessage(title, canDeleteMessage);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
forMessage(title: HTMLParagraphElement, canDeleteMessage: boolean = true){
|
||||
this.nbSecond -= 1;
|
||||
title.innerText = `${this.titleMessage} (${this.nbSecond})`;
|
||||
if(this.nbSecond > 0){
|
||||
modalTimeOut = setTimeout(() => {
|
||||
this.forMessage(title, canDeleteMessage);
|
||||
}, 1000);
|
||||
}else {
|
||||
title.innerText = this.titleMessage;
|
||||
|
||||
if (!canDeleteMessage) {
|
||||
return;
|
||||
}
|
||||
const imgCancel: HTMLImageElement = document.createElement('img');
|
||||
imgCancel.id = 'cancel-report-user';
|
||||
imgCancel.src = 'resources/logos/close.svg';
|
||||
|
||||
const div = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('report-message-user');
|
||||
div.appendChild(imgCancel);
|
||||
imgCancel.addEventListener('click', () => {
|
||||
div.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Message extends TypeMessageExt {}
|
||||
|
||||
export class Ban extends TypeMessageExt {}
|
||||
|
||||
export class Banned extends TypeMessageExt {
|
||||
showMessage(message: string){
|
||||
super.showMessage(message, false);
|
||||
}
|
||||
}
|
@ -1,43 +1,34 @@
|
||||
import * as TypeMessages from "./TypeMessage";
|
||||
import {Banned} from "./TypeMessage";
|
||||
import {adminMessagesService} from "../Connexion/AdminMessagesService";
|
||||
|
||||
export interface TypeMessageInterface {
|
||||
showMessage(message: string): void;
|
||||
}
|
||||
import { AdminMessageEventTypes, adminMessagesService } from "../Connexion/AdminMessagesService";
|
||||
import { textMessageContentStore, textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore";
|
||||
import { soundPlayingStore } from "../Stores/SoundPlayingStore";
|
||||
import { UPLOADER_URL } from "../Enum/EnvironmentVariable";
|
||||
import { banMessageContentStore, banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore";
|
||||
|
||||
class UserMessageManager {
|
||||
|
||||
typeMessages: Map<string, TypeMessageInterface> = new Map<string, TypeMessageInterface>();
|
||||
receiveBannedMessageListener!: Function;
|
||||
|
||||
constructor() {
|
||||
const valueTypeMessageTab = Object.values(TypeMessages);
|
||||
Object.keys(TypeMessages).forEach((value: string, index: number) => {
|
||||
const typeMessageInstance: TypeMessageInterface = (new valueTypeMessageTab[index]() as TypeMessageInterface);
|
||||
this.typeMessages.set(value.toLowerCase(), typeMessageInstance);
|
||||
});
|
||||
|
||||
adminMessagesService.messageStream.subscribe((event) => {
|
||||
const typeMessage = this.showMessage(event.type, event.text);
|
||||
if(typeMessage instanceof Banned) {
|
||||
textMessageVisibleStore.set(false);
|
||||
banMessageVisibleStore.set(false);
|
||||
if (event.type === AdminMessageEventTypes.admin) {
|
||||
textMessageContentStore.set(event.text);
|
||||
textMessageVisibleStore.set(true);
|
||||
} else if (event.type === AdminMessageEventTypes.audio) {
|
||||
soundPlayingStore.playSound(UPLOADER_URL + event.text);
|
||||
} else if (event.type === AdminMessageEventTypes.ban) {
|
||||
banMessageContentStore.set(event.text);
|
||||
banMessageVisibleStore.set(true);
|
||||
} else if (event.type === AdminMessageEventTypes.banned) {
|
||||
banMessageContentStore.set(event.text);
|
||||
banMessageVisibleStore.set(true);
|
||||
this.receiveBannedMessageListener();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
showMessage(type: string, message: string) {
|
||||
const classTypeMessage = this.typeMessages.get(type.toLowerCase());
|
||||
if (!classTypeMessage) {
|
||||
console.error('Message unknown');
|
||||
return;
|
||||
}
|
||||
classTypeMessage.showMessage(message);
|
||||
return classTypeMessage;
|
||||
}
|
||||
|
||||
setReceiveBanListener(callback: Function){
|
||||
setReceiveBanListener(callback: Function) {
|
||||
this.receiveBannedMessageListener = callback;
|
||||
}
|
||||
}
|
||||
export const userMessageManager = new UserMessageManager()
|
||||
export const userMessageManager = new UserMessageManager();
|
||||
|
@ -4,10 +4,11 @@ export const isGameStateEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
roomId: tg.isString,
|
||||
mapUrl: tg.isString,
|
||||
nickname: tg.isUnion(tg.isString, tg.isNull),
|
||||
nickname: tg.isString,
|
||||
uuid: tg.isUnion(tg.isString, tg.isUndefined),
|
||||
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
||||
tags: tg.isArray(tg.isString),
|
||||
variables: tg.isObject,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { GameStateEvent } from "./GameStateEvent";
|
||||
import * as tg from "generic-type-guard";
|
||||
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
|
||||
import type { ChatEvent } from "./ChatEvent";
|
||||
import type { ClosePopupEvent } from "./ClosePopupEvent";
|
||||
@ -9,7 +9,6 @@ import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
|
||||
import type { OpenPopupEvent } from "./OpenPopupEvent";
|
||||
import type { OpenTabEvent } from "./OpenTabEvent";
|
||||
import type { UserInputChatEvent } from "./UserInputChatEvent";
|
||||
import type { DataLayerEvent } from "./DataLayerEvent";
|
||||
import type { LayerEvent } from "./LayerEvent";
|
||||
import type { SetPropertyEvent } from "./setPropertyEvent";
|
||||
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
||||
@ -18,6 +17,12 @@ import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
|
||||
import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
|
||||
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
|
||||
import type { SetTilesEvent } from "./SetTilesEvent";
|
||||
import type { SetVariableEvent } from "./SetVariableEvent";
|
||||
import { isGameStateEvent } from "./GameStateEvent";
|
||||
import { isMapDataEvent } from "./MapDataEvent";
|
||||
import { isSetVariableEvent } from "./SetVariableEvent";
|
||||
import type { LoadTilesetEvent } from "./LoadTilesetEvent";
|
||||
import { isLoadTilesetEvent } from "./LoadTilesetEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T;
|
||||
@ -43,11 +48,11 @@ export type IframeEventMap = {
|
||||
showLayer: LayerEvent;
|
||||
hideLayer: LayerEvent;
|
||||
setProperty: SetPropertyEvent;
|
||||
getDataLayer: undefined;
|
||||
loadSound: LoadSoundEvent;
|
||||
playSound: PlaySoundEvent;
|
||||
stopSound: null;
|
||||
getState: undefined;
|
||||
loadTileset: LoadTilesetEvent;
|
||||
registerMenuCommand: MenuItemRegisterEvent;
|
||||
setTiles: SetTilesEvent;
|
||||
};
|
||||
@ -66,8 +71,8 @@ export interface IframeResponseEventMap {
|
||||
leaveEvent: EnterLeaveEvent;
|
||||
buttonClickedEvent: ButtonClickedEvent;
|
||||
hasPlayerMoved: HasPlayerMovedEvent;
|
||||
dataLayer: DataLayerEvent;
|
||||
menuItemClicked: MenuItemClickedEvent;
|
||||
setVariable: SetVariableEvent;
|
||||
}
|
||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||
type: T;
|
||||
@ -79,20 +84,43 @@ export const isIframeResponseEventWrapper = (event: {
|
||||
type?: string;
|
||||
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
|
||||
|
||||
|
||||
/**
|
||||
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame
|
||||
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame.
|
||||
* Types are defined using Type guards that will actually bused to enforce and check types.
|
||||
*/
|
||||
export type IframeQueryMap = {
|
||||
export const iframeQueryMapTypeGuards = {
|
||||
getState: {
|
||||
query: undefined,
|
||||
answer: GameStateEvent
|
||||
query: tg.isUndefined,
|
||||
answer: isGameStateEvent,
|
||||
},
|
||||
}
|
||||
getMapData: {
|
||||
query: tg.isUndefined,
|
||||
answer: isMapDataEvent,
|
||||
},
|
||||
setVariable: {
|
||||
query: isSetVariableEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
loadTileset: {
|
||||
query: isLoadTilesetEvent,
|
||||
answer: tg.isNumber,
|
||||
},
|
||||
};
|
||||
|
||||
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
|
||||
type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards;
|
||||
type UnknownToVoid<T> = undefined extends T ? void : T;
|
||||
|
||||
export type IframeQueryMap = {
|
||||
[key in keyof IframeQueryMapTypeGuardsType]: {
|
||||
query: GuardedType<IframeQueryMapTypeGuardsType[key]["query"]>;
|
||||
answer: UnknownToVoid<GuardedType<IframeQueryMapTypeGuardsType[key]["answer"]>>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface IframeQuery<T extends keyof IframeQueryMap> {
|
||||
type: T;
|
||||
data: IframeQueryMap[T]['query'];
|
||||
data: IframeQueryMap[T]["query"];
|
||||
}
|
||||
|
||||
export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
|
||||
@ -100,19 +128,36 @@ export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
|
||||
query: IframeQuery<T>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => typeof event.type === 'string';
|
||||
export const isIframeQueryKey = (type: string): type is keyof IframeQueryMap => {
|
||||
return type in iframeQueryMapTypeGuards;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> => typeof event.id === 'number' && isIframeQuery(event.query);
|
||||
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => {
|
||||
const type = event.type;
|
||||
if (typeof type !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (!isIframeQueryKey(type)) {
|
||||
return false;
|
||||
}
|
||||
return iframeQueryMapTypeGuards[type].query(event.data);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> =>
|
||||
typeof event.id === "number" && isIframeQuery(event.query);
|
||||
|
||||
export interface IframeAnswerEvent<T extends keyof IframeQueryMap> {
|
||||
id: number;
|
||||
type: T;
|
||||
data: IframeQueryMap[T]['answer'];
|
||||
data: IframeQueryMap[T]["answer"];
|
||||
}
|
||||
|
||||
export const isIframeAnswerEvent = (event: { type?: string, id?: number }): event is IframeAnswerEvent<keyof IframeQueryMap> => typeof event.type === 'string' && typeof event.id === 'number';
|
||||
export const isIframeAnswerEvent = (event: {
|
||||
type?: string;
|
||||
id?: number;
|
||||
}): event is IframeAnswerEvent<keyof IframeQueryMap> => typeof event.type === "string" && typeof event.id === "number";
|
||||
|
||||
export interface IframeErrorAnswerEvent {
|
||||
id: number;
|
||||
@ -120,4 +165,9 @@ export interface IframeErrorAnswerEvent {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export const isIframeErrorAnswerEvent = (event: { type?: string, id?: number, error?: string }): event is IframeErrorAnswerEvent => typeof event.type === 'string' && typeof event.id === 'number' && typeof event.error === 'string';
|
||||
export const isIframeErrorAnswerEvent = (event: {
|
||||
type?: string;
|
||||
id?: number;
|
||||
error?: string;
|
||||
}): event is IframeErrorAnswerEvent =>
|
||||
typeof event.type === "string" && typeof event.id === "number" && typeof event.error === "string";
|
||||
|
12
front/src/Api/Events/LoadTilesetEvent.ts
Normal file
12
front/src/Api/Events/LoadTilesetEvent.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isLoadTilesetEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
url: tg.isString,
|
||||
})
|
||||
.get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type LoadTilesetEvent = tg.GuardedType<typeof isLoadTilesetEvent>;
|
@ -1,6 +1,6 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isDataLayerEvent = new tg.IsInterface()
|
||||
export const isMapDataEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
data: tg.isObject,
|
||||
})
|
||||
@ -9,4 +9,4 @@ export const isDataLayerEvent = new tg.IsInterface()
|
||||
/**
|
||||
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
|
||||
*/
|
||||
export type DataLayerEvent = tg.GuardedType<typeof isDataLayerEvent>;
|
||||
export type MapDataEvent = tg.GuardedType<typeof isMapDataEvent>;
|
20
front/src/Api/Events/SetVariableEvent.ts
Normal file
20
front/src/Api/Events/SetVariableEvent.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
import { isMenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
|
||||
|
||||
export const isSetVariableEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
key: tg.isString,
|
||||
value: tg.isUnknown,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to change the value of the property of the layer
|
||||
*/
|
||||
export type SetVariableEvent = tg.GuardedType<typeof isSetVariableEvent>;
|
||||
|
||||
export const isSetVariableIframeEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
type: tg.isSingletonString("setVariable"),
|
||||
data: isSetVariableEvent,
|
||||
})
|
||||
.get();
|
@ -12,7 +12,8 @@ import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent
|
||||
import {
|
||||
IframeErrorAnswerEvent,
|
||||
IframeEvent,
|
||||
IframeEventMap, IframeQueryMap,
|
||||
IframeEventMap,
|
||||
IframeQueryMap,
|
||||
IframeResponseEvent,
|
||||
IframeResponseEventMap,
|
||||
isIframeEventWrapper,
|
||||
@ -25,21 +26,25 @@ import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
|
||||
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
|
||||
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
|
||||
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
|
||||
import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||
import type { DataLayerEvent } from "./Events/DataLayerEvent";
|
||||
import type { GameStateEvent } from "./Events/GameStateEvent";
|
||||
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
|
||||
import { isLoadPageEvent } from "./Events/LoadPageEvent";
|
||||
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
|
||||
import type { SetVariableEvent } from "./Events/SetVariableEvent";
|
||||
|
||||
type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']>;
|
||||
type AnswererCallback<T extends keyof IframeQueryMap> = (
|
||||
query: IframeQueryMap[T]["query"],
|
||||
source: MessageEventSource | null
|
||||
) => IframeQueryMap[T]["answer"] | PromiseLike<IframeQueryMap[T]["answer"]>;
|
||||
|
||||
/**
|
||||
* 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 _readyStream: Subject<HTMLIFrameElement> = new Subject();
|
||||
public readonly readyStream = this._readyStream.asObservable();
|
||||
|
||||
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
||||
public readonly chatStream = this._chatStream.asObservable();
|
||||
|
||||
@ -85,9 +90,6 @@ class IframeListener {
|
||||
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
|
||||
public readonly setPropertyStream = this._setPropertyStream.asObservable();
|
||||
|
||||
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
|
||||
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
|
||||
|
||||
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
|
||||
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
|
||||
|
||||
@ -111,8 +113,9 @@ class IframeListener {
|
||||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||
private sendPlayerMove: boolean = false;
|
||||
|
||||
// Note: we are forced to type this in unknown and later cast with "as" because of https://github.com/microsoft/TypeScript/issues/31904
|
||||
private answerers: {
|
||||
[key in keyof IframeQueryMap]?: AnswererCallback<key>
|
||||
[str in keyof IframeQueryMap]?: unknown;
|
||||
} = {};
|
||||
|
||||
init() {
|
||||
@ -152,40 +155,61 @@ class IframeListener {
|
||||
const queryId = payload.id;
|
||||
const query = payload.query;
|
||||
|
||||
const answerer = this.answerers[query.type];
|
||||
const answerer = this.answerers[query.type] as AnswererCallback<keyof IframeQueryMap> | undefined;
|
||||
if (answerer === undefined) {
|
||||
const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.';
|
||||
const errorMsg =
|
||||
'The iFrame sent a message of type "' +
|
||||
query.type +
|
||||
'" but there is no service configured to answer these messages.';
|
||||
console.error(errorMsg);
|
||||
iframe.contentWindow?.postMessage({
|
||||
iframe.contentWindow?.postMessage(
|
||||
{
|
||||
id: queryId,
|
||||
type: query.type,
|
||||
error: errorMsg
|
||||
} as IframeErrorAnswerEvent, '*');
|
||||
error: errorMsg,
|
||||
} as IframeErrorAnswerEvent,
|
||||
"*"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.resolve(answerer(query.data)).then((value) => {
|
||||
iframe?.contentWindow?.postMessage({
|
||||
id: queryId,
|
||||
type: query.type,
|
||||
data: value
|
||||
}, '*');
|
||||
}).catch(reason => {
|
||||
console.error('An error occurred while responding to an iFrame query.', reason);
|
||||
let reasonMsg: string;
|
||||
const errorHandler = (reason: unknown) => {
|
||||
console.error("An error occurred while responding to an iFrame query.", reason);
|
||||
let reasonMsg: string = "";
|
||||
if (reason instanceof Error) {
|
||||
reasonMsg = reason.message;
|
||||
} else {
|
||||
reasonMsg = reason.toString();
|
||||
} else if (typeof reason === "object") {
|
||||
reasonMsg = reason ? reason.toString() : "";
|
||||
} else if (typeof reason === "string") {
|
||||
reasonMsg = reason;
|
||||
}
|
||||
|
||||
iframe?.contentWindow?.postMessage({
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
id: queryId,
|
||||
type: query.type,
|
||||
error: reasonMsg
|
||||
} as IframeErrorAnswerEvent, '*');
|
||||
});
|
||||
error: reasonMsg,
|
||||
} as IframeErrorAnswerEvent,
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
Promise.resolve(answerer(query.data, message.source))
|
||||
.then((value) => {
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
id: queryId,
|
||||
type: query.type,
|
||||
data: value,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
})
|
||||
.catch(errorHandler);
|
||||
} catch (reason) {
|
||||
errorHandler(reason);
|
||||
}
|
||||
} else if (isIframeEventWrapper(payload)) {
|
||||
if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
|
||||
this._showLayerStream.next(payload.data);
|
||||
@ -230,8 +254,6 @@ class IframeListener {
|
||||
this._removeBubbleStream.next();
|
||||
} else if (payload.type == "onPlayerMove") {
|
||||
this.sendPlayerMove = true;
|
||||
} else if (payload.type == "getDataLayer") {
|
||||
this._dataLayerChangeStream.next();
|
||||
} else if (isMenuItemRegisterIframeEvent(payload)) {
|
||||
const data = payload.data.menutItem;
|
||||
// @ts-ignore
|
||||
@ -248,13 +270,6 @@ class IframeListener {
|
||||
);
|
||||
}
|
||||
|
||||
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
|
||||
this.postMessage({
|
||||
type: "dataLayer",
|
||||
data: dataLayerEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the passed iFrame to send/receive messages via the API.
|
||||
*/
|
||||
@ -394,6 +409,13 @@ class IframeListener {
|
||||
});
|
||||
}
|
||||
|
||||
setVariable(setVariableEvent: SetVariableEvent) {
|
||||
this.postMessage({
|
||||
type: "setVariable",
|
||||
data: setVariableEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message... to all allowed iframes.
|
||||
*/
|
||||
@ -411,13 +433,31 @@ class IframeListener {
|
||||
* @param key The "type" of the query we are answering
|
||||
* @param callback
|
||||
*/
|
||||
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']> ): void {
|
||||
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: AnswererCallback<T>): void {
|
||||
this.answerers[key] = callback;
|
||||
}
|
||||
|
||||
public unregisterAnswerer(key: keyof IframeQueryMap): void {
|
||||
delete this.answerers[key];
|
||||
}
|
||||
|
||||
dispatchVariableToOtherIframes(key: string, value: unknown, source: MessageEventSource | null) {
|
||||
// Let's dispatch the message to the other iframes
|
||||
for (const iframe of this.iframes) {
|
||||
if (iframe.contentWindow !== source) {
|
||||
iframe.contentWindow?.postMessage(
|
||||
{
|
||||
type: "setVariable",
|
||||
data: {
|
||||
key,
|
||||
value,
|
||||
},
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const iframeListener = new IframeListener();
|
||||
|
@ -6,6 +6,24 @@ import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
||||
|
||||
const moveStream = new Subject<HasPlayerMovedEvent>();
|
||||
|
||||
let playerName: string | undefined;
|
||||
|
||||
export const setPlayerName = (name: string) => {
|
||||
playerName = name;
|
||||
};
|
||||
|
||||
let tags: string[] | undefined;
|
||||
|
||||
export const setTags = (_tags: string[]) => {
|
||||
tags = _tags;
|
||||
};
|
||||
|
||||
let uuid: string | undefined;
|
||||
|
||||
export const setUuid = (_uuid: string | undefined) => {
|
||||
uuid = _uuid;
|
||||
};
|
||||
|
||||
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
@ -24,6 +42,31 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
if (playerName === undefined) {
|
||||
throw new Error(
|
||||
"Player name not initialized yet. You should call WA.player.name within a WA.onInit callback."
|
||||
);
|
||||
}
|
||||
return playerName;
|
||||
}
|
||||
|
||||
get tags(): string[] {
|
||||
if (tags === undefined) {
|
||||
throw new Error("Tags not initialized yet. You should call WA.player.tags within a WA.onInit callback.");
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
get id(): string | undefined {
|
||||
// Note: this is not a type, we are checking if playerName is undefined because playerName cannot be undefined
|
||||
// while uuid could.
|
||||
if (playerName === undefined) {
|
||||
throw new Error("Player id not initialized yet. You should call WA.player.id within a WA.onInit callback.");
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventurePlayerCommands();
|
||||
|
@ -1,35 +1,14 @@
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { isDataLayerEvent } from "../Events/DataLayerEvent";
|
||||
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
||||
import { isGameStateEvent } from "../Events/GameStateEvent";
|
||||
|
||||
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
|
||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
||||
import type { DataLayerEvent } from "../Events/DataLayerEvent";
|
||||
import type { GameStateEvent } from "../Events/GameStateEvent";
|
||||
|
||||
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const dataLayerResolver = new Subject<DataLayerEvent>();
|
||||
const stateResolvers = new Subject<GameStateEvent>();
|
||||
|
||||
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
|
||||
|
||||
interface Room {
|
||||
id: string;
|
||||
mapUrl: string;
|
||||
map: ITiledMap;
|
||||
startLayer: string | null;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string | undefined;
|
||||
nickName: string | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface TileDescriptor {
|
||||
x: number;
|
||||
@ -38,19 +17,17 @@ interface TileDescriptor {
|
||||
layer: string;
|
||||
}
|
||||
|
||||
function getGameState(): Promise<GameStateEvent> {
|
||||
if (immutableDataPromise === undefined) {
|
||||
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
|
||||
}
|
||||
return immutableDataPromise;
|
||||
}
|
||||
let roomId: string | undefined;
|
||||
|
||||
function getDataLayer(): Promise<DataLayerEvent> {
|
||||
return new Promise<DataLayerEvent>((resolver, thrower) => {
|
||||
dataLayerResolver.subscribe(resolver);
|
||||
sendToWorkadventure({ type: "getDataLayer", data: null });
|
||||
});
|
||||
}
|
||||
export const setRoomId = (id: string) => {
|
||||
roomId = id;
|
||||
};
|
||||
|
||||
let mapURL: string | undefined;
|
||||
|
||||
export const setMapURL = (url: string) => {
|
||||
mapURL = url;
|
||||
};
|
||||
|
||||
export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
|
||||
callbacks = [
|
||||
@ -68,13 +45,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
||||
leaveStreams.get(payloadData.name)?.next();
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "dataLayer",
|
||||
typeChecker: isDataLayerEvent,
|
||||
callback: (payloadData) => {
|
||||
dataLayerResolver.next(payloadData);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
onEnterZone(name: string, callback: () => void): void {
|
||||
@ -109,22 +79,9 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
||||
},
|
||||
});
|
||||
}
|
||||
getCurrentRoom(): Promise<Room> {
|
||||
return getGameState().then((gameState) => {
|
||||
return getDataLayer().then((mapJson) => {
|
||||
return {
|
||||
id: gameState.roomId,
|
||||
map: mapJson.data as ITiledMap,
|
||||
mapUrl: gameState.mapUrl,
|
||||
startLayer: gameState.startLayerName,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
getCurrentUser(): Promise<User> {
|
||||
return getGameState().then((gameState) => {
|
||||
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
|
||||
});
|
||||
async getTiledMap(): Promise<ITiledMap> {
|
||||
const event = await queryWorkadventure({ type: "getMapData", data: undefined });
|
||||
return event.data as ITiledMap;
|
||||
}
|
||||
setTiles(tiles: TileDescriptor[]) {
|
||||
sendToWorkadventure({
|
||||
@ -132,6 +89,30 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
||||
data: tiles,
|
||||
});
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
if (roomId === undefined) {
|
||||
throw new Error("Room id not initialized yet. You should call WA.room.id within a WA.onInit callback.");
|
||||
}
|
||||
return roomId;
|
||||
}
|
||||
|
||||
get mapURL(): string {
|
||||
if (mapURL === undefined) {
|
||||
throw new Error(
|
||||
"mapURL is not initialized yet. You should call WA.room.mapURL within a WA.onInit callback."
|
||||
);
|
||||
}
|
||||
return mapURL;
|
||||
}
|
||||
async loadTileset(url: string): Promise<number> {
|
||||
return await queryWorkadventure({
|
||||
type: "loadTileset",
|
||||
data: {
|
||||
url: url,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventureRoomCommands();
|
||||
|
90
front/src/Api/iframe/state.ts
Normal file
90
front/src/Api/iframe/state.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
||||
|
||||
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent";
|
||||
|
||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
||||
|
||||
const setVariableResolvers = new Subject<SetVariableEvent>();
|
||||
const variables = new Map<string, unknown>();
|
||||
const variableSubscribers = new Map<string, Subject<unknown>>();
|
||||
|
||||
export const initVariables = (_variables: Map<string, unknown>): void => {
|
||||
for (const [name, value] of _variables.entries()) {
|
||||
// In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
|
||||
if (!variables.has(name)) {
|
||||
variables.set(name, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setVariableResolvers.subscribe((event) => {
|
||||
const oldValue = variables.get(event.key);
|
||||
// If we are setting the same value, no need to do anything.
|
||||
// No need to do this check since it is already performed in SharedVariablesManager
|
||||
/*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
|
||||
return;
|
||||
}*/
|
||||
|
||||
variables.set(event.key, event.value);
|
||||
const subject = variableSubscribers.get(event.key);
|
||||
if (subject !== undefined) {
|
||||
subject.next(event.value);
|
||||
}
|
||||
});
|
||||
|
||||
export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
type: "setVariable",
|
||||
typeChecker: isSetVariableEvent,
|
||||
callback: (payloadData) => {
|
||||
setVariableResolvers.next(payloadData);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
saveVariable(key: string, value: unknown): Promise<void> {
|
||||
variables.set(key, value);
|
||||
return queryWorkadventure({
|
||||
type: "setVariable",
|
||||
data: {
|
||||
key,
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
loadVariable(key: string): unknown {
|
||||
return variables.get(key);
|
||||
}
|
||||
|
||||
onVariableChange(key: string): Observable<unknown> {
|
||||
let subject = variableSubscribers.get(key);
|
||||
if (subject === undefined) {
|
||||
subject = new Subject<unknown>();
|
||||
variableSubscribers.set(key, subject);
|
||||
}
|
||||
return subject.asObservable();
|
||||
}
|
||||
}
|
||||
|
||||
const proxyCommand = new Proxy(new WorkadventureStateCommands(), {
|
||||
get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
|
||||
if (p in target) {
|
||||
return Reflect.get(target, p, receiver);
|
||||
}
|
||||
return target.loadVariable(p.toString());
|
||||
},
|
||||
set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
|
||||
// Note: when using "set", there is no way to wait, so we ignore the return of the promise.
|
||||
// User must use WA.state.saveVariable to have error message.
|
||||
target.saveVariable(p.toString(), value);
|
||||
return true;
|
||||
},
|
||||
}) as WorkadventureStateCommands & { [key: string]: unknown };
|
||||
|
||||
export default proxyCommand;
|
@ -30,6 +30,12 @@
|
||||
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
|
||||
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
|
||||
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
|
||||
import AdminMessage from "./TypeMessage/BanMessage.svelte";
|
||||
import TextMessage from "./TypeMessage/TextMessage.svelte";
|
||||
import {banMessageVisibleStore} from "../Stores/TypeMessageStore/BanMessageStore";
|
||||
import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore";
|
||||
import {warningContainerStore} from "../Stores/MenuStore";
|
||||
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
@ -61,6 +67,16 @@
|
||||
<EnableCameraScene game={game}></EnableCameraScene>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $banMessageVisibleStore}
|
||||
<div>
|
||||
<AdminMessage></AdminMessage>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $textMessageVisibleStore}
|
||||
<div>
|
||||
<TextMessage></TextMessage>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $soundPlayingStore}
|
||||
<div>
|
||||
<AudioPlaying url={$soundPlayingStore} />
|
||||
@ -88,7 +104,7 @@
|
||||
{/if}
|
||||
{#if $consoleGlobalMessageManagerVisibleStore}
|
||||
<div>
|
||||
<ConsoleGlobalMessageManager game={game}></ConsoleGlobalMessageManager>
|
||||
<ConsoleGlobalMessageManager></ConsoleGlobalMessageManager>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $helpCameraSettingsVisibleStore}
|
||||
@ -107,4 +123,7 @@
|
||||
{#if $chatVisibilityStore}
|
||||
<Chat></Chat>
|
||||
{/if}
|
||||
{#if $warningContainerStore}
|
||||
<WarningContainer></WarningContainer>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -3,9 +3,12 @@
|
||||
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
|
||||
import ChatMessageForm from './ChatMessageForm.svelte';
|
||||
import ChatElement from './ChatElement.svelte';
|
||||
import { afterUpdate, beforeUpdate } from "svelte";
|
||||
import {afterUpdate, beforeUpdate} from "svelte";
|
||||
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
||||
|
||||
let listDom: HTMLElement;
|
||||
let chatWindowElement: HTMLElement;
|
||||
let handleFormBlur: { blur():void };
|
||||
let autoscroll: boolean;
|
||||
|
||||
beforeUpdate(() => {
|
||||
@ -16,6 +19,12 @@
|
||||
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
|
||||
});
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
if (HtmlUtils.isClickedOutside(event, chatWindowElement)) {
|
||||
handleFormBlur.blur();
|
||||
}
|
||||
}
|
||||
|
||||
function closeChat() {
|
||||
chatVisibilityStore.set(false);
|
||||
}
|
||||
@ -26,37 +35,42 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown}/>
|
||||
<svelte:window on:keydown={onKeyDown} on:click={onClick}/>
|
||||
|
||||
|
||||
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}">
|
||||
<section class="chatWindowTitle">
|
||||
<h1>Your chat history <span class="float-right" on:click={closeChat}>×</span></h1>
|
||||
|
||||
</section>
|
||||
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}" bind:this={chatWindowElement}>
|
||||
<p class="close-icon" on:click={closeChat}>×</p>
|
||||
<section class="messagesList" bind:this={listDom}>
|
||||
<ul>
|
||||
<li><p class="system-text">Here is your chat history: </p></li>
|
||||
{#each $chatMessagesStore as message, i}
|
||||
<li><ChatElement message={message} line={i}></ChatElement></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
<section class="messageForm">
|
||||
<ChatMessageForm></ChatMessageForm>
|
||||
<ChatMessageForm bind:handleForm={handleFormBlur}></ChatMessageForm>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<style lang="scss">
|
||||
h1 {
|
||||
font-family: 'Whiteney';
|
||||
|
||||
span.float-right {
|
||||
p.close-icon {
|
||||
position: absolute;
|
||||
padding: 4px;
|
||||
right: 12px;
|
||||
font-size: 30px;
|
||||
line-height: 25px;
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
p.system-text {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding:6px;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
background: gray;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
aside.chatWindow {
|
||||
@ -78,16 +92,8 @@
|
||||
border-bottom-right-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
|
||||
h1 {
|
||||
background-color: #5f5f5f;
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.chatWindowTitle {
|
||||
flex: 0 100px;
|
||||
}
|
||||
.messagesList {
|
||||
margin-top: 35px;
|
||||
overflow-y: auto;
|
||||
flex: auto;
|
||||
|
||||
@ -98,7 +104,7 @@
|
||||
}
|
||||
.messageForm {
|
||||
flex: 0 70px;
|
||||
padding-top: 20px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -7,13 +7,14 @@
|
||||
|
||||
export let message: ChatMessage;
|
||||
export let line: number;
|
||||
const chatStyleLink = "color: white; text-decoration: underline;";
|
||||
|
||||
$: author = message.author as PlayerInterface;
|
||||
$: targets = message.targets || [];
|
||||
$: texts = message.text || [];
|
||||
|
||||
function urlifyText(text: string): string {
|
||||
return HtmlUtils.urlify(text)
|
||||
return HtmlUtils.urlify(text, chatStyleLink);
|
||||
}
|
||||
function renderDate(date: Date) {
|
||||
return date.toLocaleTimeString(navigator.language, {
|
||||
@ -29,7 +30,7 @@
|
||||
<div class="chatElement">
|
||||
<div class="messagePart">
|
||||
{#if message.type === ChatMessageTypes.userIncoming}
|
||||
>> {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} enter <span class="date">({renderDate(message.date)})</span>
|
||||
>> {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} entered <span class="date">({renderDate(message.date)})</span>
|
||||
{:else if message.type === ChatMessageTypes.userOutcoming}
|
||||
<< {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} left <span class="date">({renderDate(message.date)})</span>
|
||||
{:else if message.type === ChatMessageTypes.me}
|
||||
@ -48,7 +49,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
h4, p {
|
||||
font-family: 'Whiteney';
|
||||
font-family: Lato;
|
||||
}
|
||||
div.chatElement {
|
||||
display: flex;
|
||||
|
@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore";
|
||||
|
||||
export const handleForm = {
|
||||
blur() {
|
||||
inputElement.blur();
|
||||
}
|
||||
}
|
||||
let inputElement: HTMLElement;
|
||||
let newMessageText = '';
|
||||
|
||||
function onFocus() {
|
||||
@ -18,7 +24,7 @@
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={saveMessage}>
|
||||
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} >
|
||||
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} bind:this={inputElement}>
|
||||
<button type="submit">
|
||||
<img src="/static/images/send.png" alt="Send" width="20">
|
||||
</button>
|
||||
@ -32,26 +38,25 @@
|
||||
|
||||
input {
|
||||
flex: auto;
|
||||
background-color: #42464d;
|
||||
background-color: #254560;
|
||||
color: white;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
border: none;
|
||||
font-size: 22px;
|
||||
font-family: Whiteney;
|
||||
font-family: Lato;
|
||||
padding-left: 6px;
|
||||
min-width: 0; //Needed so that the input doesn't overflow the container in firefox
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #42464d;
|
||||
color: white;
|
||||
background-color: #254560;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border: none;
|
||||
border-left: solid black 1px;
|
||||
border-left: solid white 1px;
|
||||
font-size: 16px;
|
||||
font-family: Whiteney;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,11 +1,25 @@
|
||||
<script lang="typescript">
|
||||
import { fly } from 'svelte/transition';
|
||||
import InputTextGlobalMessage from "./InputTextGlobalMessage.svelte";
|
||||
import UploadAudioGlobalMessage from "./UploadAudioGlobalMessage.svelte";
|
||||
import {gameManager} from "../../Phaser/Game/GameManager";
|
||||
import type {Game} from "../../Phaser/Game/Game";
|
||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||
|
||||
let inputSendTextActive = true;
|
||||
let uploadMusicActive = false;
|
||||
let handleSendText: { sendTextMessage(broadcast: boolean): void };
|
||||
let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
|
||||
let broadcastToWorld = false;
|
||||
|
||||
function closeConsoleGlobalMessage() {
|
||||
consoleGlobalMessageManagerVisibleStore.set(false)
|
||||
}
|
||||
|
||||
function onKeyDown(e:KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
closeConsoleGlobalMessage();
|
||||
}
|
||||
}
|
||||
|
||||
function inputSendTextActivate() {
|
||||
inputSendTextActive = true;
|
||||
@ -16,28 +30,121 @@
|
||||
uploadMusicActive = true;
|
||||
inputSendTextActive = false;
|
||||
}
|
||||
|
||||
function send() {
|
||||
if (inputSendTextActive) {
|
||||
handleSendText.sendTextMessage(broadcastToWorld);
|
||||
}
|
||||
if (uploadMusicActive) {
|
||||
handleSendAudio.sendAudioMessage(broadcastToWorld);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown}/>
|
||||
|
||||
<div class="main-console nes-container is-rounded">
|
||||
<!-- <div class="console nes-container is-rounded">
|
||||
<img class="btn-close" src="resources/logos/send-yellow.svg" alt="Close">
|
||||
</div>-->
|
||||
<div class="main-global-message">
|
||||
<h2> Global Message </h2>
|
||||
<div class="global-message">
|
||||
<div class="menu">
|
||||
<button class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={inputSendTextActivate}>Message</button>
|
||||
<button class="nes-btn {uploadMusicActive ? 'is-disabled' : ''}" on:click|preventDefault={inputUploadMusicActivate}>Audio</button>
|
||||
<div class="console-global-message">
|
||||
<div class="menu-console-global-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
|
||||
<button type="button" class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={inputSendTextActivate}>Message</button>
|
||||
<button type="button" class="nes-btn {uploadMusicActive ? 'is-disabled' : ''}" on:click|preventDefault={inputUploadMusicActivate}>Audio</button>
|
||||
</div>
|
||||
<div class="main-input">
|
||||
<div class="main-console-global-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
|
||||
<div class="title-console-global-message">
|
||||
<h2>Global Message</h2>
|
||||
<button type="button" class="nes-btn is-error" on:click|preventDefault={closeConsoleGlobalMessage}><i class="nes-icon close is-small"></i></button>
|
||||
</div>
|
||||
<div class="content-console-global-message">
|
||||
{#if inputSendTextActive}
|
||||
<InputTextGlobalMessage gameManager={gameManager}></InputTextGlobalMessage>
|
||||
<InputTextGlobalMessage gameManager={gameManager} bind:handleSending={handleSendText}/>
|
||||
{/if}
|
||||
{#if uploadMusicActive}
|
||||
<UploadAudioGlobalMessage gameManager={gameManager}></UploadAudioGlobalMessage>
|
||||
<UploadAudioGlobalMessage gameManager={gameManager} bind:handleSending={handleSendAudio}/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="footer-console-global-message">
|
||||
<label>
|
||||
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld}>
|
||||
<span>Broadcast to all rooms of the world</span>
|
||||
</label>
|
||||
<button class="nes-btn is-primary" on:click|preventDefault={send}>Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.nes-container {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
div.console-global-message {
|
||||
top: 20vh;
|
||||
width: 50vw;
|
||||
height: 50vh;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0;
|
||||
|
||||
pointer-events: auto;
|
||||
|
||||
div.menu-console-global-message {
|
||||
flex: 1 1 auto;
|
||||
max-width: 180px;
|
||||
|
||||
text-align: center;
|
||||
background-color: #333333;
|
||||
|
||||
button {
|
||||
width: 136px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
div.main-console-global-message {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: #333333;
|
||||
|
||||
div.title-console-global-message {
|
||||
flex: 0 0 auto;
|
||||
height: 50px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
text-align: center;
|
||||
color: whitesmoke;
|
||||
|
||||
.nes-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
div.content-console-global-message {
|
||||
flex: 1 1 auto;
|
||||
max-height: calc(100% - 120px);
|
||||
}
|
||||
|
||||
div.footer-console-global-message {
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
max-width: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import {consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||
import {onMount} from "svelte";
|
||||
import type {GameManager} from "../../Phaser/Game/GameManager";
|
||||
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
|
||||
import {AdminMessageEventTypes} from "../../Connexion/AdminMessagesService";
|
||||
import type {Quill} from "quill";
|
||||
import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||
import {onDestroy, onMount} from "svelte";
|
||||
import type { GameManager } from "../../Phaser/Game/GameManager";
|
||||
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
|
||||
import type { Quill } from "quill";
|
||||
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
|
||||
|
||||
//toolbar
|
||||
export const toolbarOptions = [
|
||||
const toolbarOptions = [
|
||||
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
||||
['blockquote', 'code-block'],
|
||||
|
||||
@ -32,12 +32,31 @@
|
||||
|
||||
export let gameManager: GameManager;
|
||||
|
||||
let gameScene = gameManager.getCurrentGameScene();
|
||||
const gameScene = gameManager.getCurrentGameScene();
|
||||
let quill: Quill;
|
||||
let INPUT_CONSOLE_MESSAGE: HTMLDivElement;
|
||||
|
||||
const MESSAGE_TYPE = AdminMessageEventTypes.admin;
|
||||
|
||||
export const handleSending = {
|
||||
sendTextMessage(broadcastToWorld: boolean) {
|
||||
if (gameScene == undefined) {
|
||||
return;
|
||||
}
|
||||
const text = JSON.stringify(quill.getContents(0, quill.getLength()));
|
||||
|
||||
const textGlobalMessage: PlayGlobalMessageInterface = {
|
||||
type: MESSAGE_TYPE,
|
||||
content: text,
|
||||
broadcastToWorld: broadcastToWorld
|
||||
};
|
||||
|
||||
quill.deleteText(0, quill.getLength());
|
||||
gameScene.connection?.emitGlobalMessage(textGlobalMessage);
|
||||
disableConsole();
|
||||
}
|
||||
}
|
||||
|
||||
//Quill
|
||||
onMount(async () => {
|
||||
|
||||
@ -45,49 +64,28 @@
|
||||
const {default: Quill} = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
quill = new Quill(INPUT_CONSOLE_MESSAGE, {
|
||||
placeholder: 'Enter your message here...',
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: toolbarOptions
|
||||
},
|
||||
});
|
||||
|
||||
quill.on('selection-change', function (range, oldRange) {
|
||||
if (range === null && oldRange !== null) {
|
||||
consoleGlobalMessageManagerFocusStore.set(false);
|
||||
} else if (range !== null && oldRange === null)
|
||||
consoleGlobalMessageManagerFocusStore.set(true);
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
consoleGlobalMessageManagerFocusStore.set(false);
|
||||
})
|
||||
|
||||
function disableConsole() {
|
||||
consoleGlobalMessageManagerVisibleStore.set(false);
|
||||
consoleGlobalMessageManagerFocusStore.set(false);
|
||||
}
|
||||
|
||||
function SendTextMessage() {
|
||||
if (gameScene == undefined) {
|
||||
return;
|
||||
}
|
||||
const text = quill.getText(0, quill.getLength());
|
||||
|
||||
const GlobalMessage: PlayGlobalMessageInterface = {
|
||||
id: "1", // FIXME: use another ID?
|
||||
message: text,
|
||||
type: MESSAGE_TYPE
|
||||
};
|
||||
|
||||
quill.deleteText(0, quill.getLength());
|
||||
gameScene.connection?.emitGlobalMessage(GlobalMessage);
|
||||
disableConsole();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<section class="section-input-send-text">
|
||||
<div class="input-send-text" bind:this={INPUT_CONSOLE_MESSAGE}></div>
|
||||
<div class="btn-action">
|
||||
<button class="nes-btn is-primary" on:click|preventDefault={SendTextMessage}>Send</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
||||
import type {GameManager} from "../../Phaser/Game/GameManager";
|
||||
import {consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||
import {AdminMessageEventTypes} from "../../Connexion/AdminMessagesService";
|
||||
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
|
||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||
import type { GameManager } from "../../Phaser/Game/GameManager";
|
||||
import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
|
||||
import uploadFile from "../images/music-file.svg";
|
||||
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
|
||||
|
||||
interface EventTargetFiles extends EventTarget {
|
||||
files: Array<File>;
|
||||
@ -13,22 +13,22 @@
|
||||
export let gameManager: GameManager;
|
||||
|
||||
let gameScene = gameManager.getCurrentGameScene();
|
||||
let fileinput: HTMLInputElement;
|
||||
let filename: string;
|
||||
let filesize: string;
|
||||
let errorfile: boolean;
|
||||
let fileInput: HTMLInputElement;
|
||||
let fileName: string;
|
||||
let fileSize: string;
|
||||
let errorFile: boolean;
|
||||
|
||||
const AUDIO_TYPE = AdminMessageEventTypes.audio;
|
||||
|
||||
|
||||
async function SendAudioMessage() {
|
||||
export const handleSending = {
|
||||
async sendAudioMessage(broadcast: boolean) {
|
||||
if (gameScene == undefined) {
|
||||
return;
|
||||
}
|
||||
const inputAudio = HtmlUtils.getElementByIdOrFail<HTMLInputElement>("input-send-audio");
|
||||
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
|
||||
if (!selectedFile) {
|
||||
errorfile = true;
|
||||
errorFile = true;
|
||||
throw 'no file selected';
|
||||
}
|
||||
|
||||
@ -36,15 +36,16 @@
|
||||
fd.append('file', selectedFile);
|
||||
const res = await gameScene.connection?.uploadAudio(fd);
|
||||
|
||||
const GlobalMessage: PlayGlobalMessageInterface = {
|
||||
id: (res as { id: string }).id,
|
||||
message: (res as { path: string }).path,
|
||||
type: AUDIO_TYPE
|
||||
const audioGlobalMessage: PlayGlobalMessageInterface = {
|
||||
content: (res as { path: string }).path,
|
||||
type: AUDIO_TYPE,
|
||||
broadcastToWorld: broadcast
|
||||
}
|
||||
inputAudio.value = '';
|
||||
gameScene.connection?.emitGlobalMessage(GlobalMessage);
|
||||
gameScene.connection?.emitGlobalMessage(audioGlobalMessage);
|
||||
disableConsole();
|
||||
}
|
||||
}
|
||||
|
||||
function inputAudioFile(event: Event) {
|
||||
const eventTarget : EventTargetFiles = (event.target as EventTargetFiles);
|
||||
@ -57,9 +58,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
filename = file.name;
|
||||
filesize = getFileSize(file.size);
|
||||
errorfile = false;
|
||||
fileName = file.name;
|
||||
fileSize = getFileSize(file.size);
|
||||
errorFile = false;
|
||||
}
|
||||
|
||||
function getFileSize(number: number) {
|
||||
@ -82,46 +83,46 @@
|
||||
|
||||
|
||||
<section class="section-input-send-audio">
|
||||
<div class="input-send-audio">
|
||||
<img src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileinput.click();}}>
|
||||
{#if filename != undefined}
|
||||
<label for="input-send-audio">{filename} : {filesize}</label>
|
||||
<img class="nes-pointer" src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileInput.click();}}>
|
||||
{#if fileName !== undefined}
|
||||
<p>{fileName} : {fileSize}</p>
|
||||
{/if}
|
||||
{#if errorfile}
|
||||
{#if errorFile}
|
||||
<p class="err">No file selected. You need to upload a file before sending it.</p>
|
||||
{/if}
|
||||
<input type="file" id="input-send-audio" bind:this={fileinput} on:change={(e) => {inputAudioFile(e)}}>
|
||||
</div>
|
||||
<div class="btn-action">
|
||||
<button class="nes-btn is-primary" on:click|preventDefault={SendAudioMessage}>Send</button>
|
||||
</div>
|
||||
<input type="file" id="input-send-audio" bind:this={fileInput} on:change={(e) => {inputAudioFile(e)}}>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
//UploadAudioGlobalMessage
|
||||
.section-input-send-audio {
|
||||
margin: 10px;
|
||||
}
|
||||
section.section-input-send-audio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.section-input-send-audio .input-send-audio {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
flex: 1 1 auto;
|
||||
|
||||
max-height: 80%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-input-send-audio #input-send-audio{
|
||||
p {
|
||||
flex: 1 1 auto;
|
||||
|
||||
margin-bottom: 5px;
|
||||
|
||||
color: whitesmoke;
|
||||
font-size: 1rem;
|
||||
|
||||
&.err {
|
||||
color: #ce372b;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section-input-send-audio div.input-send-audio label{
|
||||
color: white;
|
||||
}
|
||||
|
||||
.section-input-send-audio div.input-send-audio p.err {
|
||||
color: #ce372b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-input-send-audio div.input-send-audio img{
|
||||
height: 150px;
|
||||
cursor: url('../../../style/images/cursor_pointer.png'), pointer;
|
||||
}
|
||||
</style>
|
96
front/src/Components/TypeMessage/BanMessage.svelte
Normal file
96
front/src/Components/TypeMessage/BanMessage.svelte
Normal file
@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { fly } from "svelte/transition";
|
||||
import {banMessageVisibleStore, banMessageContentStore} from "../../Stores/TypeMessageStore/BanMessageStore";
|
||||
import {onMount} from "svelte";
|
||||
|
||||
const text = $banMessageContentStore;
|
||||
const NAME_BUTTON = 'Ok';
|
||||
let nbSeconds = 10;
|
||||
let nameButton = '';
|
||||
|
||||
onMount(() => {
|
||||
timeToRead()
|
||||
})
|
||||
|
||||
function timeToRead() {
|
||||
nbSeconds -= 1;
|
||||
nameButton = nbSeconds.toString();
|
||||
if ( nbSeconds > 0 ) {
|
||||
setTimeout( () => {
|
||||
timeToRead();
|
||||
}, 1000);
|
||||
} else {
|
||||
nameButton = NAME_BUTTON;
|
||||
}
|
||||
}
|
||||
|
||||
function closeBanMessage() {
|
||||
banMessageVisibleStore.set(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="main-ban-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
|
||||
<h2 class="title-ban-message"><img src="resources/logos/report.svg" alt="***"/> Important message <img src="resources/logos/report.svg" alt="***"/></h2>
|
||||
<div class="content-ban-message">
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
<div class="footer-ban-message">
|
||||
<button type="button" class="nes-btn {nameButton === NAME_BUTTON ? 'is-primary' : 'is-error'}" disabled="{!(nameButton === NAME_BUTTON)}" on:click|preventDefault={closeBanMessage}>{nameButton}</button>
|
||||
</div>
|
||||
<audio id="report-message" autoplay>
|
||||
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
div.main-ban-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
top: 15vh;
|
||||
|
||||
height: 70vh;
|
||||
width: 60vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-bottom: 0;
|
||||
|
||||
pointer-events: auto;
|
||||
background-color: #333333;
|
||||
color: whitesmoke;
|
||||
|
||||
h2.title-ban-message {
|
||||
flex: 1 1 auto;
|
||||
max-height: 50px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
div.content-ban-message {
|
||||
flex: 1 1 auto;
|
||||
max-height: calc(100% - 50px);
|
||||
overflow: auto;
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
div.footer-ban-message {
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
|
||||
button {
|
||||
width: 88px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
59
front/src/Components/TypeMessage/TextMessage.svelte
Normal file
59
front/src/Components/TypeMessage/TextMessage.svelte
Normal file
@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { fly } from "svelte/transition";
|
||||
import {textMessageContentStore, textMessageVisibleStore} from "../../Stores/TypeMessageStore/TextMessageStore";
|
||||
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
|
||||
|
||||
const content = JSON.parse($textMessageContentStore);
|
||||
const converter = new QuillDeltaToHtmlConverter(content.ops, {inlineStyles: true});
|
||||
const NAME_BUTTON = 'Ok';
|
||||
|
||||
function closeTextMessage() {
|
||||
textMessageVisibleStore.set(false);
|
||||
}
|
||||
|
||||
function onKeyDown(e:KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
closeTextMessage();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown}/>
|
||||
|
||||
<div class="main-text-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
|
||||
<div class="content-text-message">
|
||||
{@html converter.convert()}
|
||||
</div>
|
||||
<div class="footer-text-message">
|
||||
<button type="button" class="nes-btn is-primary" on:click|preventDefault={closeTextMessage}>{NAME_BUTTON}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div.main-text-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-height: 25vh;
|
||||
width: 80vw;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding-bottom: 0;
|
||||
|
||||
pointer-events: auto;
|
||||
background-color: #333333;
|
||||
|
||||
div.content-text-message {
|
||||
flex: 1 1 auto;
|
||||
max-height: calc(100% - 50px);
|
||||
color: whitesmoke;
|
||||
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div.footer-text-message {
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -37,9 +37,7 @@
|
||||
<img alt="Report this user" src={reportImg}>
|
||||
<span>Report/Block</span>
|
||||
</button>
|
||||
{#if $streamStore }
|
||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
{/if}
|
||||
<img src={blockSignImg} class="block-logo" alt="Block" />
|
||||
{#if $constraintStore && $constraintStore.audio !== false}
|
||||
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
|
||||
|
@ -1,3 +1,6 @@
|
||||
import type { UserSimplePeerInterface } from "../../WebRtc/SimplePeer";
|
||||
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../../Enum/EnvironmentVariable";
|
||||
|
||||
export function getColorByString(str: string): string | null {
|
||||
let hash = 0;
|
||||
if (str.length === 0) {
|
||||
@ -15,7 +18,7 @@ export function getColorByString(str: string): string | null {
|
||||
return color;
|
||||
}
|
||||
|
||||
export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
||||
export function srcObject(node: HTMLVideoElement, stream: MediaStream | null) {
|
||||
node.srcObject = stream;
|
||||
return {
|
||||
update(newStream: MediaStream) {
|
||||
@ -25,3 +28,19 @@ export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getIceServersConfig(user: UserSimplePeerInterface): RTCIceServer[] {
|
||||
const config: RTCIceServer[] = [
|
||||
{
|
||||
urls: STUN_SERVER.split(","),
|
||||
},
|
||||
];
|
||||
if (TURN_SERVER !== "") {
|
||||
config.push({
|
||||
urls: TURN_SERVER.split(","),
|
||||
username: user.webRtcUser || TURN_USER,
|
||||
credential: user.webRtcPassword || TURN_PASSWORD,
|
||||
});
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
@ -45,8 +45,9 @@
|
||||
|
||||
.visitCard {
|
||||
pointer-events: all;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
margin-top: 200px;
|
||||
max-width: 80vw;
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
<script lang="typescript">
|
||||
import { fly } from 'svelte/transition';
|
||||
import {userIsAdminStore} from "../../Stores/GameStore";
|
||||
import {ADMIN_URL} from "../../Enum/EnvironmentVariable";
|
||||
|
||||
const upgradeLink = ADMIN_URL+'/pricing';
|
||||
|
||||
</script>
|
||||
|
||||
<main class="warningMain" transition:fly="{{ y: -200, duration: 500 }}">
|
||||
<h2>Warning!</h2>
|
||||
{#if $userIsAdminStore}
|
||||
<p>This world is close to its limit!. You can upgrade its capacity <a href="{upgradeLink}" target="_blank">here</a></p>
|
||||
{:else}
|
||||
<p>This world is close to its limit!</p>
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
|
||||
<style lang="scss">
|
||||
main.warningMain {
|
||||
pointer-events: auto;
|
||||
width: 100vw;
|
||||
background-color: red;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
font-family: Lato;
|
||||
min-width: 300px;
|
||||
opacity: 0.9;
|
||||
z-index: 2;
|
||||
h2 {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,149 +1,218 @@
|
||||
import Axios from "axios";
|
||||
import {PUSHER_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable";
|
||||
import {RoomConnection} from "./RoomConnection";
|
||||
import type {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
|
||||
import {GameConnexionTypes, urlManager} from "../Url/UrlManager";
|
||||
import {localUserStore} from "./LocalUserStore";
|
||||
import {CharacterTexture, LocalUser} from "./LocalUser";
|
||||
import {Room} from "./Room";
|
||||
|
||||
import { PUSHER_URL, START_ROOM_URL } from "../Enum/EnvironmentVariable";
|
||||
import { RoomConnection } from "./RoomConnection";
|
||||
import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels";
|
||||
import { GameConnexionTypes, urlManager } from "../Url/UrlManager";
|
||||
import { localUserStore } from "./LocalUserStore";
|
||||
import { CharacterTexture, LocalUser } from "./LocalUser";
|
||||
import { Room } from "./Room";
|
||||
import { _ServiceWorker } from "../Network/ServiceWorker";
|
||||
|
||||
class ConnectionManager {
|
||||
private localUser!:LocalUser;
|
||||
private localUser!: LocalUser;
|
||||
|
||||
private connexionType?: GameConnexionTypes
|
||||
private reconnectingTimeout: NodeJS.Timeout|null = null;
|
||||
private _unloading:boolean = false;
|
||||
private connexionType?: GameConnexionTypes;
|
||||
private reconnectingTimeout: NodeJS.Timeout | null = null;
|
||||
private _unloading: boolean = false;
|
||||
private authToken: string | null = null;
|
||||
|
||||
get unloading () {
|
||||
private serviceWorker?: _ServiceWorker;
|
||||
|
||||
get unloading() {
|
||||
return this._unloading;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
window.addEventListener('beforeunload', () => {
|
||||
window.addEventListener("beforeunload", () => {
|
||||
this._unloading = true;
|
||||
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout)
|
||||
})
|
||||
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
public loadOpenIDScreen() {
|
||||
localUserStore.setAuthToken(null);
|
||||
const state = localUserStore.generateState();
|
||||
const nonce = localUserStore.generateNonce();
|
||||
window.location.assign(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`);
|
||||
}
|
||||
|
||||
public logout() {
|
||||
localUserStore.setAuthToken(null);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to login to the node server and return the starting map url to be loaded
|
||||
*/
|
||||
public async initGameConnexion(): Promise<Room> {
|
||||
|
||||
const connexionType = urlManager.getGameConnexionType();
|
||||
this.connexionType = connexionType;
|
||||
if(connexionType === GameConnexionTypes.register) {
|
||||
const organizationMemberToken = urlManager.getOrganizationToken();
|
||||
const data = await Axios.post(`${PUSHER_URL}/register`, {organizationMemberToken}).then(res => res.data);
|
||||
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
|
||||
localUserStore.saveUser(this.localUser);
|
||||
|
||||
const organizationSlug = data.organizationSlug;
|
||||
const worldSlug = data.worldSlug;
|
||||
const roomSlug = data.roomSlug;
|
||||
|
||||
const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.search + window.location.hash);
|
||||
let room: Room | null = null;
|
||||
if (connexionType === GameConnexionTypes.jwt) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get("code");
|
||||
const state = urlParams.get("state");
|
||||
if (!state || !localUserStore.verifyState(state)) {
|
||||
throw "Could not validate state!";
|
||||
}
|
||||
if (!code) {
|
||||
throw "No Auth code provided";
|
||||
}
|
||||
const nonce = localUserStore.getNonce();
|
||||
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce } }).then(
|
||||
(res) => res.data
|
||||
);
|
||||
localUserStore.setAuthToken(authToken);
|
||||
this.authToken = authToken;
|
||||
room = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
|
||||
urlManager.pushRoomIdToUrl(room);
|
||||
return Promise.resolve(room);
|
||||
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
|
||||
} else if (connexionType === GameConnexionTypes.register) {
|
||||
//@deprecated
|
||||
const organizationMemberToken = urlManager.getOrganizationToken();
|
||||
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
|
||||
(res) => res.data
|
||||
);
|
||||
this.localUser = new LocalUser(data.userUuid, data.textures);
|
||||
this.authToken = data.authToken;
|
||||
localUserStore.saveUser(this.localUser);
|
||||
localUserStore.setAuthToken(this.authToken);
|
||||
|
||||
let localUser = localUserStore.getLocalUser();
|
||||
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
|
||||
this.localUser = localUser;
|
||||
try {
|
||||
await this.verifyToken(localUser.jwtToken);
|
||||
} catch(e) {
|
||||
// If the token is invalid, let's generate an anonymous one.
|
||||
console.error('JWT token invalid. Did it expire? Login anonymously instead.');
|
||||
await this.anonymousLogin();
|
||||
}
|
||||
}else{
|
||||
const roomUrl = data.roomUrl;
|
||||
|
||||
room = await Room.createRoom(
|
||||
new URL(
|
||||
window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
roomUrl +
|
||||
window.location.search +
|
||||
window.location.hash
|
||||
)
|
||||
);
|
||||
urlManager.pushRoomIdToUrl(room);
|
||||
} else if (
|
||||
connexionType === GameConnexionTypes.organization ||
|
||||
connexionType === GameConnexionTypes.anonymous ||
|
||||
connexionType === GameConnexionTypes.empty
|
||||
) {
|
||||
this.authToken = localUserStore.getAuthToken();
|
||||
//todo: add here some kind of warning if authToken has expired.
|
||||
if (!this.authToken) {
|
||||
await this.anonymousLogin();
|
||||
}
|
||||
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
|
||||
|
||||
localUser = localUserStore.getLocalUser();
|
||||
if(!localUser){
|
||||
throw "Error to store local user data";
|
||||
}
|
||||
|
||||
let roomId: string;
|
||||
let roomPath: string;
|
||||
if (connexionType === GameConnexionTypes.empty) {
|
||||
roomId = START_ROOM_URL;
|
||||
roomPath = window.location.protocol + "//" + window.location.host + START_ROOM_URL;
|
||||
} else {
|
||||
roomId = window.location.pathname + window.location.search + window.location.hash;
|
||||
roomPath =
|
||||
window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
window.location.pathname +
|
||||
window.location.search +
|
||||
window.location.hash;
|
||||
}
|
||||
|
||||
//get detail map for anonymous login and set texture in local storage
|
||||
const room = new Room(roomId);
|
||||
const mapDetail = await room.getMapDetail();
|
||||
if(mapDetail.textures != undefined && mapDetail.textures.length > 0) {
|
||||
room = await Room.createRoom(new URL(roomPath));
|
||||
if (room.textures != undefined && room.textures.length > 0) {
|
||||
//check if texture was changed
|
||||
if(localUser.textures.length === 0){
|
||||
localUser.textures = mapDetail.textures;
|
||||
}else{
|
||||
mapDetail.textures.forEach((newTexture) => {
|
||||
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
||||
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
|
||||
if (this.localUser.textures.length === 0) {
|
||||
this.localUser.textures = room.textures;
|
||||
} else {
|
||||
room.textures.forEach((newTexture) => {
|
||||
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
|
||||
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
|
||||
return;
|
||||
}
|
||||
localUser?.textures.push(newTexture)
|
||||
this.localUser.textures.push(newTexture);
|
||||
});
|
||||
}
|
||||
this.localUser = localUser;
|
||||
localUserStore.saveUser(localUser);
|
||||
localUserStore.saveUser(this.localUser);
|
||||
}
|
||||
}
|
||||
if (room == undefined) {
|
||||
return Promise.reject(new Error("Invalid URL"));
|
||||
}
|
||||
|
||||
this.serviceWorker = new _ServiceWorker();
|
||||
return Promise.resolve(room);
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('Invalid URL'));
|
||||
}
|
||||
|
||||
private async verifyToken(token: string): Promise<void> {
|
||||
await Axios.get(`${PUSHER_URL}/verify`, {params: {token}});
|
||||
}
|
||||
|
||||
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
|
||||
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then(res => res.data);
|
||||
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
|
||||
if (!isBenchmark) { // In benchmark, we don't have a local storage.
|
||||
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
|
||||
this.localUser = new LocalUser(data.userUuid, []);
|
||||
this.authToken = data.authToken;
|
||||
if (!isBenchmark) {
|
||||
// In benchmark, we don't have a local storage.
|
||||
localUserStore.saveUser(this.localUser);
|
||||
localUserStore.setAuthToken(this.authToken);
|
||||
}
|
||||
}
|
||||
|
||||
public initBenchmark(): void {
|
||||
this.localUser = new LocalUser('', 'test', []);
|
||||
this.localUser = new LocalUser("", []);
|
||||
}
|
||||
|
||||
public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
|
||||
public connectToRoomSocket(
|
||||
roomUrl: string,
|
||||
name: string,
|
||||
characterLayers: string[],
|
||||
position: PositionInterface,
|
||||
viewport: ViewportInterface,
|
||||
companion: string | null
|
||||
): Promise<OnConnectInterface> {
|
||||
return new Promise<OnConnectInterface>((resolve, reject) => {
|
||||
const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport, companion);
|
||||
const connection = new RoomConnection(
|
||||
this.authToken,
|
||||
roomUrl,
|
||||
name,
|
||||
characterLayers,
|
||||
position,
|
||||
viewport,
|
||||
companion
|
||||
);
|
||||
|
||||
connection.onConnectError((error: object) => {
|
||||
console.log('An error occurred while connecting to socket server. Retrying');
|
||||
console.log("An error occurred while connecting to socket server. Retrying");
|
||||
reject(error);
|
||||
});
|
||||
|
||||
connection.onConnectingError((event: CloseEvent) => {
|
||||
console.log('An error occurred while connecting to socket server. Retrying');
|
||||
reject(new Error('An error occurred while connecting to socket server. Retrying. Code: '+event.code+', Reason: '+event.reason));
|
||||
console.log("An error occurred while connecting to socket server. Retrying");
|
||||
reject(
|
||||
new Error(
|
||||
"An error occurred while connecting to socket server. Retrying. Code: " +
|
||||
event.code +
|
||||
", Reason: " +
|
||||
event.reason
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
connection.onConnect((connect: OnConnectInterface) => {
|
||||
//save last room url connected
|
||||
localUserStore.setLastRoomUrl(roomUrl);
|
||||
|
||||
resolve(connect);
|
||||
});
|
||||
|
||||
}).catch((err) => {
|
||||
// Let's retry in 4-6 seconds
|
||||
return new Promise<OnConnectInterface>((resolve, reject) => {
|
||||
this.reconnectingTimeout = setTimeout(() => {
|
||||
//todo: allow a way to break recursion?
|
||||
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
|
||||
this.connectToRoomSocket(roomId, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
|
||||
}, 4000 + Math.floor(Math.random() * 2000) );
|
||||
this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then(
|
||||
(connection) => resolve(connection)
|
||||
);
|
||||
}, 4000 + Math.floor(Math.random() * 2000));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get getConnexionType(){
|
||||
get getConnexionType() {
|
||||
return this.connexionType;
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ export enum EventMessage {
|
||||
TELEPORT = "teleport",
|
||||
USER_MESSAGE = "user-message",
|
||||
START_JITSI_ROOM = "start-jitsi-room",
|
||||
SET_VARIABLE = "set-variable",
|
||||
}
|
||||
|
||||
export interface PointInterface {
|
||||
@ -105,12 +106,13 @@ export interface RoomJoinedMessageInterface {
|
||||
//users: MessageUserPositionInterface[],
|
||||
//groups: GroupCreatedUpdatedMessageInterface[],
|
||||
items: { [itemId: number]: unknown };
|
||||
variables: Map<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PlayGlobalMessageInterface {
|
||||
id: string;
|
||||
type: string;
|
||||
message: string;
|
||||
content: string;
|
||||
broadcastToWorld: boolean;
|
||||
}
|
||||
|
||||
export interface OnConnectInterface {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {MAX_USERNAME_LENGTH} from "../Enum/EnvironmentVariable";
|
||||
import { MAX_USERNAME_LENGTH } from "../Enum/EnvironmentVariable";
|
||||
|
||||
export interface CharacterTexture {
|
||||
id: number,
|
||||
level: number,
|
||||
url: string,
|
||||
rights: string
|
||||
id: number;
|
||||
level: number;
|
||||
url: string;
|
||||
rights: string;
|
||||
}
|
||||
|
||||
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
|
||||
@ -24,6 +24,5 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
|
||||
}
|
||||
|
||||
export class LocalUser {
|
||||
constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) {
|
||||
}
|
||||
constructor(public readonly uuid: string, public textures: CharacterTexture[]) {}
|
||||
}
|
||||
|
@ -1,60 +1,65 @@
|
||||
import {areCharacterLayersValid, isUserNameValid, LocalUser} from "./LocalUser";
|
||||
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const playerNameKey = 'playerName';
|
||||
const selectedPlayerKey = 'selectedPlayer';
|
||||
const customCursorPositionKey = 'customCursorPosition';
|
||||
const characterLayersKey = 'characterLayers';
|
||||
const companionKey = 'companion';
|
||||
const gameQualityKey = 'gameQuality';
|
||||
const videoQualityKey = 'videoQuality';
|
||||
const audioPlayerVolumeKey = 'audioVolume';
|
||||
const audioPlayerMuteKey = 'audioMute';
|
||||
const helpCameraSettingsShown = 'helpCameraSettingsShown';
|
||||
const fullscreenKey = 'fullscreen';
|
||||
const playerNameKey = "playerName";
|
||||
const selectedPlayerKey = "selectedPlayer";
|
||||
const customCursorPositionKey = "customCursorPosition";
|
||||
const characterLayersKey = "characterLayers";
|
||||
const companionKey = "companion";
|
||||
const gameQualityKey = "gameQuality";
|
||||
const videoQualityKey = "videoQuality";
|
||||
const audioPlayerVolumeKey = "audioVolume";
|
||||
const audioPlayerMuteKey = "audioMute";
|
||||
const helpCameraSettingsShown = "helpCameraSettingsShown";
|
||||
const fullscreenKey = "fullscreen";
|
||||
const lastRoomUrl = "lastRoomUrl";
|
||||
const authToken = "authToken";
|
||||
const state = "state";
|
||||
const nonce = "nonce";
|
||||
|
||||
class LocalUserStore {
|
||||
saveUser(localUser: LocalUser) {
|
||||
localStorage.setItem('localUser', JSON.stringify(localUser));
|
||||
localStorage.setItem("localUser", JSON.stringify(localUser));
|
||||
}
|
||||
getLocalUser(): LocalUser|null {
|
||||
const data = localStorage.getItem('localUser');
|
||||
getLocalUser(): LocalUser | null {
|
||||
const data = localStorage.getItem("localUser");
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
setName(name:string): void {
|
||||
setName(name: string): void {
|
||||
localStorage.setItem(playerNameKey, name);
|
||||
}
|
||||
getName(): string|null {
|
||||
const value = localStorage.getItem(playerNameKey) || '';
|
||||
getName(): string | null {
|
||||
const value = localStorage.getItem(playerNameKey) || "";
|
||||
return isUserNameValid(value) ? value : null;
|
||||
}
|
||||
|
||||
setPlayerCharacterIndex(playerCharacterIndex: number): void {
|
||||
localStorage.setItem(selectedPlayerKey, ''+playerCharacterIndex);
|
||||
localStorage.setItem(selectedPlayerKey, "" + playerCharacterIndex);
|
||||
}
|
||||
getPlayerCharacterIndex(): number {
|
||||
return parseInt(localStorage.getItem(selectedPlayerKey) || '');
|
||||
return parseInt(localStorage.getItem(selectedPlayerKey) || "");
|
||||
}
|
||||
|
||||
setCustomCursorPosition(activeRow:number, selectedLayers: number[]): void {
|
||||
localStorage.setItem(customCursorPositionKey, JSON.stringify({activeRow, selectedLayers}));
|
||||
setCustomCursorPosition(activeRow: number, selectedLayers: number[]): void {
|
||||
localStorage.setItem(customCursorPositionKey, JSON.stringify({ activeRow, selectedLayers }));
|
||||
}
|
||||
getCustomCursorPosition(): {activeRow:number, selectedLayers:number[]}|null {
|
||||
getCustomCursorPosition(): { activeRow: number; selectedLayers: number[] } | null {
|
||||
return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null");
|
||||
}
|
||||
|
||||
setCharacterLayers(layers: string[]): void {
|
||||
localStorage.setItem(characterLayersKey, JSON.stringify(layers));
|
||||
}
|
||||
getCharacterLayers(): string[]|null {
|
||||
getCharacterLayers(): string[] | null {
|
||||
const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null");
|
||||
return areCharacterLayersValid(value) ? value : null;
|
||||
}
|
||||
|
||||
setCompanion(companion: string|null): void {
|
||||
setCompanion(companion: string | null): void {
|
||||
return localStorage.setItem(companionKey, JSON.stringify(companion));
|
||||
}
|
||||
getCompanion(): string|null {
|
||||
getCompanion(): string | null {
|
||||
const companion = JSON.parse(localStorage.getItem(companionKey) || "null");
|
||||
|
||||
if (typeof companion !== "string" || companion === "") {
|
||||
@ -68,45 +73,82 @@ class LocalUserStore {
|
||||
}
|
||||
|
||||
setGameQualityValue(value: number): void {
|
||||
localStorage.setItem(gameQualityKey, '' + value);
|
||||
localStorage.setItem(gameQualityKey, "" + value);
|
||||
}
|
||||
getGameQualityValue(): number {
|
||||
return parseInt(localStorage.getItem(gameQualityKey) || '60');
|
||||
return parseInt(localStorage.getItem(gameQualityKey) || "60");
|
||||
}
|
||||
|
||||
setVideoQualityValue(value: number): void {
|
||||
localStorage.setItem(videoQualityKey, '' + value);
|
||||
localStorage.setItem(videoQualityKey, "" + value);
|
||||
}
|
||||
getVideoQualityValue(): number {
|
||||
return parseInt(localStorage.getItem(videoQualityKey) || '20');
|
||||
return parseInt(localStorage.getItem(videoQualityKey) || "20");
|
||||
}
|
||||
|
||||
setAudioPlayerVolume(value: number): void {
|
||||
localStorage.setItem(audioPlayerVolumeKey, '' + value);
|
||||
localStorage.setItem(audioPlayerVolumeKey, "" + value);
|
||||
}
|
||||
getAudioPlayerVolume(): number {
|
||||
return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || '1');
|
||||
return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || "1");
|
||||
}
|
||||
|
||||
setAudioPlayerMuted(value: boolean): void {
|
||||
localStorage.setItem(audioPlayerMuteKey, value.toString());
|
||||
}
|
||||
getAudioPlayerMuted(): boolean {
|
||||
return localStorage.getItem(audioPlayerMuteKey) === 'true';
|
||||
return localStorage.getItem(audioPlayerMuteKey) === "true";
|
||||
}
|
||||
|
||||
setHelpCameraSettingsShown(): void {
|
||||
localStorage.setItem(helpCameraSettingsShown, '1');
|
||||
localStorage.setItem(helpCameraSettingsShown, "1");
|
||||
}
|
||||
getHelpCameraSettingsShown(): boolean {
|
||||
return localStorage.getItem(helpCameraSettingsShown) === '1';
|
||||
return localStorage.getItem(helpCameraSettingsShown) === "1";
|
||||
}
|
||||
|
||||
setFullscreen(value: boolean): void {
|
||||
localStorage.setItem(fullscreenKey, value.toString());
|
||||
}
|
||||
getFullscreen(): boolean {
|
||||
return localStorage.getItem(fullscreenKey) === 'true';
|
||||
return localStorage.getItem(fullscreenKey) === "true";
|
||||
}
|
||||
|
||||
setLastRoomUrl(roomUrl: string): void {
|
||||
localStorage.setItem(lastRoomUrl, roomUrl.toString());
|
||||
}
|
||||
getLastRoomUrl(): string {
|
||||
return localStorage.getItem(lastRoomUrl) ?? "";
|
||||
}
|
||||
|
||||
setAuthToken(value: string | null) {
|
||||
value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken);
|
||||
}
|
||||
getAuthToken(): string | null {
|
||||
return localStorage.getItem(authToken);
|
||||
}
|
||||
|
||||
generateState(): string {
|
||||
const newState = uuidv4();
|
||||
localStorage.setItem(state, newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
verifyState(value: string): boolean {
|
||||
const oldValue = localStorage.getItem(state);
|
||||
localStorage.removeItem(state);
|
||||
return oldValue === value;
|
||||
}
|
||||
generateNonce(): string {
|
||||
const newNonce = uuidv4();
|
||||
localStorage.setItem(nonce, newNonce);
|
||||
return newNonce;
|
||||
}
|
||||
|
||||
getNonce(): string | null {
|
||||
const oldValue = localStorage.getItem(nonce);
|
||||
localStorage.removeItem(nonce);
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,91 +3,103 @@ import { PUSHER_URL } from "../Enum/EnvironmentVariable";
|
||||
import type { CharacterTexture } from "./LocalUser";
|
||||
|
||||
export class MapDetail {
|
||||
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {
|
||||
}
|
||||
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
|
||||
}
|
||||
|
||||
export interface RoomRedirect {
|
||||
redirectUrl: string;
|
||||
}
|
||||
|
||||
export class Room {
|
||||
public readonly id: string;
|
||||
public readonly isPublic: boolean;
|
||||
private mapUrl: string | undefined;
|
||||
private textures: CharacterTexture[] | undefined;
|
||||
private _mapUrl: string | undefined;
|
||||
private _textures: CharacterTexture[] | undefined;
|
||||
private instance: string | undefined;
|
||||
private _search: URLSearchParams;
|
||||
private readonly _search: URLSearchParams;
|
||||
|
||||
constructor(id: string) {
|
||||
const url = new URL(id, 'https://example.com');
|
||||
private constructor(private roomUrl: URL) {
|
||||
this.id = roomUrl.pathname;
|
||||
|
||||
this.id = url.pathname;
|
||||
|
||||
if (this.id.startsWith('/')) {
|
||||
if (this.id.startsWith("/")) {
|
||||
this.id = this.id.substr(1);
|
||||
}
|
||||
if (this.id.startsWith('_/')) {
|
||||
if (this.id.startsWith("_/")) {
|
||||
this.isPublic = true;
|
||||
} else if (this.id.startsWith('@/')) {
|
||||
} else if (this.id.startsWith("@/")) {
|
||||
this.isPublic = false;
|
||||
} else {
|
||||
throw new Error('Invalid room ID');
|
||||
throw new Error("Invalid room ID");
|
||||
}
|
||||
|
||||
this._search = new URLSearchParams(url.search);
|
||||
this._search = new URLSearchParams(roomUrl.search);
|
||||
}
|
||||
|
||||
public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): { roomId: string, hash: string | null } {
|
||||
let roomId = '';
|
||||
let hash = null;
|
||||
if (!identifier.startsWith('/_/') && !identifier.startsWith('/@/')) { //relative file link
|
||||
//Relative identifier can be deep enough to rewrite the base domain, so we cannot use the variable 'baseUrl' as the actual base url for the URL objects.
|
||||
//We instead use 'workadventure' as a dummy base value.
|
||||
const baseUrlObject = new URL(baseUrl);
|
||||
const absoluteExitSceneUrl = new URL(identifier, 'http://workadventure/_/' + currentInstance + '/' + baseUrlObject.hostname + baseUrlObject.pathname);
|
||||
roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId
|
||||
roomId = roomId.substring(1); //remove the leading slash
|
||||
hash = absoluteExitSceneUrl.hash;
|
||||
hash = hash.substring(1); //remove the leading diese
|
||||
if (!hash.length) {
|
||||
hash = null
|
||||
/**
|
||||
* Creates a "Room" object representing the room.
|
||||
* This method will follow room redirects if necessary, so the instance returned is a "real" room.
|
||||
*/
|
||||
public static async createRoom(roomUrl: URL): Promise<Room> {
|
||||
let redirectCount = 0;
|
||||
while (redirectCount < 32) {
|
||||
const room = new Room(roomUrl);
|
||||
const result = await room.getMapDetail();
|
||||
if (result instanceof MapDetail) {
|
||||
return room;
|
||||
}
|
||||
} else { //absolute room Id
|
||||
const parts = identifier.split('#');
|
||||
roomId = parts[0];
|
||||
roomId = roomId.substring(1); //remove the leading slash
|
||||
if (parts.length > 1) {
|
||||
hash = parts[1]
|
||||
redirectCount++;
|
||||
roomUrl = new URL(result.redirectUrl);
|
||||
}
|
||||
}
|
||||
return { roomId, hash }
|
||||
throw new Error("Room resolving seems stuck in a redirect loop after 32 redirect attempts");
|
||||
}
|
||||
|
||||
public async getMapDetail(): Promise<MapDetail> {
|
||||
return new Promise<MapDetail>((resolve, reject) => {
|
||||
if (this.mapUrl !== undefined && this.textures != undefined) {
|
||||
resolve(new MapDetail(this.mapUrl, this.textures));
|
||||
return;
|
||||
public static getRoomPathFromExitUrl(exitUrl: string, currentRoomUrl: string): URL {
|
||||
const url = new URL(exitUrl, currentRoomUrl);
|
||||
return url;
|
||||
}
|
||||
|
||||
if (this.isPublic) {
|
||||
const match = /_\/[^/]+\/(.+)/.exec(this.id);
|
||||
if (!match) throw new Error('Could not extract url from "' + this.id + '"');
|
||||
this.mapUrl = window.location.protocol + '//' + match[1];
|
||||
resolve(new MapDetail(this.mapUrl, this.textures));
|
||||
return;
|
||||
} else {
|
||||
// We have a private ID, we need to query the map URL from the server.
|
||||
const urlParts = this.parsePrivateUrl(this.id);
|
||||
/**
|
||||
* @deprecated USage of exitSceneUrl is deprecated and therefore, this method is deprecated too.
|
||||
*/
|
||||
public static getRoomPathFromExitSceneUrl(
|
||||
exitSceneUrl: string,
|
||||
currentRoomUrl: string,
|
||||
currentMapUrl: string
|
||||
): URL {
|
||||
const absoluteExitSceneUrl = new URL(exitSceneUrl, currentMapUrl);
|
||||
const baseUrl = new URL(currentRoomUrl);
|
||||
|
||||
Axios.get(`${PUSHER_URL}/map`, {
|
||||
params: urlParts
|
||||
}).then(({ data }) => {
|
||||
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl);
|
||||
resolve(data);
|
||||
return;
|
||||
}).catch((reason) => {
|
||||
reject(reason);
|
||||
const currentRoom = new Room(baseUrl);
|
||||
let instance: string = "global";
|
||||
if (currentRoom.isPublic) {
|
||||
instance = currentRoom.instance as string;
|
||||
}
|
||||
|
||||
baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
|
||||
if (absoluteExitSceneUrl.hash) {
|
||||
baseUrl.hash = absoluteExitSceneUrl.hash;
|
||||
}
|
||||
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
|
||||
const result = await Axios.get(`${PUSHER_URL}/map`, {
|
||||
params: {
|
||||
playUri: this.roomUrl.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
const data = result.data;
|
||||
if (data.redirectUrl) {
|
||||
return {
|
||||
redirectUrl: data.redirectUrl as string,
|
||||
};
|
||||
}
|
||||
});
|
||||
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
|
||||
this._mapUrl = data.mapUrl;
|
||||
this._textures = data.textures;
|
||||
return new MapDetail(data.mapUrl, data.textures);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,21 +120,24 @@ export class Room {
|
||||
} else {
|
||||
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
|
||||
if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
|
||||
this.instance = match[1] + '/' + match[2];
|
||||
this.instance = match[1] + "/" + match[2];
|
||||
return this.instance;
|
||||
}
|
||||
}
|
||||
|
||||
private parsePrivateUrl(url: string): { organizationSlug: string, worldSlug: string, roomSlug?: string } {
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
private parsePrivateUrl(url: string): { organizationSlug: string; worldSlug: string; roomSlug?: string } {
|
||||
const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm;
|
||||
const match = regex.exec(url);
|
||||
if (!match) {
|
||||
throw new Error('Invalid URL ' + url);
|
||||
throw new Error("Invalid URL " + url);
|
||||
}
|
||||
const results: { organizationSlug: string, worldSlug: string, roomSlug?: string } = {
|
||||
const results: { organizationSlug: string; worldSlug: string; roomSlug?: string } = {
|
||||
organizationSlug: match[1],
|
||||
worldSlug: match[2],
|
||||
}
|
||||
};
|
||||
if (match[3] !== undefined) {
|
||||
results.roomSlug = match[3];
|
||||
}
|
||||
@ -130,8 +145,8 @@ export class Room {
|
||||
}
|
||||
|
||||
public isDisconnected(): boolean {
|
||||
const alone = this._search.get('alone');
|
||||
if (alone && alone !== '0' && alone.toLowerCase() !== 'false') {
|
||||
const alone = this._search.get("alone");
|
||||
if (alone && alone !== "0" && alone.toLowerCase() !== "false") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -140,4 +155,33 @@ export class Room {
|
||||
public get search(): URLSearchParams {
|
||||
return this._search;
|
||||
}
|
||||
|
||||
/**
|
||||
* 2 rooms are equal if they share the same path (but not necessarily the same hash)
|
||||
* @param room
|
||||
*/
|
||||
public isEqual(room: Room): boolean {
|
||||
return room.key === this.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* A key representing this room
|
||||
*/
|
||||
public get key(): string {
|
||||
const newUrl = new URL(this.roomUrl.toString());
|
||||
newUrl.search = "";
|
||||
newUrl.hash = "";
|
||||
return newUrl.toString();
|
||||
}
|
||||
|
||||
get textures(): CharacterTexture[] | undefined {
|
||||
return this._textures;
|
||||
}
|
||||
|
||||
get mapUrl(): string {
|
||||
if (!this._mapUrl) {
|
||||
throw new Error("Map URL not fetched yet");
|
||||
}
|
||||
return this._mapUrl;
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ import {
|
||||
EmotePromptMessage,
|
||||
SendUserMessage,
|
||||
BanUserMessage,
|
||||
VariableMessage,
|
||||
ErrorMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
|
||||
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
|
||||
@ -53,9 +55,9 @@ import {
|
||||
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
|
||||
import { adminMessagesService } from "./AdminMessagesService";
|
||||
import { worldFullMessageStream } from "./WorldFullMessageStream";
|
||||
import { worldFullWarningStream } from "./WorldFullWarningStream";
|
||||
import { connectionManager } from "./ConnectionManager";
|
||||
import { emoteEventStream } from "./EmoteEventStream";
|
||||
import { warningContainerStore } from "../Stores/MenuStore";
|
||||
|
||||
const manualPingDelay = 20000;
|
||||
|
||||
@ -74,12 +76,12 @@ export class RoomConnection implements RoomConnection {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param token A JWT token containing the UUID of the user
|
||||
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]"
|
||||
* @param token A JWT token containing the email of the user
|
||||
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
|
||||
*/
|
||||
public constructor(
|
||||
token: string | null,
|
||||
roomId: string,
|
||||
roomUrl: string,
|
||||
name: string,
|
||||
characterLayers: string[],
|
||||
position: PositionInterface,
|
||||
@ -92,7 +94,7 @@ export class RoomConnection implements RoomConnection {
|
||||
url += "/";
|
||||
}
|
||||
url += "room";
|
||||
url += "?roomId=" + (roomId ? encodeURIComponent(roomId) : "");
|
||||
url += "?roomId=" + encodeURIComponent(roomUrl);
|
||||
url += "&token=" + (token ? encodeURIComponent(token) : "");
|
||||
url += "&name=" + encodeURIComponent(name);
|
||||
for (const layer of characterLayers) {
|
||||
@ -164,6 +166,12 @@ export class RoomConnection implements RoomConnection {
|
||||
} else if (subMessage.hasEmoteeventmessage()) {
|
||||
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
|
||||
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
|
||||
} else if (subMessage.hasErrormessage()) {
|
||||
const errorMessage = subMessage.getErrormessage() as ErrorMessage;
|
||||
console.error("An error occurred server side: " + errorMessage.getMessage());
|
||||
} else if (subMessage.hasVariablemessage()) {
|
||||
event = EventMessage.SET_VARIABLE;
|
||||
payload = subMessage.getVariablemessage();
|
||||
} else {
|
||||
throw new Error("Unexpected batch message type");
|
||||
}
|
||||
@ -180,6 +188,22 @@ export class RoomConnection implements RoomConnection {
|
||||
items[item.getItemid()] = JSON.parse(item.getStatejson());
|
||||
}
|
||||
|
||||
const variables = new Map<string, unknown>();
|
||||
for (const variable of roomJoinedMessage.getVariableList()) {
|
||||
try {
|
||||
variables.set(variable.getName(), JSON.parse(variable.getValue()));
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Unable to unserialize value received from server for variable "' +
|
||||
variable.getName() +
|
||||
'". Value received: "' +
|
||||
variable.getValue() +
|
||||
'". Error: ',
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.userId = roomJoinedMessage.getCurrentuserid();
|
||||
this.tags = roomJoinedMessage.getTagList();
|
||||
|
||||
@ -187,11 +211,15 @@ export class RoomConnection implements RoomConnection {
|
||||
connection: this,
|
||||
room: {
|
||||
items,
|
||||
variables,
|
||||
} as RoomJoinedMessageInterface,
|
||||
});
|
||||
} else if (message.hasWorldfullmessage()) {
|
||||
worldFullMessageStream.onMessage();
|
||||
this.closed = true;
|
||||
} else if (message.hasTokenexpiredmessage()) {
|
||||
connectionManager.loadOpenIDScreen();
|
||||
this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
|
||||
} else if (message.hasWorldconnexionmessage()) {
|
||||
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
|
||||
this.closed = true;
|
||||
@ -219,7 +247,7 @@ export class RoomConnection implements RoomConnection {
|
||||
} else if (message.hasBanusermessage()) {
|
||||
adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage);
|
||||
} else if (message.hasWorldfullwarningmessage()) {
|
||||
worldFullWarningStream.onMessage();
|
||||
warningContainerStore.activateWarningContainer();
|
||||
} else if (message.hasRefreshroommessage()) {
|
||||
//todo: implement a way to notify the user the room was refreshed.
|
||||
} else {
|
||||
@ -536,6 +564,17 @@ export class RoomConnection implements RoomConnection {
|
||||
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||
}
|
||||
|
||||
emitSetVariableEvent(name: string, value: unknown): void {
|
||||
const variableMessage = new VariableMessage();
|
||||
variableMessage.setName(name);
|
||||
variableMessage.setValue(JSON.stringify(value));
|
||||
|
||||
const clientToServerMessage = new ClientToServerMessage();
|
||||
clientToServerMessage.setVariablemessage(variableMessage);
|
||||
|
||||
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||
}
|
||||
|
||||
onActionableEvent(callback: (message: ItemEventMessageInterface) => void): void {
|
||||
this.onMessage(EventMessage.ITEM_EVENT, (message: ItemEventMessage) => {
|
||||
callback({
|
||||
@ -558,7 +597,7 @@ export class RoomConnection implements RoomConnection {
|
||||
});
|
||||
}
|
||||
|
||||
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
|
||||
/* public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
|
||||
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
|
||||
callback({
|
||||
id: message.getId(),
|
||||
@ -566,7 +605,7 @@ export class RoomConnection implements RoomConnection {
|
||||
message: message.getMessage(),
|
||||
});
|
||||
});
|
||||
}
|
||||
}*/
|
||||
|
||||
public receiveStopGlobalMessage(callback: (messageId: string) => void) {
|
||||
return this.onMessage(EventMessage.STOP_GLOBAL_MESSAGE, (message: StopGlobalMessage) => {
|
||||
@ -580,11 +619,11 @@ export class RoomConnection implements RoomConnection {
|
||||
});
|
||||
}
|
||||
|
||||
public emitGlobalMessage(message: PlayGlobalMessageInterface) {
|
||||
public emitGlobalMessage(message: PlayGlobalMessageInterface): void {
|
||||
const playGlobalMessage = new PlayGlobalMessage();
|
||||
playGlobalMessage.setId(message.id);
|
||||
playGlobalMessage.setType(message.type);
|
||||
playGlobalMessage.setMessage(message.message);
|
||||
playGlobalMessage.setContent(message.content);
|
||||
playGlobalMessage.setBroadcasttoworld(message.broadcastToWorld);
|
||||
|
||||
const clientToServerMessage = new ClientToServerMessage();
|
||||
clientToServerMessage.setPlayglobalmessage(playGlobalMessage);
|
||||
@ -622,6 +661,29 @@ export class RoomConnection implements RoomConnection {
|
||||
});
|
||||
}
|
||||
|
||||
public onSetVariable(callback: (name: string, value: unknown) => void): void {
|
||||
this.onMessage(EventMessage.SET_VARIABLE, (message: VariableMessage) => {
|
||||
const name = message.getName();
|
||||
const serializedValue = message.getValue();
|
||||
let value: unknown = undefined;
|
||||
if (serializedValue) {
|
||||
try {
|
||||
value = JSON.parse(serializedValue);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Unable to unserialize value received from server for variable "' +
|
||||
name +
|
||||
'". Value received: "' +
|
||||
serializedValue +
|
||||
'". Error: ',
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
callback(name, value);
|
||||
});
|
||||
}
|
||||
|
||||
public hasTag(tag: string): boolean {
|
||||
return this.tags.includes(tag);
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
import {Subject} from "rxjs";
|
||||
|
||||
class WorldFullWarningStream {
|
||||
|
||||
private _stream:Subject<void> = new Subject();
|
||||
public stream = this._stream.asObservable();
|
||||
|
||||
|
||||
onMessage() {
|
||||
this._stream.next();
|
||||
}
|
||||
}
|
||||
|
||||
export const worldFullWarningStream = new WorldFullWarningStream();
|
@ -1,22 +1,24 @@
|
||||
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 PUSHER_URL = process.env.PUSHER_URL || '//pusher.workadventure.localhost';
|
||||
const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost';
|
||||
const START_ROOM_URL: string =
|
||||
process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor1/floor1.json";
|
||||
const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost";
|
||||
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re";
|
||||
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
|
||||
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
|
||||
const TURN_SERVER: string = process.env.TURN_SERVER || "";
|
||||
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true";
|
||||
const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true";
|
||||
const TURN_USER: string = process.env.TURN_USER || '';
|
||||
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || '';
|
||||
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
|
||||
const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true";
|
||||
const TURN_USER: string = process.env.TURN_USER || "";
|
||||
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || "";
|
||||
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
|
||||
const JITSI_PRIVATE_MODE: boolean = process.env.JITSI_PRIVATE_MODE == "true";
|
||||
const POSITION_DELAY = 200; // Wait 200ms between sending position events
|
||||
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player
|
||||
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8;
|
||||
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4');
|
||||
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == 'true';
|
||||
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || "") || 8;
|
||||
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
|
||||
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true";
|
||||
|
||||
export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window.innerHeight <= 600 ) );
|
||||
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
|
||||
|
||||
export {
|
||||
DEBUG_MODE,
|
||||
@ -32,5 +34,5 @@ export {
|
||||
TURN_USER,
|
||||
TURN_PASSWORD,
|
||||
JITSI_URL,
|
||||
JITSI_PRIVATE_MODE
|
||||
}
|
||||
JITSI_PRIVATE_MODE,
|
||||
};
|
||||
|
20
front/src/Network/ServiceWorker.ts
Normal file
20
front/src/Network/ServiceWorker.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export class _ServiceWorker {
|
||||
constructor() {
|
||||
if ("serviceWorker" in navigator) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/resources/service-worker.js")
|
||||
.then((serviceWorker) => {
|
||||
console.info("Service Worker registered: ", serviceWorker);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error registering the Service Worker: ", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,35 +1,43 @@
|
||||
import type {ITiledMapObject} from "../Map/ITiledMap";
|
||||
import type {GameScene} from "../Game/GameScene";
|
||||
import type { ITiledMapObject } from "../Map/ITiledMap";
|
||||
import type { GameScene } from "../Game/GameScene";
|
||||
import { type } from "os";
|
||||
|
||||
export class TextUtils {
|
||||
public static createTextFromITiledMapObject(scene: GameScene, object: ITiledMapObject): void {
|
||||
if (object.text === undefined) {
|
||||
throw new Error('This object has not textual representation.');
|
||||
throw new Error("This object has not textual representation.");
|
||||
}
|
||||
const options: {
|
||||
fontStyle?: string,
|
||||
fontSize?: string,
|
||||
fontFamily?: string,
|
||||
color?: string,
|
||||
align?: string,
|
||||
fontStyle?: string;
|
||||
fontSize?: string;
|
||||
fontFamily?: string;
|
||||
color?: string;
|
||||
align?: string;
|
||||
wordWrap?: {
|
||||
width: number,
|
||||
useAdvancedWrap?: boolean
|
||||
}
|
||||
width: number;
|
||||
useAdvancedWrap?: boolean;
|
||||
};
|
||||
} = {};
|
||||
if (object.text.italic) {
|
||||
options.fontStyle = 'italic';
|
||||
options.fontStyle = "italic";
|
||||
}
|
||||
// Note: there is no support for "strikeout" and "underline"
|
||||
let fontSize: number = 16;
|
||||
if (object.text.pixelsize) {
|
||||
fontSize = object.text.pixelsize;
|
||||
}
|
||||
options.fontSize = fontSize + 'px';
|
||||
options.fontSize = fontSize + "px";
|
||||
if (object.text.fontfamily) {
|
||||
options.fontFamily = '"'+object.text.fontfamily+'"';
|
||||
options.fontFamily = '"' + object.text.fontfamily + '"';
|
||||
}
|
||||
let color = '#000000';
|
||||
if (object.properties !== undefined) {
|
||||
for (const property of object.properties) {
|
||||
if (property.name === "font-family" && typeof property.value === "string") {
|
||||
options.fontFamily = property.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
let color = "#000000";
|
||||
if (object.text.color !== undefined) {
|
||||
color = object.text.color;
|
||||
}
|
||||
@ -38,7 +46,7 @@ export class TextUtils {
|
||||
options.wordWrap = {
|
||||
width: object.width,
|
||||
//useAdvancedWrap: true
|
||||
}
|
||||
};
|
||||
}
|
||||
if (object.text.halign !== undefined) {
|
||||
options.align = object.text.halign;
|
||||
|
@ -1,14 +0,0 @@
|
||||
|
||||
export const warningContainerKey = 'warningContainer';
|
||||
export const warningContainerHtml = 'resources/html/warningContainer.html';
|
||||
|
||||
export class WarningContainer extends Phaser.GameObjects.DOMElement {
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
super(scene, 100, 0);
|
||||
this.setOrigin(0, 0);
|
||||
this.createFromCache(warningContainerKey);
|
||||
this.scene.add.existing(this);
|
||||
}
|
||||
|
||||
}
|
@ -107,7 +107,7 @@ export const createLoadingPromise = (
|
||||
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig);
|
||||
const errorCallback = (file: { src: string }) => {
|
||||
if (file.src !== playerResourceDescriptor.img) return;
|
||||
console.error("failed loading player ressource: ", playerResourceDescriptor);
|
||||
console.error("failed loading player resource: ", playerResourceDescriptor);
|
||||
rej(playerResourceDescriptor);
|
||||
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.name, successCallback);
|
||||
loadPlugin.off("loaderror", errorCallback);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type {PointInterface} from "../../Connexion/ConnexionModels";
|
||||
import type {PlayerInterface} from "./PlayerInterface";
|
||||
import type { PointInterface } from "../../Connexion/ConnexionModels";
|
||||
import type { PlayerInterface } from "./PlayerInterface";
|
||||
|
||||
export interface AddPlayerInterface extends PlayerInterface {
|
||||
position: PointInterface;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable";
|
||||
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
|
||||
import {waScaleManager} from "../Services/WaScaleManager";
|
||||
import {ResizableScene} from "../Login/ResizableScene";
|
||||
import { SKIP_RENDER_OPTIMIZATIONS } from "../../Enum/EnvironmentVariable";
|
||||
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
|
||||
import { waScaleManager } from "../Services/WaScaleManager";
|
||||
import { ResizableScene } from "../Login/ResizableScene";
|
||||
|
||||
const Events = Phaser.Core.Events;
|
||||
|
||||
@ -14,10 +14,8 @@ const Events = Phaser.Core.Events;
|
||||
* It also automatically calls "onResize" on any scenes extending ResizableScene.
|
||||
*/
|
||||
export class Game extends Phaser.Game {
|
||||
|
||||
private _isDirty = false;
|
||||
|
||||
|
||||
constructor(GameConfig: Phaser.Types.Core.GameConfig) {
|
||||
super(GameConfig);
|
||||
|
||||
@ -27,7 +25,7 @@ export class Game extends Phaser.Game {
|
||||
scene.onResize();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
/*window.addEventListener('resize', (event) => {
|
||||
// Let's trigger the onResize method of any active scene that is a ResizableScene
|
||||
@ -39,11 +37,9 @@ export class Game extends Phaser.Game {
|
||||
});*/
|
||||
}
|
||||
|
||||
public step(time: number, delta: number)
|
||||
{
|
||||
public step(time: number, delta: number) {
|
||||
// @ts-ignore
|
||||
if (this.pendingDestroy)
|
||||
{
|
||||
if (this.pendingDestroy) {
|
||||
// @ts-ignore
|
||||
return this.runDestroy();
|
||||
}
|
||||
@ -100,15 +96,17 @@ export class Game extends Phaser.Game {
|
||||
}
|
||||
|
||||
// Loop through the scenes in forward order
|
||||
for (let i = 0; i < this.scene.scenes.length; i++)
|
||||
{
|
||||
for (let i = 0; i < this.scene.scenes.length; i++) {
|
||||
const scene = this.scene.scenes[i];
|
||||
const sys = scene.sys;
|
||||
|
||||
if (sys.settings.visible && sys.settings.status >= Phaser.Scenes.LOADING && sys.settings.status < Phaser.Scenes.SLEEPING)
|
||||
{
|
||||
if (
|
||||
sys.settings.visible &&
|
||||
sys.settings.status >= Phaser.Scenes.LOADING &&
|
||||
sys.settings.status < Phaser.Scenes.SLEEPING
|
||||
) {
|
||||
// @ts-ignore
|
||||
if(typeof scene.isDirty === 'function') {
|
||||
if (typeof scene.isDirty === "function") {
|
||||
// @ts-ignore
|
||||
const isDirty = scene.isDirty() || scene.tweens.getAllTweens().length > 0;
|
||||
if (isDirty) {
|
||||
@ -129,4 +127,11 @@ export class Game extends Phaser.Game {
|
||||
public markDirty(): void {
|
||||
this._isDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first scene found in the game
|
||||
*/
|
||||
public findAnyScene(): Phaser.Scene {
|
||||
return this.scene.getScenes()[0];
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export class GameManager {
|
||||
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
|
||||
this.scenePlugin = scenePlugin;
|
||||
this.startRoom = await connectionManager.initGameConnexion();
|
||||
await this.loadMap(this.startRoom);
|
||||
this.loadMap(this.startRoom);
|
||||
|
||||
if (!this.playerName) {
|
||||
return LoginSceneName;
|
||||
@ -72,19 +72,19 @@ export class GameManager {
|
||||
return this.companion;
|
||||
}
|
||||
|
||||
public async loadMap(room: Room): Promise<void> {
|
||||
const roomID = room.id;
|
||||
const mapDetail = await room.getMapDetail();
|
||||
public loadMap(room: Room) {
|
||||
const roomID = room.key;
|
||||
|
||||
const gameIndex = this.scenePlugin.getIndex(roomID);
|
||||
if (gameIndex === -1) {
|
||||
const game: Phaser.Scene = new GameScene(room, mapDetail.mapUrl);
|
||||
const game: Phaser.Scene = new GameScene(room, room.mapUrl);
|
||||
this.scenePlugin.add(roomID, game, false);
|
||||
}
|
||||
}
|
||||
|
||||
public goToStartingMap(): void {
|
||||
this.scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
|
||||
console.log("starting " + (this.currentGameSceneName || this.startRoom.key));
|
||||
this.scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
|
||||
this.scenePlugin.launch(MenuSceneName);
|
||||
|
||||
if (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap";
|
||||
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiledMap";
|
||||
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
|
||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
|
||||
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
|
||||
@ -19,7 +19,7 @@ export class GameMap {
|
||||
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
|
||||
private tileNameMap = new Map<string, number>();
|
||||
|
||||
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {};
|
||||
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {};
|
||||
public readonly flatLayers: ITiledMapLayer[];
|
||||
public readonly phaserLayers: TilemapLayer[] = [];
|
||||
|
||||
@ -61,7 +61,7 @@ export class GameMap {
|
||||
}
|
||||
}
|
||||
|
||||
public getPropertiesForIndex(index: number): Array<ITiledMapLayerProperty> {
|
||||
public getPropertiesForIndex(index: number): Array<ITiledMapProperty> {
|
||||
if (this.tileSetPropertyMap[index]) {
|
||||
return this.tileSetPropertyMap[index];
|
||||
}
|
||||
@ -151,7 +151,7 @@ export class GameMap {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
private getTileProperty(index: number): Array<ITiledMapLayerProperty> {
|
||||
private getTileProperty(index: number): Array<ITiledMapProperty> {
|
||||
if (this.tileSetPropertyMap[index]) {
|
||||
return this.tileSetPropertyMap[index];
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Subscription } from "rxjs";
|
||||
import { GlobalMessageManager } from "../../Administration/GlobalMessageManager";
|
||||
import { userMessageManager } from "../../Administration/UserMessageManager";
|
||||
import { iframeListener } from "../../Api/IframeListener";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
@ -47,13 +46,7 @@ import { RemotePlayer } from "../Entity/RemotePlayer";
|
||||
import type { ActionableItem } from "../Items/ActionableItem";
|
||||
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
|
||||
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
|
||||
import type {
|
||||
ITiledMap,
|
||||
ITiledMapLayer,
|
||||
ITiledMapLayerProperty,
|
||||
ITiledMapObject,
|
||||
ITiledTileSet,
|
||||
} from "../Map/ITiledMap";
|
||||
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap";
|
||||
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
|
||||
import { PlayerAnimationDirections } from "../Player/Animation";
|
||||
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
|
||||
@ -81,8 +74,6 @@ import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey }
|
||||
import { waScaleManager } from "../Services/WaScaleManager";
|
||||
import { EmoteManager } from "./EmoteManager";
|
||||
import EVENT_TYPE = Phaser.Scenes.Events;
|
||||
import RenderTexture = Phaser.GameObjects.RenderTexture;
|
||||
import Tilemap = Phaser.Tilemaps.Tilemap;
|
||||
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
|
||||
|
||||
import AnimatedTiles from "phaser-animated-tiles";
|
||||
@ -91,8 +82,11 @@ import { soundManager } from "./SoundManager";
|
||||
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
|
||||
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
||||
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
||||
import { SharedVariablesManager } from "./SharedVariablesManager";
|
||||
import { playersStore } from "../../Stores/PlayersStore";
|
||||
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||
import Tileset = Phaser.Tilemaps.Tileset;
|
||||
import { userIsAdminStore } from "../../Stores/GameStore";
|
||||
|
||||
export interface GameSceneInitInterface {
|
||||
initPosition: PointInterface | null;
|
||||
@ -160,7 +154,6 @@ export class GameScene extends DirtyScene {
|
||||
private playersPositionInterpolator = new PlayersPositionInterpolator();
|
||||
public connection: RoomConnection | undefined;
|
||||
private simplePeer!: SimplePeer;
|
||||
private GlobalMessageManager!: GlobalMessageManager;
|
||||
private connectionAnswerPromise: Promise<RoomJoinedMessageInterface>;
|
||||
private connectionAnswerPromiseResolve!: (
|
||||
value: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface>
|
||||
@ -173,7 +166,7 @@ export class GameScene extends DirtyScene {
|
||||
private chatVisibilityUnsubscribe!: () => void;
|
||||
private biggestAvailableAreaStoreUnsubscribe!: () => void;
|
||||
MapUrlFile: string;
|
||||
RoomId: string;
|
||||
roomUrl: string;
|
||||
instance: string;
|
||||
|
||||
currentTick!: number;
|
||||
@ -199,21 +192,22 @@ export class GameScene extends DirtyScene {
|
||||
private popUpElements: Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>();
|
||||
private originalMapUrl: string | undefined;
|
||||
private pinchManager: PinchManager | undefined;
|
||||
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
|
||||
private mapTransitioning: boolean = false; //used to prevent transitions happening at the same time.
|
||||
private emoteManager!: EmoteManager;
|
||||
private preloading: boolean = true;
|
||||
startPositionCalculator!: StartPositionCalculator;
|
||||
private startPositionCalculator!: StartPositionCalculator;
|
||||
private sharedVariablesManager!: SharedVariablesManager;
|
||||
|
||||
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
||||
super({
|
||||
key: customKey ?? room.id,
|
||||
key: customKey ?? room.key,
|
||||
});
|
||||
this.Terrains = [];
|
||||
this.groups = new Map<number, Sprite>();
|
||||
this.instance = room.getInstance();
|
||||
|
||||
this.MapUrlFile = MapUrlFile;
|
||||
this.RoomId = room.id;
|
||||
this.roomUrl = room.key;
|
||||
|
||||
this.createPromise = new Promise<void>((resolve, reject): void => {
|
||||
this.createPromiseResolve = resolve;
|
||||
@ -225,6 +219,9 @@ export class GameScene extends DirtyScene {
|
||||
|
||||
//hook preload scene
|
||||
preload(): void {
|
||||
//initialize frame event of scripting API
|
||||
this.listenToIframeEvents();
|
||||
|
||||
const localUser = localUserStore.getLocalUser();
|
||||
const textures = localUser?.textures;
|
||||
if (textures) {
|
||||
@ -440,7 +437,7 @@ export class GameScene extends DirtyScene {
|
||||
this.characterLayers = gameManager.getCharacterLayers();
|
||||
this.companion = gameManager.getCompanion();
|
||||
|
||||
//initalise map
|
||||
//initialise map
|
||||
this.Map = this.add.tilemap(this.MapUrlFile);
|
||||
const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
|
||||
this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => {
|
||||
@ -465,11 +462,13 @@ export class GameScene extends DirtyScene {
|
||||
if (layer.type === "tilelayer") {
|
||||
const exitSceneUrl = this.getExitSceneUrl(layer);
|
||||
if (exitSceneUrl !== undefined) {
|
||||
this.loadNextGame(exitSceneUrl);
|
||||
this.loadNextGame(
|
||||
Room.getRoomPathFromExitSceneUrl(exitSceneUrl, window.location.toString(), this.MapUrlFile)
|
||||
);
|
||||
}
|
||||
const exitUrl = this.getExitUrl(layer);
|
||||
if (exitUrl !== undefined) {
|
||||
this.loadNextGame(exitUrl);
|
||||
this.loadNextGameFromExitUrl(exitUrl);
|
||||
}
|
||||
}
|
||||
if (layer.type === "objectgroup") {
|
||||
@ -482,7 +481,7 @@ export class GameScene extends DirtyScene {
|
||||
}
|
||||
|
||||
this.gameMap.exitUrls.forEach((exitUrl) => {
|
||||
this.loadNextGame(exitUrl);
|
||||
this.loadNextGameFromExitUrl(exitUrl);
|
||||
});
|
||||
|
||||
this.startPositionCalculator = new StartPositionCalculator(
|
||||
@ -551,7 +550,6 @@ export class GameScene extends DirtyScene {
|
||||
);
|
||||
|
||||
this.triggerOnMapLayerPropertyChange();
|
||||
this.listenToIframeEvents();
|
||||
|
||||
if (!this.room.isDisconnected()) {
|
||||
this.connect();
|
||||
@ -587,7 +585,7 @@ export class GameScene extends DirtyScene {
|
||||
|
||||
connectionManager
|
||||
.connectToRoomSocket(
|
||||
this.RoomId,
|
||||
this.roomUrl,
|
||||
this.playerName,
|
||||
this.characterLayers,
|
||||
{
|
||||
@ -606,6 +604,8 @@ export class GameScene extends DirtyScene {
|
||||
|
||||
playersStore.connectToRoomConnection(this.connection);
|
||||
|
||||
userIsAdminStore.set(this.connection.hasTag("admin"));
|
||||
|
||||
this.connection.onUserJoins((message: MessageUserJoined) => {
|
||||
const userMessage: AddPlayerInterface = {
|
||||
userId: message.userId,
|
||||
@ -692,7 +692,6 @@ export class GameScene extends DirtyScene {
|
||||
peerStore.connectToSimplePeer(this.simplePeer);
|
||||
screenSharingPeerStore.connectToSimplePeer(this.simplePeer);
|
||||
videoFocusStore.connectToSimplePeer(this.simplePeer);
|
||||
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
|
||||
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
|
||||
|
||||
const self = this;
|
||||
@ -716,6 +715,13 @@ export class GameScene extends DirtyScene {
|
||||
this.gameMap.setPosition(event.x, event.y);
|
||||
});
|
||||
|
||||
// Set up variables manager
|
||||
this.sharedVariablesManager = new SharedVariablesManager(
|
||||
this.connection,
|
||||
this.gameMap,
|
||||
onConnect.room.variables
|
||||
);
|
||||
|
||||
//this.initUsersPosition(roomJoinedMessage.users);
|
||||
this.connectionAnswerPromiseResolve(onConnect.room);
|
||||
// Analyze tags to find if we are admin. If yes, show console.
|
||||
@ -775,10 +781,13 @@ export class GameScene extends DirtyScene {
|
||||
|
||||
private triggerOnMapLayerPropertyChange() {
|
||||
this.gameMap.onPropertyChange("exitSceneUrl", (newValue, oldValue) => {
|
||||
if (newValue) this.onMapExit(newValue as string);
|
||||
if (newValue)
|
||||
this.onMapExit(
|
||||
Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile)
|
||||
);
|
||||
});
|
||||
this.gameMap.onPropertyChange("exitUrl", (newValue, oldValue) => {
|
||||
if (newValue) this.onMapExit(newValue as string);
|
||||
if (newValue) this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString()));
|
||||
});
|
||||
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
|
||||
if (newValue === undefined) {
|
||||
@ -1003,9 +1012,9 @@ ${escapedMessage}
|
||||
);
|
||||
this.iframeSubscriptionList.push(
|
||||
iframeListener.loadPageStream.subscribe((url: string) => {
|
||||
this.loadNextGame(url).then(() => {
|
||||
this.loadNextGameFromExitUrl(url).then(() => {
|
||||
this.events.once(EVENT_TYPE.POST_UPDATE, () => {
|
||||
this.onMapExit(url);
|
||||
this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString()));
|
||||
});
|
||||
});
|
||||
})
|
||||
@ -1048,20 +1057,24 @@ ${escapedMessage}
|
||||
})
|
||||
);
|
||||
|
||||
this.iframeSubscriptionList.push(
|
||||
iframeListener.dataLayerChangeStream.subscribe(() => {
|
||||
iframeListener.sendDataLayerEvent({ data: this.gameMap.getMap() });
|
||||
})
|
||||
);
|
||||
iframeListener.registerAnswerer("getMapData", () => {
|
||||
return {
|
||||
data: this.gameMap.getMap(),
|
||||
};
|
||||
});
|
||||
|
||||
iframeListener.registerAnswerer("getState", () => {
|
||||
iframeListener.registerAnswerer("getState", async () => {
|
||||
// The sharedVariablesManager is not instantiated before the connection is established. So we need to wait
|
||||
// for the connection to send back the answer.
|
||||
await this.connectionAnswerPromise;
|
||||
return {
|
||||
mapUrl: this.MapUrlFile,
|
||||
startLayerName: this.startPositionCalculator.startLayerName,
|
||||
uuid: localUserStore.getLocalUser()?.uuid,
|
||||
nickname: localUserStore.getName(),
|
||||
roomId: this.RoomId,
|
||||
nickname: this.playerName,
|
||||
roomId: this.roomUrl,
|
||||
tags: this.connection ? this.connection.getAllTags() : [],
|
||||
variables: this.sharedVariablesManager.variables,
|
||||
};
|
||||
});
|
||||
this.iframeSubscriptionList.push(
|
||||
@ -1069,8 +1082,74 @@ ${escapedMessage}
|
||||
for (const eventTile of eventTiles) {
|
||||
this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer);
|
||||
}
|
||||
this.markDirty();
|
||||
})
|
||||
);
|
||||
iframeListener.registerAnswerer("loadTileset", (eventTileset) => {
|
||||
return this.connectionAnswerPromise.then(() => {
|
||||
const jsonTilesetDir = eventTileset.url.substr(0, eventTileset.url.lastIndexOf("/"));
|
||||
//Initialise the firstgid to 1 because if there is no tileset in the tilemap, the firstgid will be 1
|
||||
let newFirstgid = 1;
|
||||
const lastTileset = this.mapFile.tilesets[this.mapFile.tilesets.length - 1];
|
||||
if (lastTileset) {
|
||||
//If there is at least one tileset in the tilemap then calculate the firstgid of the new tileset
|
||||
newFirstgid = lastTileset.firstgid + lastTileset.tilecount;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.load.on("filecomplete-json-" + eventTileset.url, () => {
|
||||
let jsonTileset = this.cache.json.get(eventTileset.url);
|
||||
const imageUrl = jsonTilesetDir + "/" + jsonTileset.image;
|
||||
this.load.image(imageUrl, imageUrl);
|
||||
this.load.on("filecomplete-image-" + imageUrl, () => {
|
||||
//Add the firstgid of the tileset to the json file
|
||||
jsonTileset = { ...jsonTileset, firstgid: newFirstgid };
|
||||
this.mapFile.tilesets.push(jsonTileset);
|
||||
this.Map.tilesets.push(
|
||||
new Tileset(
|
||||
jsonTileset.name,
|
||||
jsonTileset.firstgid,
|
||||
jsonTileset.tileWidth,
|
||||
jsonTileset.tileHeight,
|
||||
jsonTileset.margin,
|
||||
jsonTileset.spacing,
|
||||
jsonTileset.tiles
|
||||
)
|
||||
);
|
||||
this.Terrains.push(
|
||||
this.Map.addTilesetImage(
|
||||
jsonTileset.name,
|
||||
imageUrl,
|
||||
jsonTileset.tilewidth,
|
||||
jsonTileset.tileheight,
|
||||
jsonTileset.margin,
|
||||
jsonTileset.spacing
|
||||
)
|
||||
);
|
||||
//destroy the tilemapayer because they are unique and we need to reuse their key and layerdData
|
||||
for (const layer of this.Map.layers) {
|
||||
layer.tilemapLayer.destroy(false);
|
||||
}
|
||||
//Create a new GameMap with the changed file
|
||||
this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains);
|
||||
//Destroy the colliders of the old tilemapLayer
|
||||
this.physics.add.world.colliders.destroy();
|
||||
//Create new colliders with the new GameMap
|
||||
this.createCollisionWithPlayer();
|
||||
//Create new trigger with the new GameMap
|
||||
this.triggerOnMapLayerPropertyChange();
|
||||
resolve(newFirstgid);
|
||||
});
|
||||
});
|
||||
this.load.on("loaderror", () => {
|
||||
console.error("Error while loading " + eventTileset.url + ".");
|
||||
reject(-1);
|
||||
});
|
||||
|
||||
this.load.json(eventTileset.url, eventTileset.url);
|
||||
this.load.start();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private setPropertyLayer(
|
||||
@ -1084,7 +1163,7 @@ ${escapedMessage}
|
||||
return;
|
||||
}
|
||||
if (propertyName === "exitUrl" && typeof propertyValue === "string") {
|
||||
this.loadNextGame(propertyValue);
|
||||
this.loadNextGameFromExitUrl(propertyValue);
|
||||
}
|
||||
if (layer.properties === undefined) {
|
||||
layer.properties = [];
|
||||
@ -1131,28 +1210,38 @@ ${escapedMessage}
|
||||
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
|
||||
}
|
||||
|
||||
private onMapExit(exitKey: string) {
|
||||
private async onMapExit(roomUrl: URL) {
|
||||
if (this.mapTransitioning) return;
|
||||
this.mapTransitioning = true;
|
||||
const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance);
|
||||
if (!roomId) throw new Error("Could not find the room from its exit key: " + exitKey);
|
||||
if (hash) {
|
||||
urlManager.pushStartLayerNameToUrl(hash);
|
||||
|
||||
let targetRoom: Room;
|
||||
try {
|
||||
targetRoom = await Room.createRoom(roomUrl);
|
||||
} catch (e) {
|
||||
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
|
||||
this.mapTransitioning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomUrl.hash) {
|
||||
urlManager.pushStartLayerNameToUrl(roomUrl.hash);
|
||||
}
|
||||
|
||||
const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene;
|
||||
menuScene.reset();
|
||||
if (roomId !== this.scene.key) {
|
||||
if (this.scene.get(roomId) === null) {
|
||||
console.error("next room not loaded", exitKey);
|
||||
|
||||
if (!targetRoom.isEqual(this.room)) {
|
||||
if (this.scene.get(targetRoom.key) === null) {
|
||||
console.error("next room not loaded", targetRoom.key);
|
||||
return;
|
||||
}
|
||||
this.cleanupClosingScene();
|
||||
this.scene.stop();
|
||||
this.scene.start(targetRoom.key);
|
||||
this.scene.remove(this.scene.key);
|
||||
this.scene.start(roomId);
|
||||
} else {
|
||||
//if the exit points to the current map, we simply teleport the user back to the startLayer
|
||||
this.startPositionCalculator.initPositionFromLayerName(hash, hash);
|
||||
this.startPositionCalculator.initPositionFromLayerName(roomUrl.hash, roomUrl.hash);
|
||||
this.CurrentPlayer.x = this.startPositionCalculator.startPosition.x;
|
||||
this.CurrentPlayer.y = this.startPositionCalculator.startPosition.y;
|
||||
setTimeout(() => (this.mapTransitioning = false), 500);
|
||||
@ -1182,6 +1271,8 @@ ${escapedMessage}
|
||||
this.chatVisibilityUnsubscribe();
|
||||
this.biggestAvailableAreaStoreUnsubscribe();
|
||||
iframeListener.unregisterAnswerer("getState");
|
||||
iframeListener.unregisterAnswerer("loadTileset");
|
||||
this.sharedVariablesManager?.close();
|
||||
|
||||
mediaManager.hideGameOverlay();
|
||||
|
||||
@ -1221,12 +1312,12 @@ ${escapedMessage}
|
||||
}
|
||||
|
||||
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
|
||||
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
|
||||
const properties: ITiledMapProperty[] | undefined = layer.properties;
|
||||
if (!properties) {
|
||||
return undefined;
|
||||
}
|
||||
const obj = properties.find(
|
||||
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
|
||||
(property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase()
|
||||
);
|
||||
if (obj === undefined) {
|
||||
return undefined;
|
||||
@ -1235,20 +1326,27 @@ ${escapedMessage}
|
||||
}
|
||||
|
||||
private getProperties(layer: ITiledMapLayer | ITiledMap, name: string): (string | number | boolean | undefined)[] {
|
||||
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
|
||||
const properties: ITiledMapProperty[] | undefined = layer.properties;
|
||||
if (!properties) {
|
||||
return [];
|
||||
}
|
||||
return properties
|
||||
.filter((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase())
|
||||
.filter((property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase())
|
||||
.map((property) => property.value);
|
||||
}
|
||||
|
||||
private loadNextGameFromExitUrl(exitUrl: string): Promise<void> {
|
||||
return this.loadNextGame(Room.getRoomPathFromExitUrl(exitUrl, window.location.toString()));
|
||||
}
|
||||
|
||||
//todo: push that into the gameManager
|
||||
private loadNextGame(exitSceneIdentifier: string): Promise<void> {
|
||||
const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
|
||||
const room = new Room(roomId);
|
||||
return gameManager.loadMap(room).catch(() => {});
|
||||
private async loadNextGame(exitRoomPath: URL): Promise<void> {
|
||||
try {
|
||||
const room = await Room.createRoom(exitRoomPath);
|
||||
return gameManager.loadMap(room);
|
||||
} catch (e) {
|
||||
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
|
||||
}
|
||||
}
|
||||
|
||||
//todo: in a dedicated class/function?
|
||||
@ -1691,7 +1789,7 @@ ${escapedMessage}
|
||||
this.scene.start(ErrorSceneName, {
|
||||
title: "Banned",
|
||||
subTitle: "You were banned from WorkAdventure",
|
||||
message: "If you want more information, you may contact us at: workadventure@thecodingmachine.com",
|
||||
message: "If you want more information, you may contact us at: hello@workadventu.re",
|
||||
});
|
||||
}
|
||||
|
||||
@ -1706,14 +1804,14 @@ ${escapedMessage}
|
||||
this.scene.start(ErrorSceneName, {
|
||||
title: "Connection rejected",
|
||||
subTitle: "The world you are trying to join is full. Try again later.",
|
||||
message: "If you want more information, you may contact us at: workadventure@thecodingmachine.com",
|
||||
message: "If you want more information, you may contact us at: hello@workadventu.re",
|
||||
});
|
||||
} else {
|
||||
this.scene.start(ErrorSceneName, {
|
||||
title: "Connection rejected",
|
||||
subTitle: "You cannot join the World. Try again later. \n\r \n\r Error: " + message + ".",
|
||||
message:
|
||||
"If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com",
|
||||
"If you want more information, you may contact administrator or contact us at: hello@workadventu.re",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
167
front/src/Phaser/Game/SharedVariablesManager.ts
Normal file
167
front/src/Phaser/Game/SharedVariablesManager.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import type { RoomConnection } from "../../Connexion/RoomConnection";
|
||||
import { iframeListener } from "../../Api/IframeListener";
|
||||
import type { Subscription } from "rxjs";
|
||||
import type { GameMap } from "./GameMap";
|
||||
import type { ITile, ITiledMapObject } from "../Map/ITiledMap";
|
||||
import type { Var } from "svelte/types/compiler/interfaces";
|
||||
import { init } from "svelte/internal";
|
||||
|
||||
interface Variable {
|
||||
defaultValue: unknown;
|
||||
readableBy?: string;
|
||||
writableBy?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores variables and provides a bridge between scripts and the pusher server.
|
||||
*/
|
||||
export class SharedVariablesManager {
|
||||
private _variables = new Map<string, unknown>();
|
||||
private variableObjects: Map<string, Variable>;
|
||||
|
||||
constructor(
|
||||
private roomConnection: RoomConnection,
|
||||
private gameMap: GameMap,
|
||||
serverVariables: Map<string, unknown>
|
||||
) {
|
||||
// We initialize the list of variable object at room start. The objects cannot be edited later
|
||||
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
|
||||
this.variableObjects = SharedVariablesManager.findVariablesInMap(gameMap);
|
||||
|
||||
// Let's initialize default values
|
||||
for (const [name, variableObject] of this.variableObjects.entries()) {
|
||||
if (variableObject.readableBy && !this.roomConnection.hasTag(variableObject.readableBy)) {
|
||||
// Do not initialize default value for variables that are not readable
|
||||
continue;
|
||||
}
|
||||
|
||||
this._variables.set(name, variableObject.defaultValue);
|
||||
}
|
||||
|
||||
// Override default values with the variables from the server:
|
||||
for (const [name, value] of serverVariables) {
|
||||
this._variables.set(name, value);
|
||||
}
|
||||
|
||||
roomConnection.onSetVariable((name, value) => {
|
||||
this._variables.set(name, value);
|
||||
|
||||
// On server change, let's notify the iframes
|
||||
iframeListener.setVariable({
|
||||
key: name,
|
||||
value: value,
|
||||
});
|
||||
});
|
||||
|
||||
// When a variable is modified from an iFrame
|
||||
iframeListener.registerAnswerer("setVariable", (event, source) => {
|
||||
const key = event.key;
|
||||
|
||||
const object = this.variableObjects.get(key);
|
||||
|
||||
if (object === undefined) {
|
||||
const errMsg =
|
||||
'A script is trying to modify variable "' +
|
||||
key +
|
||||
'" but this variable is not defined in the map.' +
|
||||
'There should be an object in the map whose name is "' +
|
||||
key +
|
||||
'" and whose type is "variable"';
|
||||
console.error(errMsg);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) {
|
||||
const errMsg =
|
||||
'A script is trying to modify variable "' +
|
||||
key +
|
||||
'" but this variable is only writable for users with tag "' +
|
||||
object.writableBy +
|
||||
'".';
|
||||
console.error(errMsg);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
// Let's stop any propagation of the value we set is the same as the existing value.
|
||||
if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._variables.set(key, event.value);
|
||||
|
||||
// Dispatch to the room connection.
|
||||
this.roomConnection.emitSetVariableEvent(key, event.value);
|
||||
|
||||
// Dispatch to other iframes
|
||||
iframeListener.dispatchVariableToOtherIframes(key, event.value, source);
|
||||
});
|
||||
}
|
||||
|
||||
private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {
|
||||
const objects = new Map<string, Variable>();
|
||||
for (const layer of gameMap.getMap().layers) {
|
||||
if (layer.type === "objectgroup") {
|
||||
for (const object of layer.objects) {
|
||||
if (object.type === "variable") {
|
||||
if (object.template) {
|
||||
console.warn(
|
||||
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
|
||||
);
|
||||
}
|
||||
|
||||
// We store a copy of the object (to make it immutable)
|
||||
objects.set(object.name, this.iTiledObjectToVariable(object));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
|
||||
const variable: Variable = {
|
||||
defaultValue: undefined,
|
||||
};
|
||||
|
||||
if (object.properties) {
|
||||
for (const property of object.properties) {
|
||||
const value = property.value;
|
||||
switch (property.name) {
|
||||
case "default":
|
||||
variable.defaultValue = value;
|
||||
break;
|
||||
case "writableBy":
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(
|
||||
'The writableBy property of variable "' + object.name + '" must be a string'
|
||||
);
|
||||
}
|
||||
if (value) {
|
||||
variable.writableBy = value;
|
||||
}
|
||||
break;
|
||||
case "readableBy":
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(
|
||||
'The readableBy property of variable "' + object.name + '" must be a string'
|
||||
);
|
||||
}
|
||||
if (value) {
|
||||
variable.readableBy = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return variable;
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
iframeListener.unregisterAnswerer("setVariable");
|
||||
}
|
||||
|
||||
get variables(): Map<string, unknown> {
|
||||
return this._variables;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import type { PositionInterface } from "../../Connexion/ConnexionModels";
|
||||
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
|
||||
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
|
||||
import type { GameMap } from "./GameMap";
|
||||
|
||||
const defaultStartLayerName = "start";
|
||||
@ -45,7 +45,7 @@ export class StartPositionCalculator {
|
||||
/**
|
||||
*
|
||||
* @param selectedLayer this is always the layer that is selected with the hash in the url
|
||||
* @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} didnt yield any start points
|
||||
* @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} did not yield any start points
|
||||
*/
|
||||
public initPositionFromLayerName(selectedOrDefaultLayer: string | null, selectedLayer: string | null) {
|
||||
if (!selectedOrDefaultLayer) {
|
||||
@ -73,7 +73,7 @@ export class StartPositionCalculator {
|
||||
/**
|
||||
*
|
||||
* @param selectedLayer this is always the layer that is selected with the hash in the url
|
||||
* @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} didnt yield any start points
|
||||
* @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} did not yield any start points
|
||||
*/
|
||||
private startUser(selectedOrDefaultLayer: ITiledMapTileLayer, selectedLayer: string | null): PositionInterface {
|
||||
const tiles = selectedOrDefaultLayer.data;
|
||||
@ -112,12 +112,12 @@ export class StartPositionCalculator {
|
||||
}
|
||||
|
||||
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
|
||||
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
|
||||
const properties: ITiledMapProperty[] | undefined = layer.properties;
|
||||
if (!properties) {
|
||||
return undefined;
|
||||
}
|
||||
const obj = properties.find(
|
||||
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
|
||||
(property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase()
|
||||
);
|
||||
if (obj === undefined) {
|
||||
return undefined;
|
||||
|
@ -244,6 +244,7 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
update(time: number, delta: number): void {
|
||||
if (this.lazyloadingAttempt) {
|
||||
this.moveLayers();
|
||||
this.doMoveCursorHorizontally(this.moveHorizontally);
|
||||
this.lazyloadingAttempt = false;
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {gameManager} from "../Game/GameManager";
|
||||
import {Scene} from "phaser";
|
||||
import {ErrorScene} from "../Reconnecting/ErrorScene";
|
||||
import {WAError} from "../Reconnecting/WAError";
|
||||
import {waScaleManager} from "../Services/WaScaleManager";
|
||||
import { gameManager } from "../Game/GameManager";
|
||||
import { Scene } from "phaser";
|
||||
import { ErrorScene } from "../Reconnecting/ErrorScene";
|
||||
import { WAError } from "../Reconnecting/WAError";
|
||||
import { waScaleManager } from "../Services/WaScaleManager";
|
||||
|
||||
export const EntrySceneName = "EntryScene";
|
||||
|
||||
@ -13,23 +13,29 @@ export const EntrySceneName = "EntryScene";
|
||||
export class EntryScene extends Scene {
|
||||
constructor() {
|
||||
super({
|
||||
key: EntrySceneName
|
||||
key: EntrySceneName,
|
||||
});
|
||||
}
|
||||
|
||||
create() {
|
||||
|
||||
gameManager.init(this.scene).then((nextSceneName) => {
|
||||
gameManager
|
||||
.init(this.scene)
|
||||
.then((nextSceneName) => {
|
||||
// Let's rescale before starting the game
|
||||
// We can do it at this stage.
|
||||
waScaleManager.applyNewSize();
|
||||
this.scene.start(nextSceneName);
|
||||
}).catch((err) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response && err.response.status == 404) {
|
||||
ErrorScene.showError(new WAError(
|
||||
'Access link incorrect',
|
||||
'Could not find map. Please check your access link.',
|
||||
'If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com'), this.scene);
|
||||
ErrorScene.showError(
|
||||
new WAError(
|
||||
"Access link incorrect",
|
||||
"Could not find map. Please check your access link.",
|
||||
"If you want more information, you may contact administrator or contact us at: hello@workadventu.re"
|
||||
),
|
||||
this.scene
|
||||
);
|
||||
} else {
|
||||
ErrorScene.showError(err, this.scene);
|
||||
}
|
||||
|
@ -152,6 +152,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
});
|
||||
this.players.push(player);
|
||||
}
|
||||
if (this.currentSelectUser >= this.players.length) {
|
||||
this.currentSelectUser = 0;
|
||||
}
|
||||
this.selectedPlayer = this.players[this.currentSelectUser];
|
||||
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export interface ITiledMap {
|
||||
* Map orientation (orthogonal)
|
||||
*/
|
||||
orientation: string;
|
||||
properties?: ITiledMapLayerProperty[];
|
||||
properties?: ITiledMapProperty[];
|
||||
|
||||
/**
|
||||
* Render order (right-down)
|
||||
@ -33,7 +33,7 @@ export interface ITiledMap {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface ITiledMapLayerProperty {
|
||||
export interface ITiledMapProperty {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string | boolean | number | undefined;
|
||||
@ -51,7 +51,7 @@ export interface ITiledMapGroupLayer {
|
||||
id?: number;
|
||||
name: string;
|
||||
opacity: number;
|
||||
properties?: ITiledMapLayerProperty[];
|
||||
properties?: ITiledMapProperty[];
|
||||
|
||||
type: "group";
|
||||
visible: boolean;
|
||||
@ -69,7 +69,7 @@ export interface ITiledMapTileLayer {
|
||||
height: number;
|
||||
name: string;
|
||||
opacity: number;
|
||||
properties?: ITiledMapLayerProperty[];
|
||||
properties?: ITiledMapProperty[];
|
||||
encoding?: string;
|
||||
compression?: string;
|
||||
|
||||
@ -91,7 +91,7 @@ export interface ITiledMapObjectLayer {
|
||||
height: number;
|
||||
name: string;
|
||||
opacity: number;
|
||||
properties?: ITiledMapLayerProperty[];
|
||||
properties?: ITiledMapProperty[];
|
||||
encoding?: string;
|
||||
compression?: string;
|
||||
|
||||
@ -117,7 +117,7 @@ export interface ITiledMapObject {
|
||||
gid: number;
|
||||
height: number;
|
||||
name: string;
|
||||
properties: { [key: string]: string };
|
||||
properties?: ITiledMapProperty[];
|
||||
rotation: number;
|
||||
type: string;
|
||||
visible: boolean;
|
||||
@ -141,6 +141,7 @@ export interface ITiledMapObject {
|
||||
polyline: { x: number; y: number }[];
|
||||
|
||||
text?: ITiledText;
|
||||
template?: string;
|
||||
}
|
||||
|
||||
export interface ITiledText {
|
||||
@ -163,7 +164,7 @@ export interface ITiledTileSet {
|
||||
imagewidth: number;
|
||||
margin: number;
|
||||
name: string;
|
||||
properties: { [key: string]: string };
|
||||
properties?: ITiledMapProperty[];
|
||||
spacing: number;
|
||||
tilecount: number;
|
||||
tileheight: number;
|
||||
@ -182,7 +183,7 @@ export interface ITile {
|
||||
id: number;
|
||||
type?: string;
|
||||
|
||||
properties?: Array<ITiledMapLayerProperty>;
|
||||
properties?: ITiledMapProperty[];
|
||||
}
|
||||
|
||||
export interface ITiledMapTerrain {
|
||||
|
@ -6,8 +6,6 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||
import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
import { GameConnexionTypes } from "../../Url/UrlManager";
|
||||
import { WarningContainer, warningContainerHtml, warningContainerKey } from "../Components/WarningContainer";
|
||||
import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream";
|
||||
import { menuIconVisible, menuVisible } from "../../Stores/MenuStore";
|
||||
import { videoConstraintStore } from "../../Stores/MediaStore";
|
||||
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
|
||||
@ -18,6 +16,10 @@ import { registerMenuCommandStream } from "../../Api/Events/ui/MenuItemRegisterE
|
||||
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
|
||||
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||
import { get } from "svelte/store";
|
||||
import { playersStore } from "../../Stores/PlayersStore";
|
||||
import { mediaManager } from "../../WebRtc/MediaManager";
|
||||
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
|
||||
|
||||
export const MenuSceneName = "MenuScene";
|
||||
const gameMenuKey = "gameMenu";
|
||||
@ -42,8 +44,6 @@ export class MenuScene extends Phaser.Scene {
|
||||
private gameQualityValue: number;
|
||||
private videoQualityValue: number;
|
||||
private menuButton!: Phaser.GameObjects.DOMElement;
|
||||
private warningContainer: WarningContainer | null = null;
|
||||
private warningContainerTimeout: NodeJS.Timeout | null = null;
|
||||
private subscriptions = new Subscription();
|
||||
constructor() {
|
||||
super({ key: MenuSceneName });
|
||||
@ -88,7 +88,6 @@ export class MenuScene extends Phaser.Scene {
|
||||
this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html");
|
||||
this.load.html(gameShare, "resources/html/gameShare.html");
|
||||
this.load.html(gameReportKey, gameReportRessource);
|
||||
this.load.html(warningContainerKey, warningContainerHtml);
|
||||
}
|
||||
|
||||
create() {
|
||||
@ -96,6 +95,10 @@ export class MenuScene extends Phaser.Scene {
|
||||
this.menuElement.setOrigin(0);
|
||||
MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
|
||||
|
||||
if (mediaManager.hasNotification()) {
|
||||
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
|
||||
}
|
||||
|
||||
const middleX = window.innerWidth / 3 - 298;
|
||||
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
|
||||
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, "gameQuality");
|
||||
@ -135,7 +138,9 @@ export class MenuScene extends Phaser.Scene {
|
||||
this.menuElement.addListener("click");
|
||||
this.menuElement.on("click", this.onMenuClick.bind(this));
|
||||
|
||||
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
|
||||
chatVisibilityStore.subscribe((v) => {
|
||||
this.menuButton.setVisible(!v);
|
||||
});
|
||||
}
|
||||
|
||||
//todo put this method in a parent menuElement class
|
||||
@ -182,20 +187,6 @@ export class MenuScene extends Phaser.Scene {
|
||||
});*/
|
||||
}
|
||||
|
||||
private showWorldCapacityWarning() {
|
||||
if (!this.warningContainer) {
|
||||
this.warningContainer = new WarningContainer(this);
|
||||
}
|
||||
if (this.warningContainerTimeout) {
|
||||
clearTimeout(this.warningContainerTimeout);
|
||||
}
|
||||
this.warningContainerTimeout = setTimeout(() => {
|
||||
this.warningContainer?.destroy();
|
||||
this.warningContainer = null;
|
||||
this.warningContainerTimeout = null;
|
||||
}, 120000);
|
||||
}
|
||||
|
||||
private closeSideMenu(): void {
|
||||
menuVisible.set(false);
|
||||
/* if (!this.sideMenuOpened) return;
|
||||
@ -352,9 +343,15 @@ export class MenuScene extends Phaser.Scene {
|
||||
case "editGameSettingsButton":
|
||||
this.openGameSettingsMenu();
|
||||
break;
|
||||
case "oidcLogin":
|
||||
connectionManager.loadOpenIDScreen();
|
||||
break;
|
||||
case "toggleFullscreen":
|
||||
this.toggleFullscreen();
|
||||
break;
|
||||
case "enableNotification":
|
||||
this.enableNotification();
|
||||
break;
|
||||
case "adminConsoleButton":
|
||||
if (get(consoleGlobalMessageManagerVisibleStore)) {
|
||||
consoleGlobalMessageManagerVisibleStore.set(false);
|
||||
@ -389,6 +386,10 @@ export class MenuScene extends Phaser.Scene {
|
||||
private gotToCreateMapPage() {
|
||||
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
|
||||
//TODO fix me: this button can to send us on WorkAdventure BO.
|
||||
//const sparkHost = ADMIN_URL + "/getting-started";
|
||||
|
||||
//The redirection must be only on workadventu.re domain
|
||||
//To day the domain staging cannot be use by customer
|
||||
const sparkHost = "https://workadventu.re/getting-started";
|
||||
window.open(sparkHost, "_blank");
|
||||
}
|
||||
@ -417,4 +418,12 @@ export class MenuScene extends Phaser.Scene {
|
||||
public isDirty(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
private enableNotification() {
|
||||
mediaManager.requestNotification().then(() => {
|
||||
if (mediaManager.hasNotification()) {
|
||||
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import {TextField} from "../Components/TextField";
|
||||
import { TextField } from "../Components/TextField";
|
||||
import Image = Phaser.GameObjects.Image;
|
||||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
import Text = Phaser.GameObjects.Text;
|
||||
import ScenePlugin = Phaser.Scenes.ScenePlugin;
|
||||
import {WAError} from "./WAError";
|
||||
import { WAError } from "./WAError";
|
||||
|
||||
export const ErrorSceneName = "ErrorScene";
|
||||
enum Textures {
|
||||
icon = "icon",
|
||||
mainFont = "main_font"
|
||||
mainFont = "main_font",
|
||||
}
|
||||
|
||||
export class ErrorScene extends Phaser.Scene {
|
||||
@ -23,25 +23,21 @@ export class ErrorScene extends Phaser.Scene {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
key: ErrorSceneName
|
||||
key: ErrorSceneName,
|
||||
});
|
||||
}
|
||||
|
||||
init({title, subTitle, message}: { title?: string, subTitle?: string, message?: string }) {
|
||||
this.title = title ? title : '';
|
||||
this.subTitle = subTitle ? subTitle : '';
|
||||
this.message = message ? message : '';
|
||||
init({ title, subTitle, message }: { title?: string; subTitle?: string; message?: string }) {
|
||||
this.title = title ? title : "";
|
||||
this.subTitle = subTitle ? subTitle : "";
|
||||
this.message = message ? message : "";
|
||||
}
|
||||
|
||||
preload() {
|
||||
this.load.image(Textures.icon, "resources/logos/tcm_full.png");
|
||||
this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png");
|
||||
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
|
||||
this.load.bitmapFont(Textures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
|
||||
this.load.spritesheet(
|
||||
'cat',
|
||||
'resources/characters/pipoya/Cat 01-1.png',
|
||||
{frameWidth: 32, frameHeight: 32}
|
||||
);
|
||||
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
|
||||
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
|
||||
}
|
||||
|
||||
create() {
|
||||
@ -50,15 +46,25 @@ export class ErrorScene extends Phaser.Scene {
|
||||
|
||||
this.titleField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, this.title);
|
||||
|
||||
this.subTitleField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2 + 24, this.subTitle);
|
||||
this.subTitleField = new TextField(
|
||||
this,
|
||||
this.game.renderer.width / 2,
|
||||
this.game.renderer.height / 2 + 24,
|
||||
this.subTitle
|
||||
);
|
||||
|
||||
this.messageField = this.add.text(this.game.renderer.width / 2, this.game.renderer.height / 2 + 48, this.message, {
|
||||
this.messageField = this.add.text(
|
||||
this.game.renderer.width / 2,
|
||||
this.game.renderer.height / 2 + 48,
|
||||
this.message,
|
||||
{
|
||||
fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif',
|
||||
fontSize: '10px'
|
||||
});
|
||||
fontSize: "10px",
|
||||
}
|
||||
);
|
||||
this.messageField.setOrigin(0.5, 0.5);
|
||||
|
||||
this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat', 6);
|
||||
this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat", 6);
|
||||
this.cat.flipY = true;
|
||||
}
|
||||
|
||||
@ -69,38 +75,38 @@ export class ErrorScene extends Phaser.Scene {
|
||||
public static showError(error: any, scene: ScenePlugin): void {
|
||||
console.error(error);
|
||||
|
||||
if (typeof error === 'string' || error instanceof String) {
|
||||
if (typeof error === "string" || error instanceof String) {
|
||||
scene.start(ErrorSceneName, {
|
||||
title: 'An error occurred',
|
||||
subTitle: error
|
||||
title: "An error occurred",
|
||||
subTitle: error,
|
||||
});
|
||||
} else if (error instanceof WAError) {
|
||||
scene.start(ErrorSceneName, {
|
||||
title: error.title,
|
||||
subTitle: error.subTitle,
|
||||
message: error.details
|
||||
message: error.details,
|
||||
});
|
||||
} else if (error.response) {
|
||||
// Axios HTTP error
|
||||
// client received an error response (5xx, 4xx)
|
||||
scene.start(ErrorSceneName, {
|
||||
title: 'HTTP ' + error.response.status + ' - ' + error.response.statusText,
|
||||
subTitle: 'An error occurred while accessing URL:',
|
||||
message: error.response.config.url
|
||||
title: "HTTP " + error.response.status + " - " + error.response.statusText,
|
||||
subTitle: "An error occurred while accessing URL:",
|
||||
message: error.response.config.url,
|
||||
});
|
||||
} else if (error.request) {
|
||||
// Axios HTTP error
|
||||
// client never received a response, or request never left
|
||||
scene.start(ErrorSceneName, {
|
||||
title: 'Network error',
|
||||
subTitle: error.message
|
||||
title: "Network error",
|
||||
subTitle: error.message,
|
||||
});
|
||||
} else if (error instanceof Error) {
|
||||
// Error
|
||||
scene.start(ErrorSceneName, {
|
||||
title: 'An error occurred',
|
||||
title: "An error occurred",
|
||||
subTitle: error.name,
|
||||
message: error.message
|
||||
message: error.message,
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
@ -114,7 +120,7 @@ export class ErrorScene extends Phaser.Scene {
|
||||
scene.start(ErrorSceneName, {
|
||||
title,
|
||||
subTitle,
|
||||
message
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {TextField} from "../Components/TextField";
|
||||
import { TextField } from "../Components/TextField";
|
||||
import Image = Phaser.GameObjects.Image;
|
||||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
|
||||
export const ReconnectingSceneName = "ReconnectingScene";
|
||||
enum ReconnectingTextures {
|
||||
icon = "icon",
|
||||
mainFont = "main_font"
|
||||
mainFont = "main_font",
|
||||
}
|
||||
|
||||
export class ReconnectingScene extends Phaser.Scene {
|
||||
@ -14,35 +14,40 @@ export class ReconnectingScene extends Phaser.Scene {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
key: ReconnectingSceneName
|
||||
key: ReconnectingSceneName,
|
||||
});
|
||||
}
|
||||
|
||||
preload() {
|
||||
this.load.image(ReconnectingTextures.icon, "resources/logos/tcm_full.png");
|
||||
this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
|
||||
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
|
||||
this.load.bitmapFont(ReconnectingTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
|
||||
this.load.spritesheet(
|
||||
'cat',
|
||||
'resources/characters/pipoya/Cat 01-1.png',
|
||||
{frameWidth: 32, frameHeight: 32}
|
||||
);
|
||||
this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
|
||||
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
|
||||
}
|
||||
|
||||
create() {
|
||||
this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, ReconnectingTextures.icon);
|
||||
this.logo = new Image(
|
||||
this,
|
||||
this.game.renderer.width - 30,
|
||||
this.game.renderer.height - 20,
|
||||
ReconnectingTextures.icon
|
||||
);
|
||||
this.add.existing(this.logo);
|
||||
|
||||
this.reconnectingField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, "Connection lost. Reconnecting...");
|
||||
this.reconnectingField = new TextField(
|
||||
this,
|
||||
this.game.renderer.width / 2,
|
||||
this.game.renderer.height / 2,
|
||||
"Connection lost. Reconnecting..."
|
||||
);
|
||||
|
||||
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat');
|
||||
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
|
||||
this.anims.create({
|
||||
key: 'right',
|
||||
frames: this.anims.generateFrameNumbers('cat', { start: 6, end: 8 }),
|
||||
key: "right",
|
||||
frames: this.anims.generateFrameNumbers("cat", { start: 6, end: 8 }),
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
repeat: -1,
|
||||
});
|
||||
cat.play('right');
|
||||
|
||||
cat.play("right");
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import type { Direction } from "../../types";
|
||||
import type {GameScene} from "../Game/GameScene";
|
||||
import {touchScreenManager} from "../../Touch/TouchScreenManager";
|
||||
import {MobileJoystick} from "../Components/MobileJoystick";
|
||||
import {enableUserInputsStore} from "../../Stores/UserInputStore";
|
||||
import type { GameScene } from "../Game/GameScene";
|
||||
import { touchScreenManager } from "../../Touch/TouchScreenManager";
|
||||
import { MobileJoystick } from "../Components/MobileJoystick";
|
||||
import { enableUserInputsStore } from "../../Stores/UserInputStore";
|
||||
|
||||
interface UserInputManagerDatum {
|
||||
keyInstance: Phaser.Input.Keyboard.Key;
|
||||
event: UserInputEvent
|
||||
event: UserInputEvent;
|
||||
}
|
||||
|
||||
export enum UserInputEvent {
|
||||
@ -20,10 +20,9 @@ export enum UserInputEvent {
|
||||
JoystickMove,
|
||||
}
|
||||
|
||||
|
||||
//we cannot use a map structure so we have to create a replacment
|
||||
//we cannot use a map structure so we have to create a replacement
|
||||
export class ActiveEventList {
|
||||
private eventMap : Map<UserInputEvent, boolean> = new Map<UserInputEvent, boolean>();
|
||||
private eventMap: Map<UserInputEvent, boolean> = new Map<UserInputEvent, boolean>();
|
||||
|
||||
get(event: UserInputEvent): boolean {
|
||||
return this.eventMap.get(event) || false;
|
||||
@ -43,7 +42,7 @@ export class ActiveEventList {
|
||||
export class UserInputManager {
|
||||
private KeysCode!: UserInputManagerDatum[];
|
||||
private Scene: GameScene;
|
||||
private isInputDisabled : boolean;
|
||||
private isInputDisabled: boolean;
|
||||
|
||||
private joystick!: MobileJoystick;
|
||||
private joystickEvents = new ActiveEventList();
|
||||
@ -61,8 +60,8 @@ export class UserInputManager {
|
||||
}
|
||||
|
||||
enableUserInputsStore.subscribe((enable) => {
|
||||
enable ? this.restoreControls() : this.disableControls()
|
||||
})
|
||||
enable ? this.restoreControls() : this.disableControls();
|
||||
});
|
||||
}
|
||||
|
||||
initVirtualJoystick() {
|
||||
@ -91,39 +90,81 @@ export class UserInputManager {
|
||||
});
|
||||
}
|
||||
|
||||
initKeyBoardEvent(){
|
||||
initKeyBoardEvent() {
|
||||
this.KeysCode = [
|
||||
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false) },
|
||||
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false) },
|
||||
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false) },
|
||||
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false) },
|
||||
{event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false) },
|
||||
{event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false) },
|
||||
{
|
||||
event: UserInputEvent.MoveUp,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false),
|
||||
},
|
||||
{
|
||||
event: UserInputEvent.MoveUp,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false),
|
||||
},
|
||||
{
|
||||
event: UserInputEvent.MoveLeft,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false),
|
||||
},
|
||||
{
|
||||
event: UserInputEvent.MoveLeft,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false),
|
||||
},
|
||||
{
|
||||
event: UserInputEvent.MoveDown,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false),
|
||||
},
|
||||
{
|
||||
event: UserInputEvent.MoveRight,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false),
|
||||
},
|
||||
|
||||
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false) },
|
||||
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false) },
|
||||
{event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false) },
|
||||
{event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false) },
|
||||
{
|
||||
event: UserInputEvent.MoveUp,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false),
|
||||
},
|
||||
{
|
||||
event: UserInputEvent.MoveLeft,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false),
|
||||
},
|
||||
{
|
||||
event: UserInputEvent.MoveDown,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false),
|
||||
},
|
||||
{
|
||||
event: UserInputEvent.MoveRight,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false),
|
||||
},
|
||||
|
||||
{event: UserInputEvent.SpeedUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false) },
|
||||
{
|
||||
event: UserInputEvent.SpeedUp,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false),
|
||||
},
|
||||
|
||||
{event: UserInputEvent.Interact, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false) },
|
||||
{event: UserInputEvent.Interact, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false) },
|
||||
{event: UserInputEvent.Shout, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false) },
|
||||
{
|
||||
event: UserInputEvent.Interact,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false),
|
||||
},
|
||||
{
|
||||
event: UserInputEvent.Interact,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false),
|
||||
},
|
||||
{
|
||||
event: UserInputEvent.Shout,
|
||||
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
clearAllListeners(){
|
||||
clearAllListeners() {
|
||||
this.Scene.input.keyboard.removeAllListeners();
|
||||
}
|
||||
|
||||
//todo: should we also disable the joystick?
|
||||
disableControls(){
|
||||
disableControls() {
|
||||
this.Scene.input.keyboard.removeAllKeys();
|
||||
this.isInputDisabled = true;
|
||||
}
|
||||
|
||||
restoreControls(){
|
||||
restoreControls() {
|
||||
this.initKeyBoardEvent();
|
||||
this.isInputDisabled = false;
|
||||
}
|
||||
@ -155,7 +196,7 @@ export class UserInputManager {
|
||||
}
|
||||
});
|
||||
eventsMap.set(UserInputEvent.JoystickMove, this.joystickEvents.any());
|
||||
this.KeysCode.forEach(d => {
|
||||
this.KeysCode.forEach((d) => {
|
||||
if (d.keyInstance.isDown) {
|
||||
eventsMap.set(d.event, true);
|
||||
}
|
||||
@ -163,18 +204,18 @@ export class UserInputManager {
|
||||
return eventsMap;
|
||||
}
|
||||
|
||||
spaceEvent(callback : Function){
|
||||
this.Scene.input.keyboard.on('keyup-SPACE', (event: Event) => {
|
||||
spaceEvent(callback: Function) {
|
||||
this.Scene.input.keyboard.on("keyup-SPACE", (event: Event) => {
|
||||
callback();
|
||||
return event;
|
||||
});
|
||||
}
|
||||
|
||||
addSpaceEventListner(callback : Function){
|
||||
this.Scene.input.keyboard.addListener('keyup-SPACE', callback);
|
||||
addSpaceEventListner(callback: Function) {
|
||||
this.Scene.input.keyboard.addListener("keyup-SPACE", callback);
|
||||
}
|
||||
removeSpaceEventListner(callback : Function){
|
||||
this.Scene.input.keyboard.removeListener('keyup-SPACE', callback);
|
||||
removeSpaceEventListner(callback: Function) {
|
||||
this.Scene.input.keyboard.removeListener("keyup-SPACE", callback);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
@ -182,8 +223,11 @@ export class UserInputManager {
|
||||
}
|
||||
|
||||
private initMouseWheel() {
|
||||
this.Scene.input.on('wheel', (pointer: unknown, gameObjects: unknown, deltaX: number, deltaY: number, deltaZ: number) => {
|
||||
this.Scene.zoomByFactor(1 - deltaY / 53 * 0.1);
|
||||
});
|
||||
this.Scene.input.on(
|
||||
"wheel",
|
||||
(pointer: unknown, gameObjects: unknown, deltaX: number, deltaY: number, deltaZ: number) => {
|
||||
this.Scene.zoomByFactor(1 - (deltaY / 53) * 0.1);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +96,7 @@ function createChatMessagesStore() {
|
||||
}
|
||||
return list;
|
||||
});
|
||||
chatVisibilityStore.set(true);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -2,4 +2,6 @@ import { writable } from "svelte/store";
|
||||
|
||||
export const userMovingStore = writable(false);
|
||||
|
||||
export const requestVisitCardsStore = writable<string|null>(null);
|
||||
export const requestVisitCardsStore = writable<string | null>(null);
|
||||
|
||||
export const userIsAdminStore = writable(false);
|
||||
|
@ -274,12 +274,12 @@ export const mediaStreamConstraintsStore = derived(
|
||||
currentAudioConstraint = false;
|
||||
}
|
||||
|
||||
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone)
|
||||
// Disable webcam for privacy reasons (the game is not visible and we were talking to no one)
|
||||
if ($privacyShutdownStore === true) {
|
||||
currentVideoConstraint = false;
|
||||
}
|
||||
|
||||
// Disable webcam for energy reasons (the user is not moving and we are talking to noone)
|
||||
// Disable webcam for energy reasons (the user is not moving and we are talking to no one)
|
||||
if ($cameraEnergySavingStore === true) {
|
||||
currentVideoConstraint = false;
|
||||
currentAudioConstraint = false;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user