2020-10-20 16:39:23 +02:00
|
|
|
import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
2020-11-13 18:00:22 +01:00
|
|
|
import {GameRoomPolicyTypes} from "../Model/PusherRoom";
|
2020-09-21 11:24:03 +02:00
|
|
|
import {PointInterface} from "../Model/Websocket/PointInterface";
|
2020-09-18 15:51:15 +02:00
|
|
|
import {
|
|
|
|
SetPlayerDetailsMessage,
|
|
|
|
SubMessage,
|
2020-09-24 17:24:37 +02:00
|
|
|
BatchMessage,
|
2020-09-28 18:52:54 +02:00
|
|
|
ItemEventMessage,
|
|
|
|
ViewportMessage,
|
|
|
|
ClientToServerMessage,
|
2020-09-29 16:01:22 +02:00
|
|
|
SilentMessage,
|
|
|
|
WebRtcSignalToServerMessage,
|
2020-10-06 15:37:00 +02:00
|
|
|
PlayGlobalMessage,
|
2020-10-15 17:25:16 +02:00
|
|
|
ReportPlayerMessage,
|
2020-10-19 19:32:47 +02:00
|
|
|
QueryJitsiJwtMessage
|
2020-09-24 11:16:08 +02:00
|
|
|
} from "../Messages/generated/messages_pb";
|
|
|
|
import {UserMovesMessage} from "../Messages/generated/messages_pb";
|
2020-10-06 18:09:23 +02:00
|
|
|
import {TemplatedApp} from "uWebSockets.js"
|
2020-09-30 10:12:40 +02:00
|
|
|
import {parse} from "query-string";
|
2020-10-09 14:53:18 +02:00
|
|
|
import {jwtTokenManager} from "../Services/JWTTokenManager";
|
2020-10-20 16:39:23 +02:00
|
|
|
import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi";
|
|
|
|
import {SocketManager, socketManager} from "../Services/SocketManager";
|
2020-11-10 18:26:46 +01:00
|
|
|
import {emitInBatch} from "../Services/IoSocketHelpers";
|
2020-10-16 14:36:43 +02:00
|
|
|
import {clientEventsEmitter} from "../Services/ClientEventsEmitter";
|
2020-11-10 18:26:46 +01:00
|
|
|
import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable";
|
2020-11-13 18:00:22 +01:00
|
|
|
import {Zone} from "_Model/Zone";
|
2020-12-10 17:46:15 +01:00
|
|
|
import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface";
|
2020-04-04 04:08:12 +02:00
|
|
|
|
2020-05-03 16:28:18 +02:00
|
|
|
export class IoSocketController {
|
2020-09-18 13:57:38 +02:00
|
|
|
private nextUserId: number = 1;
|
2020-05-03 16:28:18 +02:00
|
|
|
|
2020-09-28 18:52:54 +02:00
|
|
|
constructor(private readonly app: TemplatedApp) {
|
2020-04-04 04:08:12 +02:00
|
|
|
this.ioConnection();
|
2020-10-15 17:25:16 +02:00
|
|
|
this.adminRoomSocket();
|
2020-05-08 00:35:36 +02:00
|
|
|
}
|
|
|
|
|
2020-10-15 17:25:16 +02:00
|
|
|
adminRoomSocket() {
|
2020-10-16 14:36:43 +02:00
|
|
|
this.app.ws('/admin/rooms', {
|
|
|
|
upgrade: (res, req, context) => {
|
|
|
|
const query = parse(req.getQuery());
|
|
|
|
const websocketKey = req.getHeader('sec-websocket-key');
|
|
|
|
const websocketProtocol = req.getHeader('sec-websocket-protocol');
|
|
|
|
const websocketExtensions = req.getHeader('sec-websocket-extensions');
|
|
|
|
const token = query.token;
|
|
|
|
if (token !== ADMIN_API_TOKEN) {
|
|
|
|
console.log('Admin access refused for token: '+token)
|
|
|
|
res.writeStatus("401 Unauthorized").end('Incorrect token');
|
2020-11-13 18:00:22 +01:00
|
|
|
return;
|
2020-10-16 14:36:43 +02:00
|
|
|
}
|
2020-12-10 17:46:15 +01:00
|
|
|
const roomId = query.roomId;
|
|
|
|
if (typeof roomId !== 'string') {
|
|
|
|
console.error('Received')
|
|
|
|
res.writeStatus("400 Bad Request").end('Missing room id');
|
|
|
|
return;
|
|
|
|
}
|
2020-10-16 14:36:43 +02:00
|
|
|
|
|
|
|
res.upgrade(
|
|
|
|
{roomId},
|
|
|
|
websocketKey, websocketProtocol, websocketExtensions, context,
|
|
|
|
);
|
|
|
|
},
|
2020-10-15 17:25:16 +02:00
|
|
|
open: (ws) => {
|
2020-10-16 14:36:43 +02:00
|
|
|
console.log('Admin socket connect for room: '+ws.roomId);
|
2020-12-10 17:46:15 +01:00
|
|
|
const roomId = ws.roomId;
|
|
|
|
ws.disconnecting = false;
|
|
|
|
|
|
|
|
socketManager.handleAdminRoom(ws as ExAdminSocketInterface, ws.roomId as string);
|
|
|
|
|
|
|
|
/*ws.send('Data:'+JSON.stringify(socketManager.getAdminSocketDataFor(ws.roomId as string)));
|
2020-10-16 14:36:43 +02:00
|
|
|
ws.clientJoinCallback = (clientUUid: string, roomId: string) => {
|
|
|
|
const wsroomId = ws.roomId as string;
|
|
|
|
if(wsroomId === roomId) {
|
|
|
|
ws.send('MemberJoin:'+clientUUid+';'+roomId);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
ws.clientLeaveCallback = (clientUUid: string, roomId: string) => {
|
|
|
|
const wsroomId = ws.roomId as string;
|
|
|
|
if(wsroomId === roomId) {
|
|
|
|
ws.send('MemberLeave:'+clientUUid+';'+roomId);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
clientEventsEmitter.registerToClientJoin(ws.clientJoinCallback);
|
2020-12-10 17:46:15 +01:00
|
|
|
clientEventsEmitter.registerToClientLeave(ws.clientLeaveCallback);*/
|
2020-10-15 17:25:16 +02:00
|
|
|
},
|
|
|
|
message: (ws, arrayBuffer, isBinary): void => {
|
2020-10-19 19:32:47 +02:00
|
|
|
try {
|
|
|
|
//TODO refactor message type and data
|
2020-10-19 21:04:16 +02:00
|
|
|
const message: {event: string, message: {type: string, message: unknown, userUuid: string}} =
|
2020-10-19 19:32:47 +02:00
|
|
|
JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer)));
|
|
|
|
|
|
|
|
if(message.event === 'user-message') {
|
2020-10-20 08:30:11 +02:00
|
|
|
const messageToEmit = (message.message as { message: string, type: string, userUuid: string });
|
|
|
|
switch (message.message.type) {
|
2020-10-20 09:24:06 +02:00
|
|
|
case 'ban': {
|
2020-10-20 08:30:11 +02:00
|
|
|
socketManager.emitSendUserMessage(messageToEmit);
|
|
|
|
break;
|
2020-10-20 09:24:06 +02:00
|
|
|
}
|
|
|
|
case 'banned': {
|
2020-10-20 08:30:11 +02:00
|
|
|
const socketUser = socketManager.emitSendUserMessage(messageToEmit);
|
|
|
|
setTimeout(() => {
|
|
|
|
socketUser.close();
|
|
|
|
}, 10000);
|
|
|
|
break;
|
2020-10-20 09:24:06 +02:00
|
|
|
}
|
|
|
|
default: {
|
|
|
|
break;
|
|
|
|
}
|
2020-10-20 08:20:21 +02:00
|
|
|
}
|
2020-10-19 19:32:47 +02:00
|
|
|
}
|
|
|
|
}catch (err) {
|
|
|
|
console.error(err);
|
|
|
|
}
|
2020-10-15 17:25:16 +02:00
|
|
|
},
|
|
|
|
close: (ws, code, message) => {
|
2020-12-10 17:46:15 +01:00
|
|
|
const Client = (ws as ExAdminSocketInterface);
|
|
|
|
try {
|
|
|
|
Client.disconnecting = true;
|
|
|
|
//leave room
|
|
|
|
socketManager.leaveAdminRoom(Client);
|
|
|
|
} catch (e) {
|
|
|
|
console.error('An error occurred on admin "disconnect"');
|
|
|
|
console.error(e);
|
|
|
|
}
|
2020-10-15 17:25:16 +02:00
|
|
|
}
|
2020-10-16 14:36:43 +02:00
|
|
|
})
|
2020-10-15 17:25:16 +02:00
|
|
|
}
|
2020-04-05 15:51:47 +02:00
|
|
|
|
2020-09-28 18:52:54 +02:00
|
|
|
ioConnection() {
|
2020-10-12 16:23:07 +02:00
|
|
|
this.app.ws('/room', {
|
2020-09-28 18:52:54 +02:00
|
|
|
/* Options */
|
|
|
|
//compression: uWS.SHARED_COMPRESSOR,
|
2020-11-10 18:26:46 +01:00
|
|
|
idleTimeout: SOCKET_IDLE_TIMER,
|
2020-09-28 18:52:54 +02:00
|
|
|
maxPayloadLength: 16 * 1024 * 1024,
|
2020-09-30 12:16:39 +02:00
|
|
|
maxBackpressure: 65536, // Maximum 64kB of data in the buffer.
|
2020-09-29 16:01:22 +02:00
|
|
|
//idleTimeout: 10,
|
2020-09-30 10:12:40 +02:00
|
|
|
upgrade: (res, req, context) => {
|
2020-09-30 12:16:39 +02:00
|
|
|
//console.log('An Http connection wants to become WebSocket, URL: ' + req.getUrl() + '!');
|
2020-09-30 10:12:40 +02:00
|
|
|
(async () => {
|
|
|
|
/* Keep track of abortions */
|
|
|
|
const upgradeAborted = {aborted: false};
|
|
|
|
|
|
|
|
res.onAborted(() => {
|
|
|
|
/* We can simply signal that we were aborted */
|
|
|
|
upgradeAborted.aborted = true;
|
2020-07-27 22:36:07 +02:00
|
|
|
});
|
2020-04-04 12:42:02 +02:00
|
|
|
|
2020-09-30 10:12:40 +02:00
|
|
|
try {
|
2020-10-09 16:18:25 +02:00
|
|
|
const url = req.getUrl();
|
2020-10-06 15:37:00 +02:00
|
|
|
const query = parse(req.getQuery());
|
2020-10-09 16:18:25 +02:00
|
|
|
const websocketKey = req.getHeader('sec-websocket-key');
|
|
|
|
const websocketProtocol = req.getHeader('sec-websocket-protocol');
|
|
|
|
const websocketExtensions = req.getHeader('sec-websocket-extensions');
|
2020-10-06 15:37:00 +02:00
|
|
|
|
2020-10-12 16:23:07 +02:00
|
|
|
const roomId = query.roomId;
|
|
|
|
if (typeof roomId !== 'string') {
|
|
|
|
throw new Error('Undefined room ID: ');
|
|
|
|
}
|
2020-10-06 18:09:23 +02:00
|
|
|
|
2020-10-06 15:37:00 +02:00
|
|
|
const token = query.token;
|
2020-10-06 18:09:23 +02:00
|
|
|
const x = Number(query.x);
|
|
|
|
const y = Number(query.y);
|
|
|
|
const top = Number(query.top);
|
|
|
|
const bottom = Number(query.bottom);
|
|
|
|
const left = Number(query.left);
|
|
|
|
const right = Number(query.right);
|
|
|
|
const name = query.name;
|
|
|
|
if (typeof name !== 'string') {
|
|
|
|
throw new Error('Expecting name');
|
|
|
|
}
|
|
|
|
if (name === '') {
|
|
|
|
throw new Error('No empty name');
|
|
|
|
}
|
|
|
|
let characterLayers = query.characterLayers;
|
|
|
|
if (characterLayers === null) {
|
|
|
|
throw new Error('Expecting skin');
|
|
|
|
}
|
|
|
|
if (typeof characterLayers === 'string') {
|
|
|
|
characterLayers = [ characterLayers ];
|
|
|
|
}
|
2020-10-06 15:37:00 +02:00
|
|
|
|
2020-10-09 14:53:18 +02:00
|
|
|
const userUuid = await jwtTokenManager.getUserUuidFromToken(token);
|
|
|
|
|
2020-10-14 11:07:34 +02:00
|
|
|
let memberTags: string[] = [];
|
2020-10-20 16:39:23 +02:00
|
|
|
let memberTextures: CharacterTexture[] = [];
|
2020-10-15 17:25:16 +02:00
|
|
|
const room = await socketManager.getOrCreateRoom(roomId);
|
2020-11-13 18:00:22 +01:00
|
|
|
// TODO: make sure the room isFull is ported in the back part.
|
|
|
|
/*if(room.isFull){
|
2020-10-21 23:45:08 +02:00
|
|
|
throw new Error('Room is full');
|
2020-11-13 18:00:22 +01:00
|
|
|
}*/
|
2020-11-05 11:25:35 +01:00
|
|
|
if (ADMIN_API_URL) {
|
|
|
|
try {
|
|
|
|
const userData = await adminApi.fetchMemberDataByUuid(userUuid);
|
|
|
|
//console.log('USERDATA', userData)
|
|
|
|
memberTags = userData.tags;
|
|
|
|
memberTextures = userData.textures;
|
|
|
|
if (!room.anonymous && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && !room.canAccess(memberTags)) {
|
|
|
|
throw new Error('No correct tags')
|
|
|
|
}
|
|
|
|
//console.log('access granted for user '+userUuid+' and room '+roomId);
|
|
|
|
} catch (e) {
|
|
|
|
console.log('access not granted for user '+userUuid+' and room '+roomId);
|
|
|
|
console.error(e);
|
|
|
|
throw new Error('Client cannot acces this ressource.')
|
2020-10-13 16:46:46 +02:00
|
|
|
}
|
2020-10-09 14:53:18 +02:00
|
|
|
}
|
2020-09-15 16:21:41 +02:00
|
|
|
|
2020-10-20 16:39:23 +02:00
|
|
|
// Generate characterLayers objects from characterLayers string[]
|
|
|
|
const characterLayerObjs: CharacterLayer[] = SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);
|
|
|
|
|
2020-09-30 10:12:40 +02:00
|
|
|
if (upgradeAborted.aborted) {
|
|
|
|
console.log("Ouch! Client disconnected before we could upgrade it!");
|
|
|
|
/* You must not upgrade now */
|
|
|
|
return;
|
|
|
|
}
|
2020-09-15 16:21:41 +02:00
|
|
|
|
2020-09-30 10:12:40 +02:00
|
|
|
/* This immediately calls open handler, you must not use res after this call */
|
|
|
|
res.upgrade({
|
|
|
|
// Data passed here is accessible on the "websocket" socket object.
|
2020-10-09 16:18:25 +02:00
|
|
|
url,
|
2020-10-06 15:37:00 +02:00
|
|
|
token,
|
2020-10-06 18:09:23 +02:00
|
|
|
userUuid,
|
|
|
|
roomId,
|
|
|
|
name,
|
2020-10-20 16:39:23 +02:00
|
|
|
characterLayers: characterLayerObjs,
|
2020-10-14 16:00:25 +02:00
|
|
|
tags: memberTags,
|
2020-10-20 16:39:23 +02:00
|
|
|
textures: memberTextures,
|
2020-10-06 18:09:23 +02:00
|
|
|
position: {
|
|
|
|
x: x,
|
|
|
|
y: y,
|
|
|
|
direction: 'down',
|
|
|
|
moving: false
|
|
|
|
} as PointInterface,
|
|
|
|
viewport: {
|
|
|
|
top,
|
|
|
|
right,
|
|
|
|
bottom,
|
|
|
|
left
|
2020-10-14 16:00:25 +02:00
|
|
|
}
|
2020-09-30 10:12:40 +02:00
|
|
|
},
|
|
|
|
/* Spell these correctly */
|
2020-10-09 16:18:25 +02:00
|
|
|
websocketKey,
|
|
|
|
websocketProtocol,
|
|
|
|
websocketExtensions,
|
2020-09-30 10:12:40 +02:00
|
|
|
context);
|
|
|
|
|
2020-09-30 10:17:01 +02:00
|
|
|
} catch (e) {
|
2020-09-30 10:12:40 +02:00
|
|
|
if (e instanceof Error) {
|
2020-10-09 16:18:25 +02:00
|
|
|
console.log(e.message);
|
2020-09-30 10:12:40 +02:00
|
|
|
res.writeStatus("401 Unauthorized").end(e.message);
|
|
|
|
} else {
|
2020-10-09 16:18:25 +02:00
|
|
|
console.log(e);
|
2020-09-30 10:12:40 +02:00
|
|
|
res.writeStatus("500 Internal Server Error").end('An error occurred');
|
|
|
|
}
|
2020-09-15 16:21:41 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-09-30 10:12:40 +02:00
|
|
|
})();
|
|
|
|
},
|
2020-09-28 18:52:54 +02:00
|
|
|
/* Handlers */
|
|
|
|
open: (ws) => {
|
2020-10-06 18:09:23 +02:00
|
|
|
// Let's join the room
|
2020-10-15 17:25:16 +02:00
|
|
|
const client = this.initClient(ws); //todo: into the upgrade instead?
|
|
|
|
socketManager.handleJoinRoom(client);
|
2020-10-20 09:20:00 +02:00
|
|
|
|
2020-11-05 11:25:35 +01:00
|
|
|
//get data information and show messages
|
|
|
|
if (ADMIN_API_URL) {
|
|
|
|
adminApi.fetchMemberDataByUuid(client.userUuid).then((res: FetchMemberDataByUuidResponse) => {
|
|
|
|
if (!res.messages) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
res.messages.forEach((c: unknown) => {
|
|
|
|
const messageToSend = c as { type: string, message: string };
|
|
|
|
socketManager.emitSendUserMessage({
|
|
|
|
userUuid: client.userUuid,
|
|
|
|
type: messageToSend.type,
|
|
|
|
message: messageToSend.message
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}).catch((err) => {
|
|
|
|
console.error('fetchMemberDataByUuid => err', err);
|
2020-10-20 09:20:00 +02:00
|
|
|
});
|
2020-11-05 11:25:35 +01:00
|
|
|
}
|
2020-09-28 18:52:54 +02:00
|
|
|
},
|
2020-10-06 15:37:00 +02:00
|
|
|
message: (ws, arrayBuffer, isBinary): void => {
|
2020-09-28 18:52:54 +02:00
|
|
|
const client = ws as ExSocketInterface;
|
|
|
|
const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer));
|
|
|
|
|
2020-10-06 15:37:00 +02:00
|
|
|
if (message.hasViewportmessage()) {
|
2020-11-13 18:00:22 +01:00
|
|
|
socketManager.handleViewport(client, (message.getViewportmessage() as ViewportMessage).toObject());
|
2020-09-28 18:52:54 +02:00
|
|
|
} else if (message.hasUsermovesmessage()) {
|
2020-10-15 17:25:16 +02:00
|
|
|
socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage);
|
2020-09-28 18:52:54 +02:00
|
|
|
} else if (message.hasSetplayerdetailsmessage()) {
|
2020-10-15 17:25:16 +02:00
|
|
|
socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage);
|
2020-09-28 18:52:54 +02:00
|
|
|
} else if (message.hasSilentmessage()) {
|
2020-10-15 17:25:16 +02:00
|
|
|
socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage);
|
2020-09-28 18:52:54 +02:00
|
|
|
} else if (message.hasItemeventmessage()) {
|
2020-10-15 17:25:16 +02:00
|
|
|
socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage);
|
2020-09-29 16:01:22 +02:00
|
|
|
} else if (message.hasWebrtcsignaltoservermessage()) {
|
2020-10-15 17:25:16 +02:00
|
|
|
socketManager.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage);
|
2020-09-29 16:01:22 +02:00
|
|
|
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
2020-10-15 17:25:16 +02:00
|
|
|
socketManager.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage);
|
2020-10-01 14:11:34 +02:00
|
|
|
} else if (message.hasPlayglobalmessage()) {
|
2020-10-15 17:25:16 +02:00
|
|
|
socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage);
|
2020-10-12 11:22:41 +02:00
|
|
|
} else if (message.hasReportplayermessage()){
|
2020-10-15 17:25:16 +02:00
|
|
|
socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);
|
2020-10-16 19:13:26 +02:00
|
|
|
} else if (message.hasQueryjitsijwtmessage()){
|
|
|
|
socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
|
2020-04-04 17:22:02 +02:00
|
|
|
}
|
2020-06-11 23:18:06 +02:00
|
|
|
|
2020-09-28 18:52:54 +02:00
|
|
|
/* Ok is false if backpressure was built up, wait for drain */
|
|
|
|
//let ok = ws.send(message, isBinary);
|
|
|
|
},
|
|
|
|
drain: (ws) => {
|
|
|
|
console.log('WebSocket backpressure: ' + ws.getBufferedAmount());
|
|
|
|
},
|
|
|
|
close: (ws, code, message) => {
|
|
|
|
const Client = (ws as ExSocketInterface);
|
2020-05-12 11:49:55 +02:00
|
|
|
try {
|
2020-09-28 18:52:54 +02:00
|
|
|
Client.disconnecting = true;
|
2020-05-12 11:49:55 +02:00
|
|
|
//leave room
|
2020-10-15 17:25:16 +02:00
|
|
|
socketManager.leaveRoom(Client);
|
2020-05-12 11:49:55 +02:00
|
|
|
} catch (e) {
|
|
|
|
console.error('An error occurred on "disconnect"');
|
|
|
|
console.error(e);
|
|
|
|
}
|
2020-09-28 18:52:54 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2020-08-31 14:03:40 +02:00
|
|
|
|
2020-10-15 17:25:16 +02:00
|
|
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
private initClient(ws: any): ExSocketInterface {
|
|
|
|
const client : ExSocketInterface = ws;
|
|
|
|
client.userId = this.nextUserId;
|
|
|
|
this.nextUserId++;
|
|
|
|
client.userUuid = ws.userUuid;
|
|
|
|
client.token = ws.token;
|
|
|
|
client.batchedMessages = new BatchMessage();
|
|
|
|
client.batchTimeout = null;
|
|
|
|
client.emitInBatch = (payload: SubMessage): void => {
|
|
|
|
emitInBatch(client, payload);
|
|
|
|
}
|
|
|
|
client.disconnecting = false;
|
|
|
|
|
|
|
|
client.name = ws.name;
|
|
|
|
client.tags = ws.tags;
|
2020-10-20 16:39:23 +02:00
|
|
|
client.textures = ws.textures;
|
2020-10-15 17:25:16 +02:00
|
|
|
client.characterLayers = ws.characterLayers;
|
|
|
|
client.roomId = ws.roomId;
|
2020-11-13 18:00:22 +01:00
|
|
|
client.listenedZones = new Set<Zone>();
|
2020-05-18 18:33:04 +02:00
|
|
|
return client;
|
2020-05-03 16:28:18 +02:00
|
|
|
}
|
2020-04-04 22:35:20 +02:00
|
|
|
}
|