switch to Bun

This commit is contained in:
Soph :3 2024-04-28 18:33:00 +03:00
parent ef586df9a6
commit 7159558e5e
Signed by: sophie
GPG key ID: EDA5D222A0C270F2
13 changed files with 146 additions and 159 deletions

1
.gitignore vendored
View file

@ -4,3 +4,4 @@ plugins/*
!plugins/commands.ts !plugins/commands.ts
!plugins/world.ts !plugins/world.ts
worlds/* worlds/*
node_modules

BIN
bun.lockb Executable file

Binary file not shown.

View file

@ -108,7 +108,6 @@ export class PacketWriter {
} }
} }
import { gzip } from "https://cdn.skypack.dev/pako";
import { Position, World } from "./classes.ts"; import { Position, World } from "./classes.ts";
import { Player } from "./Player.ts"; import { Player } from "./Player.ts";
@ -137,7 +136,7 @@ export class PacketDefinitions {
player.position = world.getSpawn(); player.position = world.getSpawn();
const compressedMap = gzip(world.data)!; const compressedMap = Bun.gzipSync(world.data)!;
for (let i = 0; i < compressedMap.length; i += 1024) { for (let i = 0; i < compressedMap.length; i += 1024) {
const chunk = compressedMap.slice( const chunk = compressedMap.slice(

View file

@ -3,8 +3,10 @@ import { PacketDefinitions, PacketWriter } from "./Packets.ts";
import { config, log } from "../deps.ts"; import { config, log } from "../deps.ts";
import { Server } from "./Server.ts"; import { Server } from "./Server.ts";
import {Socket} from 'bun';
export class Player { export class Player {
socket: Deno.Conn; socket: Socket<{dataBuffer?: Buffer}>;
private server: Server; private server: Server;
username: string; username: string;
@ -15,7 +17,7 @@ export class Player {
rotation: Rotation = { yaw: 0, pitch: 0 }; rotation: Rotation = { yaw: 0, pitch: 0 };
constructor( constructor(
socket: Deno.Conn, socket: Socket<{dataBuffer?: Buffer}>,
username: string, username: string,
position: Position, position: Position,
server: Server, server: Server,
@ -24,7 +26,7 @@ export class Player {
this.username = username; this.username = username;
this.position = position; this.position = position;
this.server = server; this.server = server;
this.ip = (this.socket.remoteAddr as Deno.NetAddr).hostname; this.ip = this.socket.remoteAddress;
let id = Math.floor(Math.random() * server.maxUsers); let id = Math.floor(Math.random() * server.maxUsers);
@ -37,13 +39,15 @@ export class Player {
} }
async writeToSocket(ar: Uint8Array) { async writeToSocket(ar: Uint8Array) {
await this.socket.write(ar).catch(async (e) => { try {
this.socket.write(ar)
} catch(e) {
log.critical(e); log.critical(e);
await this.server.removeUser( await this.server.removeUser(
this.socket, this.socket,
"Write failed" + e.message.split("\n")[0], "Write failed" + e.message.split("\n")[0],
); );
}); }
} }
message(text: string, id = 0) { message(text: string, id = 0) {

View file

@ -5,8 +5,10 @@ import {
Plugin, Plugin,
World, World,
} from "./classes.ts"; } from "./classes.ts";
import { Socket, TCPSocketListener } from "bun"
import { config, crypto, log, toHexString } from "../deps.ts"; import { config, log } from "../deps.ts";
import * as fs from "node:fs/promises"
type PlayerFunction = (a: Player) => void; type PlayerFunction = (a: Player) => void;
@ -16,7 +18,7 @@ interface PluginUpdateTime {
} }
export class Server { export class Server {
server!: Deno.Listener; server!: TCPSocketListener;
players: Player[] = []; players: Player[] = [];
plugins: Map<string, PluginUpdateTime> = new Map(); plugins: Map<string, PluginUpdateTime> = new Map();
@ -33,17 +35,54 @@ export class Server {
worlds: World[] = [new World({ x: 64, y: 64, z: 64 }, config.main)]; worlds: World[] = [new World({ x: 64, y: 64, z: 64 }, config.main)];
async start(port: number) { async start(port: number) {
this.server = Deno.listen({ port: port }); this.server = Bun.listen<{dataBuffer?: Buffer}>({
hostname: "localhost",
port: +process.env.PORT!,
socket: {
data: (socket, data) => {
if(socket.data.dataBuffer) {
const newBuffer = Buffer.concat([socket.data.dataBuffer, data]);
socket.data.dataBuffer = newBuffer;
} else {
socket.data.dataBuffer = data;
}
const parseBuffer = () => {
if(!socket.data.dataBuffer) return;
const packetId = socket.data.dataBuffer.readUint8(0);
const packetLength = this.lengthMap.get(packetId);
if(!packetLength) {
return;
};
if(socket.data.dataBuffer.byteLength < packetLength) return;
this.handlePacket(socket.data.dataBuffer.copyWithin(0, 1), packetId, socket);
socket.data.dataBuffer = socket.data.dataBuffer.subarray(packetLength+1);
parseBuffer();
}
parseBuffer();
},
open: (socket) => {
socket.data = {}
},
close(socket) {},
drain(socket) {},
error(socket, error) {},
},
});
try { try {
await Deno.stat("worlds/"); await fs.stat("worlds/");
for await (const dirEntry of Deno.readDir("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", "")); const world = new World({ x: 0, y: 0, z: 0 }, dirEntry.name.replace(".buf", ""));
this.worlds.push(world); this.worlds.push(world);
} }
} catch { } catch {
await Deno.mkdir("worlds") await fs.mkdir("worlds")
} }
if (config.onlineMode) { if (config.onlineMode) {
@ -65,9 +104,6 @@ export class Server {
log.info(`Listening on port ${config.port}!`); log.info(`Listening on port ${config.port}!`);
for await (const socket of this.server) {
this.startSocket(socket);
}
} }
broadcast(text: string) { broadcast(text: string) {
@ -89,13 +125,13 @@ export class Server {
} }
async updatePlugins() { async updatePlugins() {
for await (const file of Deno.readDir("./plugins")) { for await (const file of await fs.readdir("./plugins", {withFileTypes:true})) {
if (file.isFile) { if (file.isFile()) {
const name = file.name.split(".ts")[0]; const name = file.name.split(".ts")[0];
if (!this.plugins.has(name)) { if (!this.plugins.has(name)) {
this.plugins.set(name, { this.plugins.set(name, {
lastUpdated: Deno.statSync(`./plugins/${file.name}`).mtime!, lastUpdated: (await fs.stat(`./plugins/${file.name}`)).mtime!,
plugin: new ((await import(`../plugins/${file.name}`)).default)( plugin: new ((await import(`../plugins/${file.name}`)).default)(
this, this,
), ),
@ -104,12 +140,12 @@ export class Server {
const plugin = this.plugins.get(name); const plugin = this.plugins.get(name);
if ( if (
Deno.statSync(`./plugins/${file.name}`).mtime!.getTime() !== (await fs.stat(`./plugins/${file.name}`)).mtime!.getTime() !==
plugin?.lastUpdated.getTime() plugin?.lastUpdated.getTime()
) { ) {
plugin?.plugin.emit("stop"); plugin?.plugin.emit("stop");
this.plugins.set(name, { this.plugins.set(name, {
lastUpdated: Deno.statSync(`./plugins/${file.name}`).mtime!, lastUpdated: (await fs.stat(`./plugins/${file.name}`)).mtime!,
plugin: plugin:
new ((await import(`../plugins/${file.name}#` + Math.random())) new ((await import(`../plugins/${file.name}#` + Math.random()))
.default)( .default)(
@ -121,7 +157,7 @@ export class Server {
} }
} }
} }
async removeUser(conn: Deno.Conn, text: string) { async removeUser(conn: Socket<{dataBuffer?: Buffer}>, text: string) {
const player = this.players.find((e) => e.socket == conn); const player = this.players.find((e) => e.socket == conn);
if (!player) return; if (!player) return;
@ -129,7 +165,7 @@ export class Server {
this.players = this.players.filter((e) => e != player); this.players = this.players.filter((e) => e != player);
try { try {
conn.close(); conn.end();
} catch { } catch {
// whatever // whatever
} }
@ -147,7 +183,7 @@ export class Server {
async handlePacket( async handlePacket(
buffer: Uint8Array, buffer: Uint8Array,
packetType: number, packetType: number,
connection: Deno.Conn, connection: Socket<{dataBuffer?: Buffer}>,
) { ) {
const packet = new PacketReader(buffer); const packet = new PacketReader(buffer);
if (packetType == 0x00) { if (packetType == 0x00) {
@ -158,7 +194,7 @@ export class Server {
const verification = packet.readString(); const verification = packet.readString();
if (this.players.length >= this.maxUsers) { if (this.players.length >= this.maxUsers) {
connection.close(); connection.end()
return; return;
} }
@ -170,31 +206,25 @@ export class Server {
); );
if (!verification) { if (!verification) {
player.socket.close(); player.socket.end();
return true; return true;
} }
const hasher = new Bun.CryptoHasher("md5");
hasher.update(config.hash + player.username);
const str = toHexString(
new Uint8Array(
await crypto.subtle.digest(
"MD5",
new TextEncoder().encode(config.hash + player.username),
),
),
);
if ( if (
config.onlineMode && verification != config.hash && config.onlineMode && verification != config.hash &&
!this.players.find((e) => e.socket == connection) !this.players.find((e) => e.socket == connection)
) { ) {
if ( if (
str !== verification hasher.digest("hex") !== verification
) { ) {
await PacketDefinitions.disconnect( await PacketDefinitions.disconnect(
"Refresh your playerlist! Incorrect hash!", "Refresh your playerlist! Incorrect hash!",
player, player,
); );
player.socket.close(); player.socket.end();
return true; return true;
} }
@ -205,7 +235,7 @@ export class Server {
"Your name is already being used!", "Your name is already being used!",
player, player,
); );
player.socket.close(); player.socket.end();
return true; return true;
} }
@ -295,63 +325,4 @@ export class Server {
); );
} }
} }
async startSocket(connection: Deno.Conn) {
while (true) {
const packetID = new Uint8Array(1);
let packetIDReadAttempt;
try {
packetIDReadAttempt = await connection.read(packetID);
} catch {
await this.removeUser(connection, "Packet ID read failed");
break;
}
if (packetIDReadAttempt) {
const packetLength = this.lengthMap.get(packetID[0]);
if (!packetLength) {
log.critical("Unknown Packet: " + packetID[0]);
await this.removeUser(connection, "Unknown packet ID " + packetID[0]); // TODO: add a reason to this
break;
}
let rawPacket = new Uint8Array(packetLength);
let packetReadAttempt;
try {
packetReadAttempt = await connection.read(rawPacket);
} catch {
await this.removeUser(connection, "Packet read attempt failed."); // TODO: add a reason to this
break;
}
let fullRead = packetReadAttempt!;
while (fullRead < packetLength) {
const halfPacket = new Uint8Array(packetLength - fullRead);
rawPacket = new Uint8Array([...rawPacket, ...halfPacket]);
try {
fullRead += (await connection.read(halfPacket))!;
} catch {
await this.removeUser(
connection,
"Couldn't read all of packet " + packetID[0],
);
break;
}
}
this.handlePacket(rawPacket, packetID[0], connection);
} else {
await this.removeUser(
connection,
"Packet ID read returned null. Packet " + packetID[0],
);
break;
}
}
}
} }

View file

@ -1,6 +1,8 @@
import { gzip, ungzip } from "https://cdn.skypack.dev/pako"; import { decode, encode } from "cbor-x";
import { Position } from "./classes.ts"; import { Position } from "./classes.ts";
import { cbor } from "../deps.ts";
import { unlink, readFile, writeFile} from "node:fs/promises";
export class World { export class World {
size: Position; size: Position;
data: Uint8Array; data: Uint8Array;
@ -32,7 +34,7 @@ export class World {
} }
findID(block: number): Position[] { findID(block: number): Position[] {
const position = []; const position: Position[] = [];
for (let z = 0; z < this.size.z; z++) { for (let z = 0; z < this.size.z; z++) {
for (let y = 0; y < this.size.y; y++) { for (let y = 0; y < this.size.y; y++) {
for (let x = 0; x < this.size.x; x++) { for (let x = 0; x < this.size.x; x++) {
@ -71,7 +73,7 @@ export class World {
async delete() { async delete() {
try { try {
await Deno.remove(`worlds/${this.name}.buf`) await unlink(`worlds/${this.name}.buf`)
} catch { } catch {
// gang // gang
} }
@ -79,15 +81,15 @@ export class World {
private async load() { private async load() {
try { try {
const ungziped = ungzip( const ungziped = Bun.gunzipSync(
await Deno.readFile(`worlds/${this.name}.buf`) await readFile(`worlds/${this.name}.buf`)
); );
if (!(ungziped instanceof Uint8Array)) return; if (!(ungziped instanceof Uint8Array)) return;
const dv = new DataView(ungziped.buffer); const dv = new DataView(ungziped.buffer);
const cborSize = dv.getUint32(0); const cborSize = dv.getUint32(0);
this.metadata = cbor.decode(new Uint8Array(ungziped.buffer.slice(4, cborSize+4))); this.metadata = decode(new Uint8Array(ungziped.buffer.slice(4, cborSize+4)));
this.size = { this.size = {
x: this.metadata.x!, x: this.metadata.x!,
@ -117,12 +119,12 @@ export class World {
z: this.size.z!, z: this.size.z!,
...this.metadata ...this.metadata
} }
const cborData = cbor.encode(metadata); const cborData = encode(metadata);
const buffer = new Uint8Array(4 + cborData.byteLength + this.data.byteLength); const buffer = new Uint8Array(4 + cborData.byteLength + this.data.byteLength);
const dv = new DataView(buffer.buffer); const dv = new DataView(buffer.buffer);
dv.setUint32(0, cborData.byteLength); dv.setUint32(0, cborData.byteLength);
buffer.set(cborData, 4); buffer.set(cborData, 4);
buffer.set(this.data, 4 + cborData.byteLength); buffer.set(this.data, 4 + cborData.byteLength);
await Deno.writeFile(`worlds/${this.name}.buf`, gzip(buffer)!); await writeFile(`worlds/${this.name}.buf`, Bun.gzipSync(buffer)!);
} }
} }

View file

@ -1,4 +1,4 @@
import { EventEmitter } from "../deps.ts"; import EventEmitter from "./../events";
import { Player } from "./Player.ts"; import { Player } from "./Player.ts";
import { Server } from "./Server.ts"; import { Server } from "./Server.ts";

View file

@ -1,5 +0,0 @@
{
"tasks": {
"dev": "deno run --watch main.ts"
}
}

View file

@ -1,30 +0,0 @@
{
"version": "3",
"remote": {
"https://cdn.skypack.dev/-/pako@v2.1.0-45rMaYN3B7tIGOhONLdV/dist=es2019,mode=imports/optimized/pako.js": "0e70f8f81aaa277e359308e2f07d560fd6c9f98e2bb8968301f237bf7f34c1a8",
"https://cdn.skypack.dev/pako": "cc52bc53fdee35b4854461706ea3655b8bda1a3b48ed170f16d0050f6b8534d5",
"https://deno.land/std@0.136.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
"https://deno.land/std@0.136.0/_wasm_crypto/crypto.mjs": "3b383eb715e8bfe61b4450ef0644b2653429c88d494807c86c5235979f62e56b",
"https://deno.land/std@0.136.0/_wasm_crypto/crypto.wasm.mjs": "0ad9ecc0d03ca8a083d9109db22e7507f019f63cf55b82ea618ab58855617577",
"https://deno.land/std@0.136.0/_wasm_crypto/mod.ts": "30a93c8b6b6c5b269e96a3e95d2c045d86a496814a8737443b77cad941d6a0b5",
"https://deno.land/std@0.136.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d",
"https://deno.land/std@0.136.0/bytes/equals.ts": "a60ef9f01fb6e06a4e0343fc44d53d39d12dd66bc3f09ac8e6eb9cc1a6335e48",
"https://deno.land/std@0.136.0/bytes/mod.ts": "4cef6fe8f0de217b9babbcbb0a566402b667f18a8e6d094a45e5fb3fc1afff70",
"https://deno.land/std@0.136.0/crypto/mod.ts": "a10fee951ddf2c91ae5938ba2a1d123580f773e533008988ba6089ddd2b65d67",
"https://deno.land/std@0.136.0/encoding/base64.ts": "c8c16b4adaa60d7a8eee047c73ece26844435e8f7f1328d74593dbb2dd58ea4f",
"https://deno.land/std@0.136.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37",
"https://deno.land/std@0.136.0/fs/exists.ts": "cb734d872f8554ea40b8bff77ad33d4143c1187eac621a55bf37781a43c56f6d",
"https://deno.land/std@0.136.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b",
"https://deno.land/std@0.136.0/log/handlers.ts": "b88c24df61eaeee8581dbef3622f21aebfd061cd2fda49affc1711c0e54d57da",
"https://deno.land/std@0.136.0/log/levels.ts": "82c965b90f763b5313e7595d4ba78d5095a13646d18430ebaf547526131604d1",
"https://deno.land/std@0.136.0/log/logger.ts": "30770bf29053d1c8d1054ec52ab385da8e5b8a43973da32c84dea819e5a11b52",
"https://deno.land/std@0.136.0/log/mod.ts": "e200a8600a9e9ac2b10668fda0e6537a1444c5050ea6a04c826df52519bf35fd",
"https://deno.land/x/cbor@v1.5.9/decode.js": "631f504e9e811609cb388a7f226d2054fd4f432eb188423e46be10443746b305",
"https://deno.land/x/cbor@v1.5.9/encode.js": "c28da2b6291e271e1f55bc3add9119fa23ddee5274940c09ee3c4c67f7ba45d3",
"https://deno.land/x/cbor@v1.5.9/index.js": "cc8678819d77aa34b6fa9293658d85d5e53e53eaf555f85b0f98a8a18dbfaa12",
"https://deno.land/x/cbor@v1.5.9/iterators.js": "744e0469fe37c33bab3787608ced2f2cda014cb9352b3adbd949a2701f043aea",
"https://deno.land/x/dotenv@v3.2.0/load.ts": "cbd76a0aee01aad8d09222afaa1dd04b84d9d3e44637503b01bf77a91df9e041",
"https://deno.land/x/dotenv@v3.2.0/mod.ts": "077b48773de9205266a0b44c3c3a3c3083449ed64bb0b6cc461b95720678d38e",
"https://deno.land/x/dotenv@v3.2.0/util.ts": "693730877b13f8ead2b79b2aa31e2a0652862f7dc0c5f6d2f313f4d39c7b7670"
}
}

35
deps.ts
View file

@ -1,20 +1,23 @@
import "https://deno.land/x/dotenv@v3.2.0/load.ts";
export * as cbor from "https://deno.land/x/cbor@v1.5.9/index.js"
export * as log from "https://deno.land/std@0.136.0/log/mod.ts";
export { crypto } from "https://deno.land/std@0.136.0/crypto/mod.ts";
export { EventEmitter } from "./events.ts";
import "https://deno.land/x/dotenv@v3.2.0/load.ts";
export const toHexString = (bytes: Uint8Array) => export const toHexString = (bytes: Uint8Array) =>
bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
export const log = {
info: (...a) => {
console.log("[INFO]", ...a);
},
warning: (...a) => {
console.warn("[WARNING]", ...a);
},
critical: (...a) => {
console.error("[ERROR]", ...a);
}
}
export const config = { export const config = {
ops: Deno.env.get("OPS") ? JSON.parse(Deno.env.get("OPS")!) : [], ops: process.env.OPS ? JSON.parse(process.env.OPS!) : [],
port: +Deno.env.get("PORT")!, port: +process.env.PORT!,
hash: Deno.env.get("HASH"), hash: process.env.HASH,
onlineMode: Deno.env.get("ONLINEMODE") == "true", onlineMode: process.env.ONLINEMODE == "true",
main: Deno.env.get("MAIN") || "main", main: process.env.MAIN || "main",
maxUsers: +(Deno.env.get("USERS") || 24) > 255 ? 255 : +(Deno.env.get("USERS") || 24), maxUsers: +(process.env.USERS || 24) > 255 ? 255 : +(process.env.USERS || 24),
software: Deno.env.get("SOFTWARE") || "Custom Cla66ic", software: process.env.SOFTWARE || "Custom Cla66ic",
name: Deno.env.get("NAME") || "Cla66ic Server" name: process.env.NAME || "Cla66ic Server"
}; };

View file

@ -208,7 +208,7 @@ export class EventEmitter<E extends EventsType = {}> {
timeout?: number, timeout?: number,
): Promise<Parameters<Callback>> { ): Promise<Parameters<Callback>> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let timeoutId: number | null; let timeoutId: Timer | null;
let listener = (...args: any[]) => { let listener = (...args: any[]) => {
if (timeoutId !== null) clearTimeout(timeoutId); if (timeoutId !== null) clearTimeout(timeoutId);

15
package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "cla66ic",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"cbor-x": "^1.5.9",
"netlinkwrapper": "^2.0.2"
}
}

27
tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}