import { PacketDefinitions, PacketReader, Player, Plugin, World, } from "./classes.ts"; import { Socket, TCPSocketListener } from "bun" import { config, log } from "../deps.ts"; import * as fs from "node:fs/promises" type PlayerFunction = (a: Player) => void; interface PluginUpdateTime { plugin: Plugin; lastUpdated: Date; } export class Server { server!: TCPSocketListener; players: Player[] = []; plugins: Map = new Map(); lengthMap: Map = new Map([ [0, 130], [5, 8], [8, 9], [13, 65], ]); maxUsers = config.maxUsers; worlds: World[] = [new World({ x: 64, y: 64, z: 64 }, config.main), new World({ x: 256, y: 64, z: 256 }, "large")]; async start(port: number) { this.server = Bun.listen<{dataBuffer?: Buffer}>({ hostname: process.env.HOST || "127.0.0.1", port: +process.env.PORT!, socket: { data: async (socket, data) => { if(socket.data.dataBuffer) { const newBuffer = Buffer.concat([socket.data.dataBuffer, data]); socket.data.dataBuffer = newBuffer; } else { socket.data.dataBuffer = data; } log.debug("Socket", socket.remoteAddress, "has", socket.data.dataBuffer.length, "data for parsing."); //if(config.debug) await new Promise(r => setTimeout(r, 300)); const parseBuffer = () => { if(!socket.data.dataBuffer) return; if(socket.data.dataBuffer.length == 0) return; const packetId = socket.data.dataBuffer.readUint8(0); const packetLength = this.lengthMap.get(packetId); if(!packetLength) { log.debug("Incorrect packet ID", packetId, "packet length could not be found.") return; }; if(socket.data.dataBuffer.byteLength < packetLength) { log.debug("not enough bytes for packet", packetId) return; }; this.handlePacket(socket.data.dataBuffer.copyWithin(0, 1, packetLength), packetId, socket); log.debug("Parsed packet", packetId, "with packet length", packetLength) //console.log(socket.data.dataBuffer, socket.data.dataBuffer.copyWithin(0, 1, packetLength)) socket.data.dataBuffer = Uint8Array.prototype.slice.call(socket.data.dataBuffer, packetLength+1); parseBuffer(); } parseBuffer(); }, open: (socket) => { socket.data = {} }, close: async (socket) => { await this.removeUser(socket, "Disconnected"); }, drain(socket) {}, error(socket, error) {}, }, }); try { await fs.stat("worlds/"); for await (const dirEntry of await fs.readdir("worlds/", {withFileTypes: true})) { const world = new World({ x: 0, y: 0, z: 0 }, dirEntry.name.replace(".buf", "")); this.worlds.push(world); } } catch { await fs.mkdir("worlds") } if (config.onlineMode) { setInterval(async () => { await fetch( "https://www.classicube.net/heartbeat.jsp" + `?port=${config.port}` + `&max=${this.maxUsers}` + `&name=${config.name}` + "&public=True" + `&software=${config.software}` + `&version=7&salt=${config.hash}` + `&users=${[...new Set(this.players.map((obj) => obj.ip))].length}`, ); }, 10000); } await this.updatePlugins(); log.info(`Listening on port ${config.port}!`); } broadcast(text: string) { log.info(text.replace(/&./gm, "")); text.match(/.{1,64}/g)?.forEach((pic) => { this.players.forEach((e) => { e.message(pic.trim()); }); }); } broadcastPacket(func: PlayerFunction, player: Player) { this.players.forEach((e) => { if (e.world == player.world && e !== player) { func(e); } }); } async updatePlugins() { for await (const file of await fs.readdir("./plugins", {withFileTypes:true})) { if (file.isFile()) { const name = file.name.split(".ts")[0]; if (!this.plugins.has(name)) { this.plugins.set(name, { lastUpdated: (await fs.stat(`./plugins/${file.name}`)).mtime!, plugin: new ((await import(`../plugins/${file.name}`)).default)( this, ), }); } else { const plugin = this.plugins.get(name); if ( (await fs.stat(`./plugins/${file.name}`)).mtime!.getTime() !== plugin?.lastUpdated.getTime() ) { plugin?.plugin.emit("stop"); this.plugins.set(name, { lastUpdated: (await fs.stat(`./plugins/${file.name}`)).mtime!, plugin: new ((await import(`../plugins/${file.name}#` + Math.random())) .default)( this, ), }); } } } } } async removeUser(conn: Socket<{dataBuffer?: Buffer}>, text: string) { const player = this.players.find((e) => e.socket == conn); if (!player) return; this.players = this.players.filter((e) => e != player); try { conn.end(); } catch { // whatever } this.broadcast(`${player.username} has &cleft&f, "${text}"`); await this.worlds.find((e) => e.name == player.world)!.save(); this.broadcastPacket( (e) => PacketDefinitions.despawn(player.id, e), player, ); } async handlePacket( buffer: Uint8Array, packetType: number, connection: Socket<{dataBuffer?: Buffer}>, ) { const packet = new PacketReader(buffer); if (packetType == 0x00) { if (this.players.find((e) => e.socket == connection)) return; packet.readByte(); const username = packet.readString(); const verification = packet.readString(); if (this.players.length >= this.maxUsers) { connection.end() return; } const player = new Player( connection, username, this.worlds[0].getSpawn(), this, ); if (!verification) { player.socket.end(); return true; } const hasher = new Bun.CryptoHasher("md5"); hasher.update(config.hash + player.username); if ( config.onlineMode && verification != config.hash && !this.players.find((e) => e.socket == connection) ) { if ( hasher.digest("hex") !== verification ) { await PacketDefinitions.disconnect( "Refresh your playerlist! Incorrect hash!", player, ); player.socket.end(); return true; } } if (this.players.find((e) => e.username == player.username)) { await PacketDefinitions.disconnect( "Your name is already being used!", player, ); player.socket.end(); return true; } this.players.push(player); await PacketDefinitions.ident("cla66ic", "welcome 2 hell", player); player.toWorld(this.worlds.find((e) => e.name == player.world)!); this.broadcast(`${player.username} has &ajoined`); } else if (packetType == 0x08) { const player = this.players.find((e) => e.socket == connection); if (!player) return; packet.readSByte(); player.position.x = packet.readShort(); player.position.y = packet.readShort(); player.position.z = packet.readShort(); player.rotation.yaw = packet.readByte(); player.rotation.pitch = packet.readByte(); this.broadcastPacket((e) => PacketDefinitions.movement( player, player.id, e, ), player); } else if (packetType == 0x0d) { packet.readByte(); const player = this.players.find((e) => e.socket == connection); if (!player) return; const message = packet.readString(); let playerColor = "[member] &b"; if (config.ops.includes(player.username)) { playerColor = "[operator] &c"; } if (message.startsWith("/")) { const commandMessage = message.substring(1); const args = commandMessage.split(" "); const command = args.shift()!; log.warning(`Command execution "${message}" by ${player.username}.`); this.plugins.forEach((value) => { if (value.plugin.commands.includes(command)) { value.plugin.emit("command", command, player, args); } }); return; } this.broadcast(`${playerColor}${player.username}&f: ${message}`); } else if (packetType == 0x05) { const player = this.players.find((e) => e.socket == connection); if (!player) return; let position = {x:0,y:0,z:0} position.x = packet.readShort(); position.y = packet.readShort(); position.z = packet.readShort(); const mode = packet.readByte(); const block = packet.readByte(); const id = mode ? block : 0; const world = this.worlds.find((e) => e.name == player.world); if (!world) return; let pluginAnswer: boolean[] = []; for await (const [_k, v] of this.plugins) { pluginAnswer = pluginAnswer.concat( await v.plugin.emit("setblock", player, mode, id, position), ); } if (pluginAnswer.some((e) => e == true)) { PacketDefinitions.setBlock(position, world.getBlock(position), player); return; } world.setBlock(position, id); this.broadcastPacket( (e) => PacketDefinitions.setBlock(position, id, e), player, ); } } }