import * as proto from "./pianoverse_pb"; import EventEmitter from "node:events"; import type TypedEmitter from "typed-emitter"; import UserAgent from "user-agents"; const CEventType = proto.ClientMessage_EventType; const SEventType = proto.ServerMessage_EventType; type MessageEvents = { open: () => void; close: () => void; message: ( user: { id: string; name: string; color: string }, content: string ) => void; welcome: () => void; rooms: (rooms: proto.ServerMessage_Room[]) => void; join: (join: proto.Profile) => void; leave: (id: string) => void; chown: () => void; serverMessage: (serverMessage: string) => void; }; interface Player { id: string; name: string; color: string; role?: number; x?: number; y?: number; } export class Client extends (EventEmitter as new () => TypedEmitter) { private ws: WebSocket; me!: Player; chatHistory: proto.ServerMessage_Chat[] = []; room: { name?: string; owner?: string; } = {}; rooms: proto.ServerMessage_Room[] = []; players = new Map(); private resolvePingPromise?: (a: number) => void; private lastPing: number = -1; move(x: number, y: number) { this.ws.send( new proto.ClientMessage({ event: CEventType.MOVE, move: { x, y, }, }).toBinary() ); } getPing() { this.ws.send( new proto.ClientMessage({ event: CEventType.PING, }).toBinary() ); this.lastPing = Date.now(); return new Promise((res, rej) => { this.resolvePingPromise = res; }); } message(chat: string) { chat.match(/.{1,200}/gm)?.forEach((z, i) => { setTimeout(() => { this.ws.send( new proto.ClientMessage({ event: CEventType.CHAT, chat: z, }).toBinary() ); }, 500 * i); }); } keyDown(key: number, velocity: number) { this.ws.send( new proto.ClientMessage({ event: CEventType.PRESS, press: { key, vel: velocity, }, }).toBinary() ); } keyUp(key: number) { this.ws.send( new proto.ClientMessage({ event: CEventType.RELEASE, release: { key, }, }).toBinary() ); } setProfile(name: string, color: string) { this.ws.send( new proto.ClientMessage({ event: CEventType.PROFILE, profile: { name, color, }, }).toBinary() ); } setRoom(room: string, priv?: boolean) { this.ws.send( new proto.ClientMessage({ event: CEventType.ROOM, room: { room: room, private: priv, }, }).toBinary() ); } constructor(url: string) { super(); this.ws = new WebSocket(url.replace("http", "ws"), { //@ts-expect-error headers: { Origin: url, "User-Agent": new UserAgent().toString(), }, protocol: "pianoverse", }); this.ws.addEventListener("open", () => { this.ws.binaryType = "arraybuffer"; setInterval(() => { this.ws.send( new proto.ClientMessage({ event: CEventType.HEARTBEAT, }).toBinary() ); }, 2000); this.emit("open"); }); this.ws.addEventListener("message", (e) => { let data = new Uint8Array(e.data); let decode; try { decode = proto.ServerMessage.fromBinary(data); } catch { console.log("Could not decode data."); console.log(data); return; } if (decode.event == SEventType.PONG) { if (this.resolvePingPromise) this.resolvePingPromise(Date.now() - this.lastPing); } if (decode.event == SEventType.CHOWN) { this.room.owner = decode.chown; this.emit("chown"); } if (decode.event == SEventType.MESSAGE) { this.emit("serverMessage", decode.message); } if (decode.event == SEventType.RATELIMIT) { console.log("Ratelimit reached! Time left: " + decode.ratelimit); } if (decode.event == SEventType.JOIN) { this.players.set(decode.join!.id, decode.join!); this.emit("join", decode.join!); } if (decode.event == SEventType.LEAVE) { this.emit("leave", decode.leave); this.players.delete(decode.leave!); } if (decode.event == SEventType.ROOMS) { this.rooms = decode.rooms; this.emit("rooms", decode.rooms); } if (decode.event == SEventType.WELCOME) { this.me = { id: decode.welcome!.id, name: decode.welcome!.name, color: decode.welcome!.color, }; this.room = { name: decode.welcome!.room, owner: decode.welcome!.owner, }; this.chatHistory = decode.welcome!.chat; this.emit("welcome"); } if (decode.event == SEventType.CHAT) { this.emit( "message", { id: decode.chat!.id, name: decode.chat!.name, color: decode.chat!.color, }, decode.chat!.content ); } }); } }