Merge branch 'develop' into 2daysLimit

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>
This commit is contained in:
Gregoire Parant 2022-01-05 11:47:31 +01:00
commit 8b758a0053
57 changed files with 2041 additions and 2119 deletions

View File

@ -11,10 +11,42 @@ on:
jobs:
start-runner:
name: Start self-hosted EC2 runner
runs-on: ubuntu-latest
outputs:
label: ${{ steps.start-ec2-runner.outputs.label }}
ec2-instance-id: ${{ steps.start-ec2-runner.outputs.ec2-instance-id }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Start EC2 runner
id: start-ec2-runner
uses: machulav/ec2-github-runner@v2
with:
mode: start
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
ec2-image-id: ami-094dbcc53250a2480
ec2-instance-type: t3.xlarge
subnet-id: subnet-0ac40025f559df1bc
security-group-id: sg-0e36e96e3b8ed2d64
#iam-role-name: my-role-name # optional, requires additional permissions
#aws-resource-tags: > # optional, requires additional permissions
# [
# {"Key": "Name", "Value": "ec2-github-runner"},
# {"Key": "GitHubRepository", "Value": "${{ github.repository }}"}
# ]
end-to-end-tests:
name: "End-to-end testcafe tests"
runs-on: "ubuntu-latest"
needs: start-runner # required to start the main job when the runner is ready
runs-on: ${{ needs.start-runner.outputs.label }} # run the job on the newly created runner
steps:
- name: "Checkout"
@ -67,3 +99,25 @@ jobs:
- name: Display logs
if: ${{ failure() }}
run: docker-compose logs
stop-runner:
name: Stop self-hosted EC2 runner
needs:
- start-runner # required to get output from the start-runner job
- end-to-end-tests # required to wait when the main job is done
runs-on: ubuntu-latest
if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Stop EC2 runner
uses: machulav/ec2-github-runner@v2
with:
mode: stop
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
label: ${{ needs.start-runner.outputs.label }}
ec2-instance-id: ${{ needs.start-runner.outputs.ec2-instance-id }}

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

@ -21,6 +21,7 @@ import {
SubToPusherRoomMessage,
VariableMessage,
VariableWithTagMessage,
ServerToClientMessage,
} from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { RoomSocket, ZoneSocket } from "src/RoomManager";
@ -34,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;
@ -109,10 +111,6 @@ export class GameRoom {
return gameRoom;
}
public getGroups(): Group[] {
return Array.from(this.groups.values());
}
public getUsers(): Map<number, User> {
return this.users;
}
@ -175,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);
@ -213,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?
@ -245,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;
@ -279,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?
@ -485,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

@ -7,6 +7,8 @@ import { ServerDuplexStream } from "grpc";
import {
BatchMessage,
CompanionMessage,
FollowAbortMessage,
FollowConfirmationMessage,
PusherToBackMessage,
ServerToClientMessage,
SetPlayerDetailsMessage,
@ -19,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,
@ -50,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;

View File

@ -39,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})`
);
}
}

View File

@ -9,6 +9,9 @@ import {
BatchToPusherMessage,
BatchToPusherRoomMessage,
EmotePromptMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
EmptyMessage,
ItemEventMessage,
JoinRoomMessage,
@ -119,6 +122,24 @@ 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();
socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage);
@ -166,7 +187,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", () => {
@ -196,7 +217,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", () => {

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,6 +30,9 @@ import {
BanUserMessage,
RefreshRoomMessage,
EmotePromptMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
VariableMessage,
BatchToPusherRoomMessage,
SubToPusherRoomMessage,
@ -197,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);
}
@ -227,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);
}
@ -310,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);
@ -446,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);
}
@ -460,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);
}
@ -484,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,
@ -839,6 +845,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);

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,22 @@
import { AdminMessageEventTypes, adminMessagesService } from "../Connexion/AdminMessagesService";
import { textMessageContentStore, textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore";
import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import { UPLOADER_URL } from "../Enum/EnvironmentVariable";
import { banMessageContentStore, banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore";
import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
class UserMessageManager {
receiveBannedMessageListener!: Function;
constructor() {
adminMessagesService.messageStream.subscribe((event) => {
textMessageVisibleStore.set(false);
banMessageVisibleStore.set(false);
if (event.type === AdminMessageEventTypes.admin) {
textMessageContentStore.set(event.text);
textMessageVisibleStore.set(true);
textMessageStore.addMessage(event.text);
} 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);
banMessageStore.addMessage(event.text);
} else if (event.type === AdminMessageEventTypes.banned) {
banMessageContentStore.set(event.text);
banMessageVisibleStore.set(true);
banMessageStore.addMessage(event.text);
this.receiveBannedMessageListener();
}
});

View File

@ -33,10 +33,10 @@
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
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 BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte";
import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte";
import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import { warningContainerStore } from "../Stores/MenuStore";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import { layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
@ -45,6 +45,9 @@
import AudioManager from "./AudioManager/AudioManager.svelte";
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
import { followStateStore } from "../Stores/FollowStore";
import { peerStore } from "../Stores/PeerStore";
import FollowMenu from "./FollowMenu/FollowMenu.svelte";
export let game: Game;
</script>
@ -75,14 +78,13 @@
<EnableCameraScene {game} />
</div>
{/if}
{#if $banMessageVisibleStore}
{#if $banMessageStore.length > 0}
<div>
<AdminMessage />
<BanMessageContainer />
</div>
{/if}
{#if $textMessageVisibleStore}
{:else if $textMessageStore.length > 0}
<div>
<TextMessage />
<TextMessageContainer />
</div>
{/if}
{#if $soundPlayingStore}
@ -105,6 +107,11 @@
<ReportMenu />
</div>
{/if}
{#if $followStateStore !== "off" || $peerStore.size > 0}
<div>
<FollowMenu />
</div>
{/if}
{#if $menuIconVisiblilityStore}
<div>
<MenuIcon />

View File

@ -0,0 +1,208 @@
<!--
vim: ft=typescript
-->
<script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager";
import followImg from "../images/follow.svg";
import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore";
const gameScene = gameManager.getCurrentGameScene();
function name(userId: number): string | undefined {
return gameScene.MapPlayersByKey.get(userId)?.PlayerValue;
}
function sendFollowRequest() {
gameScene.connection?.emitFollowRequest();
followRoleStore.set("leader");
followStateStore.set("active");
}
function acceptFollowRequest() {
gameScene.CurrentPlayer.enableFollowing();
gameScene.connection?.emitFollowConfirmation();
}
function abortEnding() {
followStateStore.set("active");
}
function reset() {
gameScene.connection?.emitFollowAbort();
followUsersStore.stopFollowing();
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
reset();
}
}
</script>
<svelte:window on:keydown={onKeyDown} />
{#if $followStateStore === "requesting"}
<div class="interact-menu nes-container is-rounded">
{#if $followRoleStore === "follower"}
<section class="interact-menu-title">
<h2>Do you want to follow {name($followUsersStore[0])}?</h2>
</section>
<section class="interact-menu-action">
<button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest}
>Yes</button
>
<button type="button" class="nes-btn is-error" on:click|preventDefault={reset}>No</button>
</section>
{:else if $followRoleStore === "leader"}
<section class="interact-menu-question">
<p>Should never be displayed</p>
</section>
{/if}
</div>
{/if}
{#if $followStateStore === "ending"}
<div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title">
<h2>Interaction</h2>
</section>
{#if $followRoleStore === "follower"}
<section class="interact-menu-question">
<p>Do you want to stop following {name($followUsersStore[0])}?</p>
</section>
{:else if $followRoleStore === "leader"}
<section class="interact-menu-question">
<p>Do you want to stop leading the way?</p>
</section>
{/if}
<section class="interact-menu-action">
<button type="button" class="nes-btn is-success" on:click|preventDefault={reset}>Yes</button>
<button type="button" class="nes-btn is-error" on:click|preventDefault={abortEnding}>No</button>
</section>
</div>
{/if}
{#if $followStateStore === "active" || $followStateStore === "ending"}
<div class="interact-status nes-container is-rounded">
<section class="interact-status">
{#if $followRoleStore === "follower"}
<p>Following {name($followUsersStore[0])}</p>
{:else if $followUsersStore.length === 0}
<p>Waiting for followers' confirmation</p>
{:else if $followUsersStore.length === 1}
<p>{name($followUsersStore[0])} is following you</p>
{:else if $followUsersStore.length === 2}
<p>{name($followUsersStore[0])} and {name($followUsersStore[1])} are following you</p>
{:else}
<p>
{$followUsersStore.slice(0, -1).map(name).join(", ")} and {name(
$followUsersStore[$followUsersStore.length - 1]
)} are following you
</p>
{/if}
</section>
</div>
{/if}
{#if $followStateStore === "off"}
<button
type="button"
class="nes-btn is-primary follow-menu-button"
on:click|preventDefault={sendFollowRequest}
title="Ask others to follow"><img class="background-img" src={followImg} alt="" /></button
>
{/if}
{#if $followStateStore === "active" || $followStateStore === "ending"}
{#if $followRoleStore === "follower"}
<button
type="button"
class="nes-btn is-error follow-menu-button"
on:click|preventDefault={reset}
title="Stop following"><img class="background-img" src={followImg} alt="" /></button
>
{:else}
<button
type="button"
class="nes-btn is-error follow-menu-button"
on:click|preventDefault={reset}
title="Stop leading the way"><img class="background-img" src={followImg} alt="" /></button
>
{/if}
{/if}
<style lang="scss">
.nes-container {
padding: 5px;
}
div.interact-status {
background-color: #333333;
color: whitesmoke;
position: relative;
height: 2.7em;
width: 40vw;
top: 87vh;
margin: auto;
text-align: center;
}
div.interact-menu {
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
position: relative;
width: 60vw;
top: 60vh;
margin: auto;
section.interact-menu-title {
margin-bottom: 20px;
display: flex;
justify-content: center;
}
section.interact-menu-question {
margin: 4px;
margin-bottom: 20px;
p {
font-size: 1.05em;
font-weight: bold;
}
}
section.interact-menu-action {
display: grid;
grid-gap: 10%;
grid-template-columns: 45% 45%;
margin-bottom: 20px;
margin-left: 5%;
margin-right: 5%;
}
}
.follow-menu-button {
position: absolute;
bottom: 10px;
left: 10px;
pointer-events: all;
}
@media only screen and (max-width: 800px) {
div.interact-status {
width: 100vw;
top: 78vh;
font-size: 0.75em;
}
div.interact-menu {
height: 21vh;
width: 100vw;
font-size: 0.75em;
}
}
</style>

View File

@ -8,6 +8,7 @@
let fullscreen: boolean = localUserStore.getFullscreen();
let notification: boolean = localUserStore.getNotification() === "granted";
let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger();
let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests();
let valueGame: number = localUserStore.getGameQualityValue();
let valueVideo: number = localUserStore.getVideoQualityValue();
let previewValueGame = valueGame;
@ -59,6 +60,10 @@
localUserStore.setForceCowebsiteTrigger(forceCowebsiteTrigger);
}
function changeIgnoreFollowRequests() {
localUserStore.setIgnoreFollowRequests(ignoreFollowRequests);
}
function closeMenu() {
menuVisiblilityStore.set(false);
}
@ -123,6 +128,15 @@
/>
<span>Always ask before opening websites and Jitsi Meet rooms</span>
</label>
<label>
<input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={ignoreFollowRequests}
on:change={changeIgnoreFollowRequests}
/>
<span>Ignore requests to follow other users</span>
</label>
</section>
</div>

View File

@ -1,12 +1,11 @@
<script lang="ts">
import { fly } from "svelte/transition";
import { banMessageVisibleStore, banMessageContentStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import { fly, fade } from "svelte/transition";
import { onMount } from "svelte";
import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore";
export let message: Message;
let text: string;
$: {
text = $banMessageContentStore;
}
const NAME_BUTTON = "Ok";
let nbSeconds = 10;
let nameButton = "";
@ -28,17 +27,21 @@
}
function closeBanMessage() {
banMessageVisibleStore.set(false);
banMessageStore.clearMessageById(message.id);
}
</script>
<div class="main-ban-message nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
<div
class="main-ban-message nes-container is-rounded"
in:fly={{ y: -1000, duration: 500, delay: 250 }}
out:fade={{ duration: 200 }}
>
<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>
<p>{message.text}</p>
</div>
<div class="footer-ban-message">
<button

View File

@ -0,0 +1,13 @@
<script lang="ts">
import { flip } from "svelte/animate";
import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import BanMessage from "./BanMessage.svelte";
</script>
<div class="main-ban-message-container">
{#each $banMessageStore.slice(0, 1) as message (message.id)}
<div animate:flip={{ duration: 250 }}>
<BanMessage {message} />
</div>
{/each}
</div>

View File

@ -1,17 +1,17 @@
<script lang="ts">
import { fly } from "svelte/transition";
import { textMessageContentStore, textMessageVisibleStore } from "../../Stores/TypeMessageStore/TextMessageStore";
import { fly, fade } from "svelte/transition";
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
import { textMessageStore } from "../../Stores/TypeMessageStore/TextMessageStore";
let converter: QuillDeltaToHtmlConverter;
$: {
const content = JSON.parse($textMessageContentStore);
converter = new QuillDeltaToHtmlConverter(content.ops, { inlineStyles: true });
}
export let message: Message;
const content = JSON.parse(message.text);
const converter = new QuillDeltaToHtmlConverter(content.ops, { inlineStyles: true });
const NAME_BUTTON = "Ok";
function closeTextMessage() {
textMessageVisibleStore.set(false);
textMessageStore.clearMessageById(message.id);
}
function onKeyDown(e: KeyboardEvent) {
@ -23,7 +23,11 @@
<svelte:window on:keydown={onKeyDown} />
<div class="main-text-message nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
<div
class="main-text-message nes-container is-rounded"
in:fly={{ x: -1000, duration: 500, delay: 250 }}
out:fade={{ duration: 250 }}
>
<div class="content-text-message">
{@html converter.convert()}
</div>
@ -43,6 +47,8 @@
width: 80vw;
margin-right: auto;
margin-left: auto;
margin-bottom: 16px;
margin-top: 0;
padding-bottom: 0;
pointer-events: auto;

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { flip } from "svelte/animate";
import TextMessage from "./TextMessage.svelte";
import { textMessageStore } from "../../Stores/TypeMessageStore/TextMessageStore";
const MAX_MESSAGES = 3;
</script>
<div class="main-text-message-container">
{#each $textMessageStore.slice(0, MAX_MESSAGES) as message (message.id)}
<div animate:flip={{ duration: 250 }}>
<TextMessage {message} />
</div>
{/each}
</div>
<style lang="scss">
div.main-text-message-container {
padding-top: 16px;
}
</style>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><rect fill="none" height="24" width="24"/><path d="M9.5,5.5c1.1,0,2-0.9,2-2s-0.9-2-2-2s-2,0.9-2,2S8.4,5.5,9.5,5.5z M5.75,8.9L3,23h2.1l1.75-8L9,17v6h2v-7.55L8.95,13.4 l0.6-3C10.85,12,12.8,13,15,13v-2c-1.85,0-3.45-1-4.35-2.45L9.7,6.95C9.35,6.35,8.7,6,8,6C7.75,6,7.5,6.05,7.25,6.15L2,8.3V13h2 V9.65L5.75,8.9 M13,2v7h3.75v14h1.5V9H22V2H13z M18.01,8V6.25H14.5v-1.5h3.51V3l2.49,2.5L18.01,8z"/></svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -1,5 +1,5 @@
import Axios from "axios";
import { PUSHER_URL, START_ROOM_URL } from "../Enum/EnvironmentVariable";
import { PUSHER_URL } from "../Enum/EnvironmentVariable";
import { RoomConnection } from "./RoomConnection";
import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels";
import { GameConnexionTypes, urlManager } from "../Url/UrlManager";

View File

@ -14,6 +14,7 @@ const audioPlayerMuteKey = "audioMute";
const helpCameraSettingsShown = "helpCameraSettingsShown";
const fullscreenKey = "fullscreen";
const forceCowebsiteTriggerKey = "forceCowebsiteTrigger";
const ignoreFollowRequests = "ignoreFollowRequests";
const lastRoomUrl = "lastRoomUrl";
const authToken = "authToken";
const state = "state";
@ -128,6 +129,13 @@ class LocalUserStore {
return localStorage.getItem(forceCowebsiteTriggerKey) === "true";
}
setIgnoreFollowRequests(value: boolean): void {
localStorage.setItem(ignoreFollowRequests, value.toString());
}
getIgnoreFollowRequests(): boolean {
return localStorage.getItem(ignoreFollowRequests) === "true";
}
setLastRoomUrl(roomUrl: string): void {
localStorage.setItem(lastRoomUrl, roomUrl.toString());
if ("caches" in window) {

View File

@ -104,9 +104,13 @@ export class Room {
const data = result.data;
if (isRoomRedirect(data.redirectUrl)) {
if (data.authenticationMandatory !== undefined) {
data.authenticationMandatory = Boolean(data.authenticationMandatory);
}
if (isRoomRedirect(data)) {
return {
redirectUrl: data.redirectUrl as string,
redirectUrl: data.redirectUrl,
};
} else if (isMapDetailsData(data)) {
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);

View File

@ -30,6 +30,9 @@ import {
PingMessage,
EmoteEventMessage,
EmotePromptMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
SendUserMessage,
BanUserMessage,
VariableMessage,
@ -59,7 +62,10 @@ import { adminMessagesService } from "./AdminMessagesService";
import { worldFullMessageStream } from "./WorldFullMessageStream";
import { connectionManager } from "./ConnectionManager";
import { emoteEventStream } from "./EmoteEventStream";
import { get } from "svelte/store";
import { warningContainerStore } from "../Stores/MenuStore";
import { followStateStore, followRoleStore, followUsersStore } from "../Stores/FollowStore";
import { localUserStore } from "./LocalUserStore";
const manualPingDelay = 20000;
@ -262,6 +268,21 @@ export class RoomConnection implements RoomConnection {
warningContainerStore.activateWarningContainer();
} else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed.
} else if (message.hasFollowrequestmessage()) {
const requestMessage = message.getFollowrequestmessage();
if (!localUserStore.getIgnoreFollowRequests()) {
followUsersStore.addFollowRequest(requestMessage.getLeader());
}
} else if (message.hasFollowconfirmationmessage()) {
const responseMessage = message.getFollowconfirmationmessage();
followUsersStore.addFollower(responseMessage.getFollower());
} else if (message.hasFollowabortmessage()) {
const abortMessage = message.getFollowabortmessage();
if (get(followRoleStore) === "follower") {
followUsersStore.stopFollowing();
} else {
followUsersStore.removeFollower(abortMessage.getFollower());
}
} else if (message.hasErrormessage()) {
const errorMessage = message.getErrormessage() as ErrorMessage;
console.error("An error occurred server side: " + errorMessage.getMessage());
@ -746,6 +767,43 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
public emitFollowRequest(): void {
if (!this.userId) {
return;
}
const message = new FollowRequestMessage();
message.setLeader(this.userId);
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setFollowrequestmessage(message);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
public emitFollowConfirmation(): void {
if (!this.userId) {
return;
}
const message = new FollowConfirmationMessage();
message.setLeader(get(followUsersStore)[0]);
message.setFollower(this.userId);
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setFollowconfirmationmessage(message);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
public emitFollowAbort(): void {
const isLeader = get(followRoleStore) === "leader";
const hasFollowers = get(followUsersStore).length > 0;
if (!this.userId || (isLeader && !hasFollowers)) {
return;
}
const message = new FollowAbortMessage();
message.setLeader(isLeader ? this.userId : get(followUsersStore)[0]);
message.setFollower(isLeader ? 0 : this.userId);
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setFollowabortmessage(message);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
public getAllTags(): string[] {
return this.tags;
}

View File

@ -33,7 +33,7 @@ export abstract class Character extends Container {
private readonly playerName: Text;
public PlayerValue: string;
public sprites: Map<string, Sprite>;
private lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
protected lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
//private teleportation: Sprite;
private invisible: boolean;
public companion?: Companion;
@ -277,24 +277,20 @@ export abstract class Character extends Container {
body.setVelocity(x, y);
// up or down animations are prioritized over left and right
if (body.velocity.y < 0) {
//moving up
this.lastDirection = PlayerAnimationDirections.Up;
this.playAnimation(PlayerAnimationDirections.Up, true);
} else if (body.velocity.y > 0) {
//moving down
this.lastDirection = PlayerAnimationDirections.Down;
this.playAnimation(PlayerAnimationDirections.Down, true);
} else if (body.velocity.x > 0) {
//moving right
this.lastDirection = PlayerAnimationDirections.Right;
this.playAnimation(PlayerAnimationDirections.Right, true);
} else if (body.velocity.x < 0) {
//moving left
this.lastDirection = PlayerAnimationDirections.Left;
this.playAnimation(PlayerAnimationDirections.Left, true);
if (Math.abs(body.velocity.x) > Math.abs(body.velocity.y)) {
if (body.velocity.x < 0) {
this.lastDirection = PlayerAnimationDirections.Left;
} else if (body.velocity.x > 0) {
this.lastDirection = PlayerAnimationDirections.Right;
}
} else {
if (body.velocity.y < 0) {
this.lastDirection = PlayerAnimationDirections.Up;
} else if (body.velocity.y > 0) {
this.lastDirection = PlayerAnimationDirections.Down;
}
}
this.playAnimation(this.lastDirection, true);
this.setDepth(this.y);

View File

@ -81,7 +81,14 @@ export class GameMap {
let depth = -2;
for (const layer of this.flatLayers) {
if (layer.type === "tilelayer") {
this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth));
this.phaserLayers.push(
phaserMap
.createLayer(layer.name, terrains, (layer.x || 0) * 32, (layer.y || 0) * 32)
.setDepth(depth)
.setAlpha(layer.opacity)
.setVisible(layer.visible)
.setSize(layer.width, layer.height)
);
}
if (layer.type === "objectgroup" && layer.name === "floorLayer") {
depth = DEPTH_OVERLAY_INDEX;

View File

@ -1,7 +1,7 @@
import type { Subscription } from "rxjs";
import AnimatedTiles from "phaser-animated-tiles";
import { Queue } from "queue-typescript";
import { get } from "svelte/store";
import { get, Unsubscriber } from "svelte/store";
import { userMessageManager } from "../../Administration/UserMessageManager";
import { connectionManager } from "../../Connexion/ConnectionManager";
@ -91,6 +91,8 @@ import { deepCopy } from "deep-copy-ts";
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import { MapStore } from "../../Stores/Utils/MapStore";
import { SetPlayerDetailsMessage } from "../../Messages/generated/messages_pb";
import { followUsersColorStore, followUsersStore } from "../../Stores/FollowStore";
import { getColorRgbFromHue } from "../../WebRtc/ColorGenerator";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
@ -165,9 +167,11 @@ export class GameScene extends DirtyScene {
private createPromise: Promise<void>;
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
private iframeSubscriptionList!: Array<Subscription>;
private peerStoreUnsubscribe!: () => void;
private emoteUnsubscribe!: () => void;
private emoteMenuUnsubscribe!: () => void;
private peerStoreUnsubscribe!: Unsubscriber;
private emoteUnsubscribe!: Unsubscriber;
private emoteMenuUnsubscribe!: Unsubscriber;
private followUsersColorStoreUnsubscribe!: Unsubscriber;
private biggestAvailableAreaStoreUnsubscribe!: () => void;
MapUrlFile: string;
roomUrl: string;
@ -646,6 +650,16 @@ export class GameScene extends DirtyScene {
}
});
this.followUsersColorStoreUnsubscribe = followUsersColorStore.subscribe((color) => {
if (color !== undefined) {
this.CurrentPlayer.setOutlineColor(color);
this.connection?.emitPlayerOutlineColor(color);
} else {
this.CurrentPlayer.removeOutlineColor();
this.connection?.emitPlayerOutlineColor(null);
}
});
Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises]).then(() => {
this.scene.wake();
});
@ -1443,6 +1457,7 @@ ${escapedMessage}
this.peerStoreUnsubscribe();
this.emoteUnsubscribe();
this.emoteMenuUnsubscribe();
this.followUsersColorStoreUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("loadTileset");

View File

@ -41,7 +41,7 @@ export class PlayerMovement {
oldX: this.startPosition.x,
oldY: this.startPosition.y,
direction: this.endPosition.direction,
moving: true,
moving: this.endPosition.moving,
};
}
}

View File

@ -1,16 +1,17 @@
import { PlayerAnimationDirections } from "./Animation";
import type { GameScene } from "../Game/GameScene";
import { UserInputEvent, UserInputManager } from "../UserInput/UserInputManager";
import { ActiveEventList, UserInputEvent, UserInputManager } from "../UserInput/UserInputManager";
import { Character } from "../Entity/Character";
import type { RemotePlayer } from "../Entity/RemotePlayer";
import { get } from "svelte/store";
import { userMovingStore } from "../../Stores/GameStore";
import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore";
export const hasMovedEventName = "hasMoved";
export const requestEmoteEventName = "requestEmote";
export class Player extends Character {
private previousDirection: string = PlayerAnimationDirections.Down;
private wasMoving: boolean = false;
constructor(
Scene: GameScene,
x: number,
@ -29,71 +30,99 @@ export class Player extends Character {
this.getBody().setImmovable(false);
}
moveUser(delta: number): void {
//if user client on shift, camera and player speed
let direction = null;
let moving = false;
const activeEvents = this.userInputManager.getEventListForGameTick();
const speedMultiplier = activeEvents.get(UserInputEvent.SpeedUp) ? 25 : 9;
const moveAmount = speedMultiplier * 20;
let x = 0;
let y = 0;
private inputStep(activeEvents: ActiveEventList, x: number, y: number) {
// Process input events
if (activeEvents.get(UserInputEvent.MoveUp)) {
y = -moveAmount;
direction = PlayerAnimationDirections.Up;
moving = true;
y = y - 1;
} else if (activeEvents.get(UserInputEvent.MoveDown)) {
y = moveAmount;
direction = PlayerAnimationDirections.Down;
moving = true;
y = y + 1;
}
if (activeEvents.get(UserInputEvent.MoveLeft)) {
x = -moveAmount;
direction = PlayerAnimationDirections.Left;
moving = true;
x = x - 1;
} else if (activeEvents.get(UserInputEvent.MoveRight)) {
x = moveAmount;
direction = PlayerAnimationDirections.Right;
moving = true;
x = x + 1;
}
moving = moving || activeEvents.get(UserInputEvent.JoystickMove);
if (x !== 0 || y !== 0) {
// Compute movement deltas
const followMode = get(followStateStore) !== "off";
const speedup = activeEvents.get(UserInputEvent.SpeedUp) && !followMode ? 25 : 9;
const moveAmount = speedup * 20;
x = x * moveAmount;
y = y * moveAmount;
// Compute moving state
const joystickMovement = activeEvents.get(UserInputEvent.JoystickMove);
const moving = x !== 0 || y !== 0 || joystickMovement;
// Compute direction
let direction = this.lastDirection;
if (moving && !joystickMovement) {
if (Math.abs(x) > Math.abs(y)) {
direction = x < 0 ? PlayerAnimationDirections.Left : PlayerAnimationDirections.Right;
} else {
direction = y < 0 ? PlayerAnimationDirections.Up : PlayerAnimationDirections.Down;
}
}
// Send movement events
const emit = () => this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y });
if (moving) {
this.move(x, y);
this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y, oldX: x, oldY: y });
} else if (this.wasMoving && moving) {
// slow joystick movement
this.move(0, 0);
this.emit(hasMovedEventName, {
moving,
direction: this.previousDirection,
x: this.x,
y: this.y,
oldX: x,
oldY: y,
});
} else if (this.wasMoving && !moving) {
emit();
} else if (get(userMovingStore)) {
this.stop();
this.emit(hasMovedEventName, {
moving,
direction: this.previousDirection,
x: this.x,
y: this.y,
oldX: x,
oldY: y,
});
emit();
}
if (direction !== null) {
this.previousDirection = direction;
}
this.wasMoving = moving;
// Update state
userMovingStore.set(moving);
}
public isMoving(): boolean {
return this.wasMoving;
private computeFollowMovement(): number[] {
// Find followed WOKA and abort following if we lost it
const player = this.scene.MapPlayersByKey.get(get(followUsersStore)[0]);
if (!player) {
this.scene.connection?.emitFollowAbort();
followStateStore.set("off");
return [0, 0];
}
// Compute movement direction
const xDistance = player.x - this.x;
const yDistance = player.y - this.y;
const distance = Math.pow(xDistance, 2) + Math.pow(yDistance, 2);
if (distance < 2000) {
return [0, 0];
}
const xMovement = xDistance / Math.sqrt(distance);
const yMovement = yDistance / Math.sqrt(distance);
return [xMovement, yMovement];
}
public enableFollowing() {
followStateStore.set("active");
}
public moveUser(delta: number): void {
const activeEvents = this.userInputManager.getEventListForGameTick();
const state = get(followStateStore);
const role = get(followRoleStore);
if (activeEvents.get(UserInputEvent.Follow)) {
if (state === "off" && this.scene.groups.size > 0) {
followStateStore.set("requesting");
followRoleStore.set("leader");
} else if (state === "active") {
followStateStore.set("ending");
}
}
let x = 0;
let y = 0;
if ((state === "active" || state === "ending") && role === "follower") {
[x, y] = this.computeFollowMovement();
}
this.inputStep(activeEvents, x, y);
}
}

View File

@ -16,6 +16,7 @@ export enum UserInputEvent {
MoveDown,
SpeedUp,
Interact,
Follow,
Shout,
JoystickMove,
}
@ -147,6 +148,10 @@ export class UserInputManager {
event: UserInputEvent.Interact,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false),
},
{
event: UserInputEvent.Follow,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),
},
{
event: UserInputEvent.Shout,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),

View File

@ -0,0 +1,90 @@
import { derived, writable } from "svelte/store";
import { getColorRgbFromHue } from "../WebRtc/ColorGenerator";
import { gameManager } from "../Phaser/Game/GameManager";
type FollowState = "off" | "requesting" | "active" | "ending";
type FollowRole = "leader" | "follower";
export const followStateStore = writable<FollowState>("off");
export const followRoleStore = writable<FollowRole>("leader");
function createFollowUsersStore() {
const { subscribe, update, set } = writable<number[]>([]);
return {
subscribe,
addFollowRequest(leader: number): void {
followStateStore.set("requesting");
followRoleStore.set("follower");
set([leader]);
},
addFollower(user: number): void {
update((followers) => {
followers.push(user);
return followers;
});
},
/**
* Removes the follower from the store.
* Will update followStateStore and followRoleStore if nobody is following anymore.
* @param user
*/
removeFollower(user: number): void {
update((followers) => {
const oldFollowerCount = followers.length;
followers = followers.filter((id) => id !== user);
if (followers.length === 0 && oldFollowerCount > 0) {
followStateStore.set("off");
followRoleStore.set("leader");
}
return followers;
});
},
stopFollowing(): void {
set([]);
followStateStore.set("off");
followRoleStore.set("leader");
},
};
}
export const followUsersStore = createFollowUsersStore();
/**
* This store contains the color of the follow group. It is derived from the ID of the leader.
*/
export const followUsersColorStore = derived(
[followStateStore, followRoleStore, followUsersStore],
([$followStateStore, $followRoleStore, $followUsersStore]) => {
console.log($followStateStore);
if ($followStateStore !== "active") {
return undefined;
}
if ($followUsersStore.length === 0) {
return undefined;
}
let leaderId: number;
if ($followRoleStore === "leader") {
// Let's get my ID by a quite complicated way....
leaderId = gameManager.getCurrentGameScene().connection?.getUserId() ?? 0;
} else {
leaderId = $followUsersStore[0];
}
// Let's compute a random hue between 0 and 1 that varies enough to be interesting
const hue = ((leaderId * 197) % 255) / 255;
let { r, g, b } = getColorRgbFromHue(hue);
if ($followRoleStore === "follower") {
// Let's make the followers very slightly darker
r *= 0.9;
g *= 0.9;
b *= 0.9;
}
return (Math.round(r * 255) << 16) | (Math.round(g * 255) << 8) | Math.round(b * 255);
}
);

View File

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

View File

@ -0,0 +1,29 @@
import { writable } from "svelte/store";
import { v4 as uuidv4 } from "uuid";
export interface Message {
id: string;
text: string;
}
/**
* A store that contains a list of messages to be displayed.
*/
export function createMessageStore() {
const { subscribe, update } = writable<Message[]>([]);
return {
subscribe,
addMessage: (text: string): void => {
update((messages: Message[]) => {
return [...messages, { id: uuidv4(), text }];
});
},
clearMessageById: (id: string): void => {
update((messages: Message[]) => {
messages = messages.filter((message) => message.id !== id);
return messages;
});
},
};
}

View File

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

View File

@ -1,13 +1,29 @@
export function getRandomColor(): string {
const { r, g, b } = getColorRgbFromHue(Math.random());
return toHexa(r, g, b);
}
function toHexa(r: number, g: number, b: number): string {
return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16);
}
export function getColorRgbFromHue(hue: number): { r: number; g: number; b: number } {
const golden_ratio_conjugate = 0.618033988749895;
let hue = Math.random();
hue += golden_ratio_conjugate;
hue %= 1;
return hsv_to_rgb(hue, 0.5, 0.95);
}
function stringToDouble(string: string): number {
let num = 1;
for (const char of string.split("")) {
num *= char.charCodeAt(0);
}
return (num % 255) / 255;
}
//todo: test this.
function hsv_to_rgb(hue: number, saturation: number, brightness: number): string {
function hsv_to_rgb(hue: number, saturation: number, brightness: number): { r: number; g: number; b: number } {
const h_i = Math.floor(hue * 6);
const f = hue * 6 - h_i;
const p = brightness * (1 - saturation);
@ -48,5 +64,9 @@ function hsv_to_rgb(hue: number, saturation: number, brightness: number): string
default:
throw "h_i cannot be " + h_i;
}
return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16);
return {
r,
g,
b,
};
}

View File

@ -1,22 +1,24 @@
import "jasmine";
import {PlayerMovement} from "../../../src/Phaser/Game/PlayerMovement";
import { PlayerMovement } from "../../../src/Phaser/Game/PlayerMovement";
describe("Interpolation / Extrapolation", () => {
it("should interpolate", () => {
const playerMovement = new PlayerMovement({
x: 100, y: 200
}, 42000,
const playerMovement = new PlayerMovement(
{
x: 100,
y: 200,
},
42000,
{
x: 200,
y: 100,
oldX: 100,
oldY: 200,
moving: true,
direction: "up"
direction: "up",
},
42200
);
);
expect(playerMovement.isOutdated(42100)).toBe(false);
expect(playerMovement.isOutdated(43000)).toBe(true);
@ -26,8 +28,8 @@ describe("Interpolation / Extrapolation", () => {
y: 150,
oldX: 100,
oldY: 200,
direction: 'up',
moving: true
direction: "up",
moving: true,
});
expect(playerMovement.getPosition(42200)).toEqual({
@ -35,8 +37,8 @@ describe("Interpolation / Extrapolation", () => {
y: 100,
oldX: 100,
oldY: 200,
direction: 'up',
moving: true
direction: "up",
moving: true,
});
expect(playerMovement.getPosition(42300)).toEqual({
@ -44,22 +46,25 @@ describe("Interpolation / Extrapolation", () => {
y: 50,
oldX: 100,
oldY: 200,
direction: 'up',
moving: true
direction: "up",
moving: true,
});
});
it("should not extrapolate if we stop", () => {
const playerMovement = new PlayerMovement({
x: 100, y: 200
}, 42000,
const playerMovement = new PlayerMovement(
{
x: 100,
y: 200,
},
42000,
{
x: 200,
y: 100,
oldX: 100,
oldY: 200,
moving: false,
direction: "up"
direction: "up",
},
42200
);
@ -69,22 +74,25 @@ describe("Interpolation / Extrapolation", () => {
y: 100,
oldX: 100,
oldY: 200,
direction: 'up',
moving: false
direction: "up",
moving: false,
});
});
it("should should keep moving until it stops", () => {
const playerMovement = new PlayerMovement({
x: 100, y: 200
}, 42000,
it("should keep moving until it stops", () => {
const playerMovement = new PlayerMovement(
{
x: 100,
y: 200,
},
42000,
{
x: 200,
y: 100,
oldX: 100,
oldY: 200,
moving: false,
direction: "up"
direction: "up",
},
42200
);
@ -94,8 +102,8 @@ describe("Interpolation / Extrapolation", () => {
y: 150,
oldX: 100,
oldY: 200,
direction: 'up',
moving: true
direction: "up",
moving: false,
});
});
})
});

View File

@ -120,6 +120,14 @@
<a href="#" class="testLink" data-testmap="focusable_zone_map.json" target="_blank">Focusable Zones</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-layer-visibility"> Success <input type="radio" name="test-layer-visibility"> Failure <input type="radio" name="test-layer-visibility" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="layer-visibility.json" target="_blank">Layer visibility + Layer size and offset</a>
</td>
</tr>
</table>
<h2>Iframe API</h2>
<table class="table">

View File

@ -0,0 +1,148 @@
{ "compressionlevel":-1,
"height":9,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":9,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":9,
"x":0,
"y":0
},
{
"data":[34, 34, 34, 0, 0, 0, 0, 0, 0, 34, 34, 34, 0, 0, 0, 0, 0, 0, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":9,
"id":7,
"name":"invisible",
"opacity":1,
"type":"tilelayer",
"visible":false,
"width":9,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":9,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":9,
"x":0,
"y":0
},
{
"data":[78, 79, 80, 89, 90, 91, 100, 101, 102],
"height":3,
"id":9,
"name":"smaller-offset",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":3,
"x":3,
"y":3
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":113.62434712736,
"id":2,
"name":"",
"rotation":0,
"text":
{
"text":"The striped tiles in the top left corner should be invisible.",
"wrap":true
},
"type":"",
"visible":true,
"width":108.583142912147,
"x":4.76293503917236,
"y":3.18514798446498
},
{
"height":196.249229945092,
"id":3,
"name":"",
"rotation":0,
"text":
{
"text":"The striped tiles in the top right corner should be transparent and appear above the player.",
"wrap":true
},
"type":"",
"visible":true,
"width":148.867058808759,
"x":137.442726119928,
"y":2.49946430962904
},
{
"height":72.9975893933307,
"id":4,
"name":"",
"rotation":0,
"text":
{
"text":"The plant should be here (the center of the map)",
"wrap":true
},
"type":"",
"visible":true,
"width":151.609793508102,
"x":78.3025091653274,
"y":173.920383018616
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 34, 34, 34, 0, 0, 0, 0, 0, 0, 34, 34, 34, 0, 0, 0, 0, 0, 0, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":9,
"id":8,
"name":"transparent",
"opacity":0.4,
"type":"tilelayer",
"visible":true,
"width":9,
"x":0,
"y":0
}],
"nextlayerid":10,
"nextobjectid":5,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.7.2",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":"1.6",
"width":9
}

View File

@ -8,12 +8,10 @@ import { isCharacterTexture } from "./CharacterTexture";
export const isAdminApiData = new tg.IsInterface()
.withProperties({
roomUrl: tg.isString,
email: tg.isNullable(tg.isString),
mapUrlStart: tg.isString,
tags: tg.isArray(tg.isString),
policy_type: tg.isNumber,
userUuid: tg.isString,
email: tg.isNullable(tg.isString),
roomUrl: tg.isString,
mapUrlStart: tg.isString,
textures: tg.isArray(isCharacterTexture),
})
.withOptionalProperties({

View File

@ -71,12 +71,12 @@ message ReportPlayerMessage {
}
message EmotePromptMessage {
string emote = 2;
string emote = 2;
}
message EmoteEventMessage {
int32 actorUserId = 1;
string emote = 2;
int32 actorUserId = 1;
string emote = 2;
}
message QueryJitsiJwtMessage {
@ -84,6 +84,20 @@ message QueryJitsiJwtMessage {
string tag = 2; // FIXME: rather than reading the tag from the query, we should read it from the current map!
}
message FollowRequestMessage {
int32 leader = 1;
}
message FollowConfirmationMessage {
int32 leader = 1;
int32 follower = 2;
}
message FollowAbortMessage {
int32 leader = 1;
int32 follower = 2;
}
message ClientToServerMessage {
oneof message {
UserMovesMessage userMovesMessage = 2;
@ -99,6 +113,9 @@ message ClientToServerMessage {
QueryJitsiJwtMessage queryJitsiJwtMessage = 12;
EmotePromptMessage emotePromptMessage = 13;
VariableMessage variableMessage = 14;
FollowRequestMessage followRequestMessage = 15;
FollowConfirmationMessage followConfirmationMessage = 16;
FollowAbortMessage followAbortMessage = 17;
}
}
@ -243,14 +260,14 @@ message SendUserMessage{
message WorldFullWarningMessage{
}
message WorldFullWarningToRoomMessage{
string roomId = 1;
string roomId = 1;
}
message RefreshRoomPromptMessage{
string roomId = 1;
string roomId = 1;
}
message RefreshRoomMessage{
string roomId = 1;
int32 versionNumber = 2;
string roomId = 1;
int32 versionNumber = 2;
}
message WorldFullMessage{
@ -292,6 +309,9 @@ message ServerToClientMessage {
WorldConnexionMessage worldConnexionMessage = 18;
//EmoteEventMessage emoteEventMessage = 19;
TokenExpiredMessage tokenExpiredMessage = 20;
FollowRequestMessage followRequestMessage = 21;
FollowConfirmationMessage followConfirmationMessage = 22;
FollowAbortMessage followAbortMessage = 23;
}
}
@ -378,6 +398,9 @@ message PusherToBackMessage {
BanUserMessage banUserMessage = 13;
EmotePromptMessage emotePromptMessage = 14;
VariableMessage variableMessage = 15;
FollowRequestMessage followRequestMessage = 16;
FollowConfirmationMessage followConfirmationMessage = 17;
FollowAbortMessage followAbortMessage = 18;
}
}

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 pusher/yarn.lock pusher/package.json ./
RUN yarn install
@ -16,7 +16,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 pusher/yarn.lock pusher/package.json ./
COPY --from=builder2 /usr/src/dist /usr/src/dist

View File

@ -17,6 +17,9 @@ import {
ServerToClientMessage,
CompanionMessage,
EmotePromptMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
VariableMessage,
} from "../Messages/generated/messages_pb";
import { UserMovesMessage } from "../Messages/generated/messages_pb";
@ -477,6 +480,18 @@ export class IoSocketController {
client,
message.getEmotepromptmessage() as EmotePromptMessage
);
} else if (message.hasFollowrequestmessage()) {
socketManager.handleFollowRequest(
client,
message.getFollowrequestmessage() as FollowRequestMessage
);
} else if (message.hasFollowconfirmationmessage()) {
socketManager.handleFollowConfirmation(
client,
message.getFollowconfirmationmessage() as FollowConfirmationMessage
);
} else if (message.hasFollowabortmessage()) {
socketManager.handleFollowAbort(client, message.getFollowabortmessage() as FollowAbortMessage);
}
/* Ok is false if backpressure was built up, wait for drain */

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

@ -75,21 +75,6 @@ class AdminApi {
return res.data;
}
async fetchCheckUserByToken(organizationMemberToken: string): Promise<AdminApiData> {
if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!"));
}
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const res = await Axios.get(ADMIN_API_URL + "/api/check-user/" + organizationMemberToken, {
headers: { Authorization: `${ADMIN_API_TOKEN}` },
});
if (!isAdminApiData(res.data)) {
console.error("Message received from /api/check-user is not in the expected format. Message: ", res.data);
throw new Error("Message received from /api/check-user is not in the expected format.");
}
return res.data;
}
reportPlayer(
reportedUserUuid: string,
reportedUserComment: string,

View File

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

View File

@ -8,6 +8,9 @@ import {
CharacterLayerMessage,
EmoteEventMessage,
EmotePromptMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
GroupDeleteMessage,
ItemEventMessage,
JoinRoomMessage,
@ -271,6 +274,24 @@ export class SocketManager implements ZoneEventListener {
this.handleViewport(client, viewport.toObject());
}
handleFollowRequest(client: ExSocketInterface, message: FollowRequestMessage): void {
const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setFollowrequestmessage(message);
client.backConnection.write(pusherToBackMessage);
}
handleFollowConfirmation(client: ExSocketInterface, message: FollowConfirmationMessage): void {
const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setFollowconfirmationmessage(message);
client.backConnection.write(pusherToBackMessage);
}
handleFollowAbort(client: ExSocketInterface, message: FollowAbortMessage): void {
const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setFollowabortmessage(message);
client.backConnection.write(pusherToBackMessage);
}
onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void {
const subMessage = new SubMessage();
subMessage.setEmoteeventmessage(emoteMessage);

View File

@ -14,7 +14,7 @@ You'll need to adapt the `ADMIN_API_TOKEN` to the value you use in your `.env` f
Alternatively, you can use docker-compose to run the tests:
```console
$ docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
$ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
```
Note: by default, tests are running in Chrome locally and in Chromium in the Docker image.