mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-27 07:48:01 -07:00
Webatrice P.O.C. (#3854)
* port webclient POC into react shell * Abstract websocket messaging behind redux store * refactor architecture * add rooms store * introduce application service layer and login form * display room messages * implement roomSay * improve Room view styling * display room games * improve gameList update logic * hide protected games * improve game update logic * move mapping to earlier lifecycle hook * add autoscroll to bottom * tabs to spaces, refresh guard * implement server joins/leaves * show users in room * add material-ui to build * refactor, add room joins/leaves to store and render * begin using Material UI components * fix spectatorsCount * remove unused package * improve Server and Room styling * fix scroll context * route on room join * refactor room path * add auth guard * refactor authGuard export * add missing files * clear store on disconnect, add logout button to Account view * fix disconnect handling * Safari fixes * organize current todos * improve login page and server status tracking * improve login page * introduce sorting arch, refine reducers, begin viewLogHistory * audit fix for handlebars * implement moderator log view * comply with code style rules * remove original POC from codebase * add missing semi * minor improvements, begin registration functionality * retry as ws when wss fails additionally, dont mutate the default options when connecting * retain user/pass in WebClient.options for login * take protocol off of options, make it a connect param that defaults to wss * cleanup server page styling * match wss logic with desktop client * add virtual scroll component, add context menu to UserDisplay * revert VirtualTable on messages * improve styling for Room view * add routing to Player view * increase tooltip delay * begin implementing Account view * disable app level contextMenu * implement buddy/ignore list management * fix gitignore Co-authored-by: Jay Letto <jeremy.letto@merrillcorp.com> Co-authored-by: skwerlman <skwerlman@users.noreply.github.com> Co-authored-by: Jeremy Letto <jeremy.letto@datasite.com>
This commit is contained in:
parent
d5b36e8b8a
commit
0457e65751
152 changed files with 19573 additions and 1071 deletions
329
webclient/src/WebClient/WebClient.tsx
Normal file
329
webclient/src/WebClient/WebClient.tsx
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
import protobuf from "protobufjs";
|
||||
|
||||
import { StatusEnum } from "types";
|
||||
|
||||
import * as roomEvents from "./events/RoomEvents";
|
||||
import * as sessionEvents from "./events/SessionEvents";
|
||||
|
||||
import { RoomService, SessionService } from "./services";
|
||||
import { RoomCommands, SessionCommands } from "./commands";
|
||||
|
||||
import ProtoFiles from "./ProtoFiles";
|
||||
|
||||
const roomEventKeys = Object.keys(roomEvents);
|
||||
const sessionEventKeys = Object.keys(sessionEvents);
|
||||
|
||||
interface ApplicationCommands {
|
||||
room: RoomCommands;
|
||||
session: SessionCommands;
|
||||
}
|
||||
|
||||
interface ApplicationServices {
|
||||
room: RoomService;
|
||||
session: SessionService;
|
||||
}
|
||||
|
||||
export class WebClient {
|
||||
private socket: WebSocket;
|
||||
private status: StatusEnum = StatusEnum.DISCONNECTED;
|
||||
private keepalivecb;
|
||||
private lastPingPending = false;
|
||||
private cmdId = 0;
|
||||
private pendingCommands = {};
|
||||
|
||||
public commands: ApplicationCommands;
|
||||
public services: ApplicationServices;
|
||||
|
||||
public protocolVersion = 14;
|
||||
public pb;
|
||||
|
||||
public clientConfig = {
|
||||
"clientver" : "webclient-1.0 (2019-10-31)",
|
||||
"clientfeatures" : [
|
||||
"client_id",
|
||||
"client_ver",
|
||||
"feature_set",
|
||||
"room_chat_history",
|
||||
"client_warnings",
|
||||
/* unimplemented features */
|
||||
"forgot_password",
|
||||
"idle_client",
|
||||
"mod_log_lookup",
|
||||
"user_ban_history",
|
||||
// satisfy server reqs for POC
|
||||
"websocket",
|
||||
"2.6.1_min_version",
|
||||
"2.7.0_min_version",
|
||||
]
|
||||
};
|
||||
|
||||
public options: any = {
|
||||
host: "",
|
||||
port: "",
|
||||
user: "",
|
||||
pass: "",
|
||||
debug: false,
|
||||
autojoinrooms: true,
|
||||
keepalive: 5000
|
||||
};
|
||||
|
||||
constructor() {
|
||||
const files = ProtoFiles.map(file => `${WebClient.PB_FILE_DIR}/${file}`);
|
||||
|
||||
this.pb = new protobuf.Root();
|
||||
this.pb.load(files, { keepCase: false }, (err, root) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// This sucks. I can"t seem to get out of this
|
||||
// circular dependency trap, so this is my current best.
|
||||
this.commands = {
|
||||
room: new RoomCommands(this),
|
||||
session: new SessionCommands(this),
|
||||
};
|
||||
|
||||
this.services = {
|
||||
room: new RoomService(this),
|
||||
session: new SessionService(this),
|
||||
};
|
||||
|
||||
console.log(this);
|
||||
}
|
||||
|
||||
private clearStores() {
|
||||
this.services.room.clearStore();
|
||||
this.services.session.clearStore();
|
||||
}
|
||||
|
||||
public updateStatus(status, description) {
|
||||
console.log(`Status: [${status}]: ${description}`);
|
||||
this.status = status;
|
||||
this.services.session.updateStatus(status, description);
|
||||
|
||||
if (status === StatusEnum.DISCONNECTED) {
|
||||
this.clearStores();
|
||||
this.endPingLoop();
|
||||
this.resetConnectionvars();
|
||||
}
|
||||
}
|
||||
|
||||
public resetConnectionvars() {
|
||||
this.cmdId = 0;
|
||||
this.pendingCommands = {};
|
||||
this.lastPingPending = false;
|
||||
}
|
||||
|
||||
public sendCommand(cmd, callback) {
|
||||
this.cmdId++;
|
||||
cmd["cmdId"] = this.cmdId;
|
||||
|
||||
this.pendingCommands[this.cmdId] = callback;
|
||||
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(this.pb.CommandContainer.encode(cmd).finish());
|
||||
this.debug(() => console.log("Sent: " + cmd.toString()));
|
||||
} else {
|
||||
this.debug(() => console.log("Send: Not connected"));
|
||||
}
|
||||
}
|
||||
|
||||
public sendRoomCommand(roomId, roomCmd, callback?) {
|
||||
const cmd = this.pb.CommandContainer.create({
|
||||
"roomId" : roomId,
|
||||
"roomCommand" : [ roomCmd ]
|
||||
});
|
||||
|
||||
this.sendCommand(cmd, raw => {
|
||||
this.debug(() => console.log(raw));
|
||||
|
||||
if (callback) {
|
||||
callback(raw);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public sendSessionCommand(sesCmd, callback?) {
|
||||
const cmd = this.pb.CommandContainer.create({
|
||||
"sessionCommand" : [ sesCmd ]
|
||||
});
|
||||
|
||||
this.sendCommand(cmd, (raw) => {
|
||||
this.debug(() => console.log(raw));
|
||||
|
||||
if (callback) {
|
||||
callback(raw);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public sendModeratorCommand(modCmd, callback?) {
|
||||
const cmd = this.pb.CommandContainer.create({
|
||||
"moderatorCommand" : [ modCmd ]
|
||||
});
|
||||
|
||||
this.sendCommand(cmd, (raw) => {
|
||||
this.debug(() => console.log(raw));
|
||||
|
||||
if (callback) {
|
||||
callback(raw);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public startPingLoop() {
|
||||
this.keepalivecb = setInterval(() => {
|
||||
// check if the previous ping got no reply
|
||||
if (this.lastPingPending) {
|
||||
this.disconnect();
|
||||
|
||||
this.updateStatus(StatusEnum.DISCONNECTED, "Connection timeout");
|
||||
}
|
||||
|
||||
// stop the ping loop if we"re disconnected
|
||||
if (this.status !== StatusEnum.LOGGEDIN) {
|
||||
this.endPingLoop();
|
||||
return;
|
||||
}
|
||||
|
||||
// send a ping
|
||||
this.lastPingPending = true;
|
||||
|
||||
const ping = this.pb.Command_Ping.create();
|
||||
const command = this.pb.SessionCommand.create({
|
||||
".Command_Ping.ext" : ping
|
||||
});
|
||||
|
||||
this.sendSessionCommand(command, () => this.lastPingPending = false);
|
||||
}, this.options.keepalive);
|
||||
}
|
||||
|
||||
private endPingLoop() {
|
||||
clearInterval(this.keepalivecb);
|
||||
this.keepalivecb = null;
|
||||
}
|
||||
|
||||
public connect(options) {
|
||||
this.options = { ...this.options, ...options };
|
||||
|
||||
const { host, port } = this.options;
|
||||
const protocol = port === '443' ? 'wss' : 'ws';
|
||||
|
||||
this.socket = new WebSocket(protocol + "://" + host + ":" + port);
|
||||
this.socket.binaryType = "arraybuffer"; // We are talking binary
|
||||
|
||||
this.socket.onopen = () => {
|
||||
this.updateStatus(StatusEnum.CONNECTED, "Connected");
|
||||
};
|
||||
|
||||
this.socket.onclose = () => {
|
||||
// dont overwrite failure messages
|
||||
if (this.status !== StatusEnum.DISCONNECTED) {
|
||||
this.updateStatus(StatusEnum.DISCONNECTED, "Connection Closed");
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onerror = () => {
|
||||
this.updateStatus(StatusEnum.DISCONNECTED, "Connection Failed");
|
||||
};
|
||||
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
const msg = this.decodeServerMessage(event);
|
||||
|
||||
if (msg) {
|
||||
switch (msg.messageType) {
|
||||
case this.pb.ServerMessage.MessageType.RESPONSE:
|
||||
this.processServerResponse(msg.response);
|
||||
break;
|
||||
case this.pb.ServerMessage.MessageType.ROOM_EVENT:
|
||||
this.processRoomEvent(msg.roomEvent, msg);
|
||||
break;
|
||||
case this.pb.ServerMessage.MessageType.SESSION_EVENT:
|
||||
this.processSessionEvent(msg.sessionEvent, msg);
|
||||
break;
|
||||
case this.pb.ServerMessage.MessageType.GAME_EVENT_CONTAINER:
|
||||
// @TODO
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
public debug(debug) {
|
||||
if (this.options.debug) {
|
||||
debug();
|
||||
}
|
||||
}
|
||||
|
||||
private decodeServerMessage(event) {
|
||||
const uint8msg = new Uint8Array(event.data);
|
||||
let msg;
|
||||
|
||||
try {
|
||||
msg = this.pb.ServerMessage.decode(uint8msg);
|
||||
|
||||
this.debug(() => console.log(msg));
|
||||
|
||||
return msg;
|
||||
} catch (err) {
|
||||
console.error("Processing failed:", err);
|
||||
|
||||
this.debug(() => {
|
||||
let str = "";
|
||||
|
||||
for (let i = 0; i < uint8msg.length; i++) {
|
||||
str += String.fromCharCode(uint8msg[i]);
|
||||
}
|
||||
|
||||
console.log(str);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private processServerResponse(response) {
|
||||
const cmdId = response.cmdId;
|
||||
|
||||
if (!this.pendingCommands.hasOwnProperty(cmdId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingCommands[cmdId](response);
|
||||
delete this.pendingCommands[cmdId];
|
||||
}
|
||||
|
||||
private processRoomEvent(response, raw) {
|
||||
this.processEvent(response, roomEvents, roomEventKeys, raw);
|
||||
}
|
||||
|
||||
private processSessionEvent(response, raw) {
|
||||
this.processEvent(response, sessionEvents, sessionEventKeys, raw);
|
||||
}
|
||||
|
||||
private processEvent(response, events, keys, raw) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const event = events[key];
|
||||
const payload = response[event.id];
|
||||
|
||||
if (payload) {
|
||||
events[key].action(payload, this, raw);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static PB_FILE_DIR = `${process.env.PUBLIC_URL}/pb`;
|
||||
}
|
||||
|
||||
const webClient = new WebClient();
|
||||
export default webClient;
|
||||
Loading…
Add table
Add a link
Reference in a new issue