cla66ic/classes/Server.ts

342 lines
9.6 KiB
TypeScript

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<string, PluginUpdateTime> = new Map();
lengthMap: Map<number, number> = 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,
);
}
}
}