merged develop

This commit is contained in:
Hanusiak Piotr 2022-01-13 10:44:06 +01:00
commit a1c96b0524
178 changed files with 6744 additions and 3541 deletions

View file

@ -25,6 +25,7 @@
],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "error"
"@typescript-eslint/no-explicit-any": "error",
"no-throw-literal": "error"
}
}

View file

@ -1,11 +1,11 @@
# protobuf build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder
WORKDIR /usr/src
COPY messages .
RUN yarn install && yarn proto
# typescript build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder2
WORKDIR /usr/src
COPY back/yarn.lock back/package.json ./
RUN yarn install
@ -15,7 +15,7 @@ ENV NODE_ENV=production
RUN yarn run tsc
# final production image
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d
WORKDIR /usr/src
COPY back/yarn.lock back/package.json ./
COPY --from=builder2 /usr/src/dist /usr/src/dist

View file

@ -68,14 +68,14 @@
"@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",
"eslint": "^6.8.0",
"@typescript-eslint/eslint-plugin": "^5.8.0",
"@typescript-eslint/parser": "^5.8.0",
"eslint": "^8.5.0",
"jasmine": "^3.5.0",
"lint-staged": "^11.0.0",
"prettier": "^2.3.1",
"ts-node-dev": "^1.0.0-pre.44",
"typescript": "^3.8.3"
"ts-node-dev": "^1.1.8",
"typescript": "^4.5.4"
},
"lint-staged": {
"*.ts": [

View file

@ -1,15 +1,15 @@
// lib/server.ts
import App from "./src/App";
import grpc from "grpc";
import {roomManager} from "./src/RoomManager";
import {IRoomManagerServer, RoomManagerService} from "./src/Messages/generated/messages_grpc_pb";
import {HTTP_PORT, GRPC_PORT} from "./src/Enum/EnvironmentVariable";
import { roomManager } from "./src/RoomManager";
import { IRoomManagerServer, RoomManagerService } from "./src/Messages/generated/messages_grpc_pb";
import { HTTP_PORT, GRPC_PORT } from "./src/Enum/EnvironmentVariable";
App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT))
App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT));
const server = new grpc.Server();
server.addService<IRoomManagerServer>(RoomManagerService, roomManager);
server.bind('0.0.0.0:'+GRPC_PORT, grpc.ServerCredentials.createInsecure());
server.bind(`0.0.0.0:${GRPC_PORT}`, grpc.ServerCredentials.createInsecure());
server.start();
console.log('WorkAdventure HTTP/2 API starting on port %d!', GRPC_PORT);
console.log("WorkAdventure HTTP/2 API starting on port %d!", GRPC_PORT);

View file

@ -36,9 +36,11 @@ export class DebugController {
return "BatchedMessages";
}
if (value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
const obj: { [key: string | number]: unknown } = {};
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
if (typeof mapKey === "number" || typeof mapKey === "string") {
obj[mapKey] = mapValue;
}
}
return obj;
} else if (value instanceof Set) {

View file

@ -1,12 +1,10 @@
import { App } from "../Server/sifrr.server";
import { HttpRequest, HttpResponse } from "uWebSockets.js";
const register = require("prom-client").register;
const collectDefaultMetrics = require("prom-client").collectDefaultMetrics;
import { register, collectDefaultMetrics } from "prom-client";
export class PrometheusController {
constructor(private App: App) {
collectDefaultMetrics({
timeout: 10000,
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
});

View file

@ -2,7 +2,13 @@ import { PointInterface } from "./Websocket/PointInterface";
import { Group } from "./Group";
import { User, UserSocket } from "./User";
import { PositionInterface } from "_Model/PositionInterface";
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
import {
EmoteCallback,
EntersCallback,
LeavesCallback,
MovesCallback,
PlayerDetailsUpdatedCallback,
} from "_Model/Zone";
import { PositionNotifier } from "./PositionNotifier";
import { Movable } from "_Model/Movable";
import {
@ -11,9 +17,11 @@ import {
EmoteEventMessage,
ErrorMessage,
JoinRoomMessage,
SetPlayerDetailsMessage,
SubToPusherRoomMessage,
VariableMessage,
VariableWithTagMessage,
ServerToClientMessage,
} from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { RoomSocket, ZoneSocket } from "src/RoomManager";
@ -27,6 +35,7 @@ import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import { LocalUrlError } from "../Services/LocalUrlError";
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
import { VariableError } from "../Services/VariableError";
import { isRoomRedirect } from "../Services/AdminApi/RoomRedirect";
export type ConnectCallback = (user: User, group: Group) => void;
export type DisconnectCallback = (user: User, group: Group) => void;
@ -56,10 +65,19 @@ export class GameRoom {
onEnters: EntersCallback,
onMoves: MovesCallback,
onLeaves: LeavesCallback,
onEmote: EmoteCallback
onEmote: EmoteCallback,
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
) {
// A zone is 10 sprites wide.
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
this.positionNotifier = new PositionNotifier(
320,
320,
onEnters,
onMoves,
onLeaves,
onEmote,
onPlayerDetailsUpdated
);
}
public static async create(
@ -71,7 +89,8 @@ export class GameRoom {
onEnters: EntersCallback,
onMoves: MovesCallback,
onLeaves: LeavesCallback,
onEmote: EmoteCallback
onEmote: EmoteCallback,
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
): Promise<GameRoom> {
const mapDetails = await GameRoom.getMapDetails(roomUrl);
@ -85,16 +104,13 @@ export class GameRoom {
onEnters,
onMoves,
onLeaves,
onEmote
onEmote,
onPlayerDetailsUpdated
);
return gameRoom;
}
public getGroups(): Group[] {
return Array.from(this.groups.values());
}
public getUsers(): Map<number, User> {
return this.users;
}
@ -157,6 +173,14 @@ export class GameRoom {
if (userObj !== undefined && typeof userObj.group !== "undefined") {
this.leaveGroup(userObj);
}
if (user.hasFollowers()) {
user.stopLeading();
}
if (user.following) {
user.following.delFollower(user);
}
this.users.delete(user.id);
this.usersByUuid.delete(user.uuid);
@ -180,6 +204,14 @@ export class GameRoom {
this.updateUserGroup(user);
}
updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
if (playerDetailsMessage.getRemoveoutlinecolor()) {
user.outlineColor = undefined;
} else {
user.outlineColor = playerDetailsMessage.getOutlinecolor();
}
}
private updateUserGroup(user: User): void {
user.group?.updatePosition();
user.group?.searchForNearbyUsers();
@ -187,8 +219,8 @@ export class GameRoom {
if (user.silent) {
return;
}
if (user.group === undefined) {
const group = user.group;
if (group === undefined) {
// If the user is not part of a group:
// should he join a group?
@ -219,13 +251,40 @@ export class GameRoom {
} else {
// If the user is part of a group:
// should he leave the group?
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition());
if (distance > this.groupRadius) {
this.leaveGroup(user);
let noOneOutOfBounds = true;
group.getUsers().forEach((foreignUser: User) => {
if (foreignUser.group === undefined) {
return;
}
const usrPos = foreignUser.getPosition();
const grpPos = foreignUser.group.getPosition();
const distance = GameRoom.computeDistanceBetweenPositions(usrPos, grpPos);
if (distance > this.groupRadius) {
if (foreignUser.hasFollowers() || foreignUser.following) {
// If one user is out of the group bounds BUT following, the group still exists... but should be hidden.
// We put it in 'outOfBounds' mode
group.setOutOfBounds(true);
noOneOutOfBounds = false;
} else {
this.leaveGroup(foreignUser);
}
}
});
if (noOneOutOfBounds && !user.group?.isEmpty()) {
group.setOutOfBounds(false);
}
}
}
public sendToOthersInGroupIncludingUser(user: User, message: ServerToClientMessage): void {
user.group?.getUsers().forEach((currentUser: User) => {
if (currentUser.id !== user.id) {
currentUser.socket.write(message);
}
});
}
setSilent(user: User, silent: boolean) {
if (user.silent === silent) {
return;
@ -253,12 +312,9 @@ export class GameRoom {
}
group.leave(user);
if (group.isEmpty()) {
this.positionNotifier.leave(group);
group.destroy();
if (!this.groups.has(group)) {
throw new Error(
"Could not find group " + group.getId() + " referenced by user " + user.id + " in World."
);
throw new Error(`Could not find group ${group.getId()} referenced by user ${user.id} in World.`);
}
this.groups.delete(group);
//todo: is the group garbage collected?
@ -459,9 +515,9 @@ export class GameRoom {
}
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");
if (isRoomRedirect(result)) {
console.error("Unexpected room redirect received while querying map details", result);
throw new Error("Unexpected room redirect received while querying map details");
}
return result;
}

View file

@ -16,6 +16,10 @@ export class Group implements Movable {
private wasDestroyed: boolean = false;
private roomId: string;
private currentZone: Zone | null = null;
/**
* When outOfBounds = true, a user if out of the bounds of the group BUT still considered inside it (because we are in following mode)
*/
private outOfBounds = false;
constructor(
roomId: string,
@ -78,6 +82,10 @@ export class Group implements Movable {
this.x = x;
this.y = y;
if (this.outOfBounds) {
return;
}
if (oldX === undefined) {
this.currentZone = this.positionNotifier.enter(this);
} else {
@ -116,7 +124,7 @@ export class Group implements Movable {
leave(user: User): void {
const success = this.users.delete(user);
if (success === false) {
throw new Error("Could not find user " + user.id + " in the group " + this.id);
throw new Error(`Could not find user ${user.id} in the group ${this.id}`);
}
user.group = undefined;
@ -133,6 +141,10 @@ export class Group implements Movable {
* Usually used when there is only one user left.
*/
destroy(): void {
if (!this.outOfBounds) {
this.positionNotifier.leave(this);
}
for (const user of this.users) {
this.leave(user);
}
@ -142,4 +154,26 @@ export class Group implements Movable {
get getSize() {
return this.users.size;
}
/**
* A group can have at most one person leading the way in it.
*/
get leader(): User | undefined {
for (const user of this.users) {
if (user.hasFollowers()) {
return user;
}
}
return undefined;
}
setOutOfBounds(outOfBounds: boolean): void {
if (this.outOfBounds === true && outOfBounds === false) {
this.positionNotifier.enter(this);
this.outOfBounds = false;
} else if (this.outOfBounds === false && outOfBounds === true) {
this.positionNotifier.leave(this);
this.outOfBounds = true;
}
}
}

View file

@ -8,12 +8,19 @@
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
* number of players around the current player.
*/
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone";
import {
EmoteCallback,
EntersCallback,
LeavesCallback,
MovesCallback,
PlayerDetailsUpdatedCallback,
Zone,
} from "./Zone";
import { Movable } from "_Model/Movable";
import { PositionInterface } from "_Model/PositionInterface";
import { ZoneSocket } from "../RoomManager";
import { User } from "../Model/User";
import { EmoteEventMessage } from "../Messages/generated/messages_pb";
import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb";
interface ZoneDescriptor {
i: number;
@ -42,7 +49,8 @@ export class PositionNotifier {
private onUserEnters: EntersCallback,
private onUserMoves: MovesCallback,
private onUserLeaves: LeavesCallback,
private onEmote: EmoteCallback
private onEmote: EmoteCallback,
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
) {}
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
@ -98,7 +106,15 @@ export class PositionNotifier {
let zone = this.zones[j][i];
if (zone === undefined) {
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, this.onEmote, i, j);
zone = new Zone(
this.onUserEnters,
this.onUserMoves,
this.onUserLeaves,
this.onEmote,
this.onPlayerDetailsUpdated,
i,
j
);
this.zones[j][i] = zone;
}
return zone;
@ -132,4 +148,11 @@ export class PositionNotifier {
}
}
}
public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) {
const position = user.getPosition();
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
zone.updatePlayerDetails(user, playerDetails);
}
}

View file

@ -7,8 +7,11 @@ import { ServerDuplexStream } from "grpc";
import {
BatchMessage,
CompanionMessage,
FollowAbortMessage,
FollowConfirmationMessage,
PusherToBackMessage,
ServerToClientMessage,
SetPlayerDetailsMessage,
SubMessage,
} from "../Messages/generated/messages_pb";
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
@ -18,6 +21,8 @@ export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientM
export class User implements Movable {
public listenedZones: Set<Zone>;
public group?: Group;
private _following: User | undefined;
private followedBy: Set<User> = new Set<User>();
public constructor(
public id: number,
@ -31,7 +36,8 @@ export class User implements Movable {
public readonly visitCardUrl: string | null,
public readonly name: string,
public readonly characterLayers: CharacterLayer[],
public readonly companion?: CompanionMessage
public readonly companion?: CompanionMessage,
private _outlineColor?: number | undefined
) {
this.listenedZones = new Set<Zone>();
@ -48,6 +54,45 @@ export class User implements Movable {
this.positionNotifier.updatePosition(this, position, oldPosition);
}
public addFollower(follower: User): void {
this.followedBy.add(follower);
follower._following = this;
const message = new FollowConfirmationMessage();
message.setFollower(follower.id);
message.setLeader(this.id);
const clientMessage = new ServerToClientMessage();
clientMessage.setFollowconfirmationmessage(message);
this.socket.write(clientMessage);
}
public delFollower(follower: User): void {
this.followedBy.delete(follower);
follower._following = undefined;
const message = new FollowAbortMessage();
message.setFollower(follower.id);
message.setLeader(this.id);
const clientMessage = new ServerToClientMessage();
clientMessage.setFollowabortmessage(message);
this.socket.write(clientMessage);
follower.socket.write(clientMessage);
}
public hasFollowers(): boolean {
return this.followedBy.size !== 0;
}
get following(): User | undefined {
return this._following;
}
public stopLeading(): void {
for (const follower of this.followedBy) {
this.delFollower(follower);
}
}
private batchedMessages: BatchMessage = new BatchMessage();
private batchTimeout: NodeJS.Timeout | null = null;
@ -69,4 +114,17 @@ export class User implements Movable {
}, 100);
}
}
public set outlineColor(value: number | undefined) {
this._outlineColor = value;
const playerDetails = new SetPlayerDetailsMessage();
if (value === undefined) {
playerDetails.setRemoveoutlinecolor(true);
} else {
playerDetails.setOutlinecolor(value);
}
this.positionNotifier.updatePlayerDetails(this, playerDetails);
}
}

View file

@ -3,12 +3,20 @@ import { PositionInterface } from "_Model/PositionInterface";
import { Movable } from "./Movable";
import { Group } from "./Group";
import { ZoneSocket } from "../RoomManager";
import { EmoteEventMessage } from "../Messages/generated/messages_pb";
import {
EmoteEventMessage,
SetPlayerDetailsMessage,
PlayerDetailsUpdatedMessage,
} from "../Messages/generated/messages_pb";
export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void;
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void;
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
export type PlayerDetailsUpdatedCallback = (
playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage,
listener: ZoneSocket
) => void;
export class Zone {
private things: Set<Movable> = new Set<Movable>();
@ -19,6 +27,7 @@ export class Zone {
private onMoves: MovesCallback,
private onLeaves: LeavesCallback,
private onEmote: EmoteCallback,
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback,
public readonly x: number,
public readonly y: number
) {}
@ -30,21 +39,13 @@ export class Zone {
const result = this.things.delete(thing);
if (!result) {
if (thing instanceof User) {
throw new Error("Could not find user in zone " + thing.id);
throw new Error(`Could not find user in zone ${thing.id}`);
}
if (thing instanceof Group) {
throw new Error(
"Could not find group " +
thing.getId() +
" in zone (" +
this.x +
"," +
this.y +
"). Position of group: (" +
thing.getPosition().x +
"," +
thing.getPosition().y +
")"
`Could not find group ${thing.getId()} in zone (${this.x},${this.y}). Position of group: (${
thing.getPosition().x
},${thing.getPosition().y})`
);
}
}
@ -106,4 +107,14 @@ export class Zone {
this.onEmote(emoteEventMessage, listener);
}
}
public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) {
const playerDetailsUpdatedMessage = new PlayerDetailsUpdatedMessage();
playerDetailsUpdatedMessage.setUserid(user.id);
playerDetailsUpdatedMessage.setDetails(playerDetails);
for (const listener of this.listeners) {
this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener);
}
}
}

View file

@ -5,9 +5,13 @@ import {
AdminPusherToBackMessage,
AdminRoomMessage,
BanMessage,
BanUserMessage,
BatchToPusherMessage,
BatchToPusherRoomMessage,
EmotePromptMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
EmptyMessage,
ItemEventMessage,
JoinRoomMessage,
@ -16,7 +20,9 @@ import {
QueryJitsiJwtMessage,
RefreshRoomPromptMessage,
RoomMessage,
SendUserMessage,
ServerToAdminClientMessage,
SetPlayerDetailsMessage,
SilentMessage,
UserMovesMessage,
VariableMessage,
@ -100,11 +106,6 @@ const roomManager: IRoomManagerServer = {
user,
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
);
} else if (message.hasPlayglobalmessage()) {
socketManager.emitPlayGlobalMessage(
room,
message.getPlayglobalmessage() as PlayGlobalMessage
);
} else if (message.hasQueryjitsijwtmessage()) {
socketManager.handleQueryJitsiJwtMessage(
user,
@ -116,16 +117,37 @@ const roomManager: IRoomManagerServer = {
user,
message.getEmotepromptmessage() as EmotePromptMessage
);
} else if (message.hasFollowrequestmessage()) {
socketManager.handleFollowRequestMessage(
room,
user,
message.getFollowrequestmessage() as FollowRequestMessage
);
} else if (message.hasFollowconfirmationmessage()) {
socketManager.handleFollowConfirmationMessage(
room,
user,
message.getFollowconfirmationmessage() as FollowConfirmationMessage
);
} else if (message.hasFollowabortmessage()) {
socketManager.handleFollowAbortMessage(
room,
user,
message.getFollowabortmessage() as FollowAbortMessage
);
} else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage();
if (sendUserMessage !== undefined) {
socketManager.handlerSendUserMessage(user, sendUserMessage);
}
socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage);
} else if (message.hasBanusermessage()) {
const banUserMessage = message.getBanusermessage();
if (banUserMessage !== undefined) {
socketManager.handlerBanUserMessage(room, user, banUserMessage);
}
socketManager.handlerBanUserMessage(room, user, banUserMessage as BanUserMessage);
} else if (message.hasSetplayerdetailsmessage()) {
const setPlayerDetailsMessage = message.getSetplayerdetailsmessage();
socketManager.handleSetPlayerDetails(
room,
user,
setPlayerDetailsMessage as SetPlayerDetailsMessage
);
} else {
throw new Error("Unhandled message type");
}
@ -160,7 +182,7 @@ const roomManager: IRoomManagerServer = {
socketManager
.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
.catch((e) => {
emitErrorOnZoneSocket(call, e.toString());
emitErrorOnZoneSocket(call, e);
});
call.on("cancelled", () => {
@ -190,7 +212,7 @@ const roomManager: IRoomManagerServer = {
const roomMessage = call.request;
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => {
emitErrorOnRoomSocket(call, e.toString());
emitErrorOnRoomSocket(call, e);
});
call.on("cancelled", () => {
@ -251,7 +273,12 @@ const roomManager: IRoomManagerServer = {
},
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
socketManager
.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
.sendAdminMessage(
call.request.getRoomid(),
call.request.getRecipientuuid(),
call.request.getMessage(),
call.request.getType()
)
.catch((e) => console.error(e));
callback(null, new EmptyMessage());

View file

@ -1,3 +1,5 @@
/* eslint-disable */
import { Readable } from "stream";
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";

View file

@ -1,3 +1,5 @@
/* eslint-disable */
import { createWriteStream } from "fs";
import { join, dirname } from "path";
import Busboy from "busboy";

View file

@ -1,3 +1,5 @@
/* eslint-disable */
import { ReadStream } from "fs";
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View file

@ -1,7 +1,7 @@
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import Axios from "axios";
import { MapDetailsData } from "./AdminApi/MapDetailsData";
import { RoomRedirect } from "./AdminApi/RoomRedirect";
import { isMapDetailsData, MapDetailsData } from "./AdminApi/MapDetailsData";
import { isRoomRedirect, RoomRedirect } from "./AdminApi/RoomRedirect";
class AdminApi {
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
@ -17,6 +17,12 @@ class AdminApi {
headers: { Authorization: `${ADMIN_API_TOKEN}` },
params,
});
if (!isMapDetailsData(res.data) && !isRoomRedirect(res.data)) {
console.error("Unexpected answer from the /api/map admin endpoint.", res.data);
throw new Error("Unexpected answer from the /api/map admin endpoint.");
}
return res.data;
}
}

View file

@ -1,4 +1,4 @@
const EventEmitter = require("events");
import { EventEmitter } from "events";
const clientJoinEvent = "clientJoin";
const clientLeaveEvent = "clientLeave";

View file

@ -32,7 +32,7 @@ class MapFetcher {
//throw new Error("Invalid map format for map " + mapUrl);
console.error("Invalid map format for map " + mapUrl);
}
/* eslint-disable-next-line @typescript-eslint/no-unsafe-return */
return res.data;
}

View file

@ -10,7 +10,19 @@ import {
import { UserSocket } from "_Model/User";
import { RoomSocket, ZoneSocket } from "../RoomManager";
export function emitError(Client: UserSocket, message: string): void {
function getMessageFromError(error: unknown): string {
if (error instanceof Error) {
return error.message;
} else if (typeof error === "string") {
return error;
} else {
return "Unknown error";
}
}
export function emitError(Client: UserSocket, error: unknown): void {
const message = getMessageFromError(error);
const errorMessage = new ErrorMessage();
errorMessage.setMessage(message);
@ -23,8 +35,9 @@ export function emitError(Client: UserSocket, message: string): void {
console.warn(message);
}
export function emitErrorOnRoomSocket(Client: RoomSocket, message: string): void {
console.error(message);
export function emitErrorOnRoomSocket(Client: RoomSocket, error: unknown): void {
console.error(error);
const message = getMessageFromError(error);
const errorMessage = new ErrorMessage();
errorMessage.setMessage(message);
@ -41,8 +54,9 @@ export function emitErrorOnRoomSocket(Client: RoomSocket, message: string): void
console.warn(message);
}
export function emitErrorOnZoneSocket(Client: ZoneSocket, message: string): void {
console.error(message);
export function emitErrorOnZoneSocket(Client: ZoneSocket, error: unknown): void {
console.error(error);
const message = getMessageFromError(error);
const errorMessage = new ErrorMessage();
errorMessage.setMessage(message);

View file

@ -30,9 +30,14 @@ import {
BanUserMessage,
RefreshRoomMessage,
EmotePromptMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
VariableMessage,
BatchToPusherRoomMessage,
SubToPusherRoomMessage,
SetPlayerDetailsMessage,
PlayerDetailsUpdatedMessage,
} from "../Messages/generated/messages_pb";
import { User, UserSocket } from "../Model/User";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
@ -151,20 +156,9 @@ export class SocketManager {
//room.setViewport(client, client.viewport);
}
// Useless now, will be useful again if we allow editing details in game
/*handleSetPlayerDetails(client: UserSocket, playerDetailsMessage: SetPlayerDetailsMessage) {
const playerDetails = {
name: playerDetailsMessage.getName(),
characterLayers: playerDetailsMessage.getCharacterlayersList()
};
//console.log(SocketIoEvent.SET_PLAYER_DETAILS, playerDetails);
if (!isSetPlayerDetailsMessage(playerDetails)) {
emitError(client, 'Invalid SET_PLAYER_DETAILS message received: ');
return;
}
client.name = playerDetails.name;
client.characterLayers = SocketManager.mergeCharacterLayersAndCustomTextures(playerDetails.characterLayers, client.textures);
}*/
handleSetPlayerDetails(room: GameRoom, user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
room.updatePlayerDetails(user, playerDetailsMessage);
}
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
room.setSilent(user, silentMessage.getSilent());
@ -206,7 +200,7 @@ export class SocketManager {
webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password);
}
@ -236,7 +230,7 @@ export class SocketManager {
webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password);
}
@ -282,7 +276,9 @@ export class SocketManager {
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
this.onClientLeave(thing, newZone, listener),
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
this.onEmote(emoteEventMessage, listener)
this.onEmote(emoteEventMessage, listener),
(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket) =>
this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener)
)
.then((gameRoom) => {
gaugeManager.incNbRoomGauge();
@ -317,7 +313,7 @@ export class SocketManager {
if (thing instanceof User) {
const userJoinedZoneMessage = new UserJoinedZoneMessage();
if (!Number.isInteger(thing.id)) {
throw new Error("clientUser.userId is not an integer " + thing.id);
throw new Error(`clientUser.userId is not an integer ${thing.id}`);
}
userJoinedZoneMessage.setUserid(thing.id);
userJoinedZoneMessage.setUseruuid(thing.uuid);
@ -329,6 +325,12 @@ export class SocketManager {
userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl);
}
userJoinedZoneMessage.setCompanion(thing.companion);
if (thing.outlineColor === undefined) {
userJoinedZoneMessage.setHasoutline(false);
} else {
userJoinedZoneMessage.setHasoutline(true);
userJoinedZoneMessage.setOutlinecolor(thing.outlineColor);
}
const subMessage = new SubToPusherMessage();
subMessage.setUserjoinedzonemessage(userJoinedZoneMessage);
@ -378,6 +380,13 @@ export class SocketManager {
emitZoneMessage(subMessage, client);
}
private onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, client: ZoneSocket) {
const subMessage = new SubToPusherMessage();
subMessage.setPlayerdetailsupdatedmessage(playerDetailsUpdatedMessage);
emitZoneMessage(subMessage, client);
}
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void {
const position = group.getPosition();
const pointMessage = new PointMessage();
@ -440,7 +449,10 @@ export class SocketManager {
webrtcStartMessage1.setUserid(otherUser.id);
webrtcStartMessage1.setInitiator(true);
if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET);
const { username, password } = this.getTURNCredentials(
otherUser.id.toString(),
TURN_STATIC_AUTH_SECRET
);
webrtcStartMessage1.setWebrtcusername(username);
webrtcStartMessage1.setWebrtcpassword(password);
}
@ -454,7 +466,7 @@ export class SocketManager {
webrtcStartMessage2.setUserid(user.id);
webrtcStartMessage2.setInitiator(false);
if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET);
webrtcStartMessage2.setWebrtcusername(username);
webrtcStartMessage2.setWebrtcpassword(password);
}
@ -478,7 +490,7 @@ export class SocketManager {
hmac.setEncoding("base64");
hmac.write(username);
hmac.end();
const password = hmac.read();
const password = hmac.read() as string;
return {
username: username,
password: password,
@ -519,15 +531,6 @@ export class SocketManager {
}
}
emitPlayGlobalMessage(room: GameRoom, playGlobalMessage: PlayGlobalMessage) {
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setPlayglobalmessage(playGlobalMessage);
for (const [id, user] of room.getUsers().entries()) {
user.socket.write(serverToClientMessage);
}
}
public getWorlds(): Map<string, PromiseLike<GameRoom>> {
return this.roomsPromises;
}
@ -572,7 +575,7 @@ export class SocketManager {
user.socket.write(serverToClientMessage);
}
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
public handleSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
sendUserMessage.setType(sendUserMessageToSend.getType());
@ -691,7 +694,7 @@ export class SocketManager {
}
}
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string): Promise<void> {
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string, type: string): Promise<void> {
const room = await this.roomsPromises.get(roomId);
if (!room) {
console.error(
@ -715,7 +718,7 @@ export class SocketManager {
for (const recipient of recipients) {
const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(message);
sendUserMessage.setType("ban"); //todo: is the type correct?
sendUserMessage.setType(type);
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setSendusermessage(sendUserMessage);
@ -833,6 +836,39 @@ export class SocketManager {
emoteEventMessage.setActoruserid(user.id);
room.emitEmoteEvent(user, emoteEventMessage);
}
handleFollowRequestMessage(room: GameRoom, user: User, message: FollowRequestMessage) {
const clientMessage = new ServerToClientMessage();
clientMessage.setFollowrequestmessage(message);
room.sendToOthersInGroupIncludingUser(user, clientMessage);
}
handleFollowConfirmationMessage(room: GameRoom, user: User, message: FollowConfirmationMessage) {
const leader = room.getUserById(message.getLeader());
if (!leader) {
const message = `Could not follow user "{message.getLeader()}" in room "{room.roomUrl}".`;
console.info(message, "Maybe the user just left.");
return;
}
// By security, we look at the group leader. If the group leader is NOT the leader in the message,
// everybody should stop following the group leader (to avoid having 2 group leaders)
if (user?.group?.leader && user?.group?.leader !== leader) {
user?.group?.leader?.stopLeading();
}
leader.addFollower(user);
}
handleFollowAbortMessage(room: GameRoom, user: User, message: FollowAbortMessage) {
if (user.id === message.getLeader()) {
user?.group?.leader?.stopLeading();
} else {
// Forward message
const leader = room.getUserById(message.getLeader());
leader?.delFollower(user);
}
}
}
export const socketManager = new SocketManager();

View file

@ -101,11 +101,11 @@ export class VariablesManager {
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
objects.set(object.name as string, this.iTiledObjectToVariable(object));
}
}
} else if (layer.type === "group") {
for (const innerLayer of layer.layers) {
for (const innerLayer of layer.layers as ITiledMapLayer[]) {
this.recursiveFindVariablesInLayer(innerLayer, objects);
}
}
@ -116,7 +116,7 @@ export class VariablesManager {
if (object.properties) {
for (const property of object.properties) {
const value = property.value;
const value = property.value as unknown;
switch (property.name) {
case "default":
variable.defaultValue = JSON.stringify(value);

View file

@ -51,7 +51,8 @@ describe("GameRoom", () => {
() => {},
() => {},
() => {},
emote
emote,
() => {}
);
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
@ -86,7 +87,8 @@ describe("GameRoom", () => {
() => {},
() => {},
() => {},
emote
emote,
() => {}
);
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
@ -125,7 +127,8 @@ describe("GameRoom", () => {
() => {},
() => {},
() => {},
emote
emote,
() => {}
);
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));

View file

@ -1,11 +1,10 @@
import "jasmine";
import {PositionNotifier} from "../src/Model/PositionNotifier";
import {User, UserSocket} from "../src/Model/User";
import {Zone} from "_Model/Zone";
import {Movable} from "_Model/Movable";
import {PositionInterface} from "_Model/PositionInterface";
import {ZoneSocket} from "../src/RoomManager";
import { PositionNotifier } from "../src/Model/PositionNotifier";
import { User, UserSocket } from "../src/Model/User";
import { Zone } from "_Model/Zone";
import { Movable } from "_Model/Movable";
import { PositionInterface } from "_Model/PositionInterface";
import { ZoneSocket } from "../src/RoomManager";
describe("PositionNotifier", () => {
it("should receive notifications when player moves", () => {
@ -13,27 +12,59 @@ describe("PositionNotifier", () => {
let moveTriggered = false;
let leaveTriggered = false;
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => {
enterTriggered = true;
}, (thing: Movable, position: PositionInterface) => {
moveTriggered = true;
}, (thing: Movable) => {
leaveTriggered = true;
}, () => {});
const positionNotifier = new PositionNotifier(
300,
300,
(thing: Movable) => {
enterTriggered = true;
},
(thing: Movable, position: PositionInterface) => {
moveTriggered = true;
},
(thing: Movable) => {
leaveTriggered = true;
},
() => {},
() => {}
);
const user1 = new User(1, 'test', '10.0.0.2', {
x: 500,
y: 500,
moving: false,
direction: 'down'
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
const user1 = new User(
1,
"test",
"10.0.0.2",
{
x: 500,
y: 500,
moving: false,
direction: "down",
},
false,
positionNotifier,
{} as UserSocket,
[],
null,
"foo",
[]
);
const user2 = new User(2, 'test', '10.0.0.2', {
x: -9999,
y: -9999,
moving: false,
direction: 'down'
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
const user2 = new User(
2,
"test",
"10.0.0.2",
{
x: -9999,
y: -9999,
moving: false,
direction: "down",
},
false,
positionNotifier,
{} as UserSocket,
[],
null,
"foo",
[]
);
positionNotifier.addZoneListener({} as ZoneSocket, 0, 0);
positionNotifier.addZoneListener({} as ZoneSocket, 0, 1);
@ -46,21 +77,21 @@ describe("PositionNotifier", () => {
bottom: 500
});*/
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
expect(enterTriggered).toBe(true);
expect(moveTriggered).toBe(false);
enterTriggered = false;
// Move inside the zone
user2.setPosition({x:501, y:500, direction: 'down', moving: false});
user2.setPosition({ x: 501, y: 500, direction: "down", moving: false });
expect(enterTriggered).toBe(false);
expect(moveTriggered).toBe(true);
moveTriggered = false;
// Move out of the zone in a zone that we don't track
user2.setPosition({x: 901, y: 500, direction: 'down', moving: false});
user2.setPosition({ x: 901, y: 500, direction: "down", moving: false });
expect(enterTriggered).toBe(false);
expect(moveTriggered).toBe(false);
@ -68,7 +99,7 @@ describe("PositionNotifier", () => {
leaveTriggered = false;
// Move back in
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
expect(enterTriggered).toBe(true);
expect(moveTriggered).toBe(false);
expect(leaveTriggered).toBe(false);
@ -88,27 +119,59 @@ describe("PositionNotifier", () => {
let moveTriggered = false;
let leaveTriggered = false;
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable, fromZone: Zone|null ) => {
enterTriggered = true;
}, (thing: Movable, position: PositionInterface) => {
moveTriggered = true;
}, (thing: Movable) => {
leaveTriggered = true;
}, () => {});
const positionNotifier = new PositionNotifier(
300,
300,
(thing: Movable, fromZone: Zone | null) => {
enterTriggered = true;
},
(thing: Movable, position: PositionInterface) => {
moveTriggered = true;
},
(thing: Movable) => {
leaveTriggered = true;
},
() => {},
() => {}
);
const user1 = new User(1, 'test', '10.0.0.2', {
x: 500,
y: 500,
moving: false,
direction: 'down'
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
const user1 = new User(
1,
"test",
"10.0.0.2",
{
x: 500,
y: 500,
moving: false,
direction: "down",
},
false,
positionNotifier,
{} as UserSocket,
[],
null,
"foo",
[]
);
const user2 = new User(2, 'test', '10.0.0.2', {
x: 0,
y: 0,
moving: false,
direction: 'down'
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
const user2 = new User(
2,
"test",
"10.0.0.2",
{
x: 0,
y: 0,
moving: false,
direction: "down",
},
false,
positionNotifier,
{} as UserSocket,
[],
null,
"foo",
[]
);
const listener = {} as ZoneSocket;
positionNotifier.addZoneListener(listener, 0, 0);
@ -124,14 +187,12 @@ describe("PositionNotifier", () => {
positionNotifier.enter(user1);
positionNotifier.enter(user2);
//expect(newUsers.length).toBe(2);
expect(enterTriggered).toBe(true);
enterTriggered = false;
//positionNotifier.updatePosition(user2, {x:500, y:500}, {x:0, y: 0})
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
expect(enterTriggered).toBe(true);
expect(moveTriggered).toBe(false);
@ -182,4 +243,4 @@ describe("PositionNotifier", () => {
enterTriggered = false;
//expect(newUsers.length).toBe(2);
});
})
});

File diff suppressed because it is too large Load diff