Compare commits
19 commits
Author | SHA1 | Date | |
---|---|---|---|
Soph :3 | 5226d5652a | ||
Soph :3 | 957249ed71 | ||
Soph :3 | 888782c2a9 | ||
Soph :3 | 59d2db1785 | ||
Soph :3 | c72b2c494e | ||
Soph :3 | a83abf598d | ||
Soph :3 | 6715d264e4 | ||
Soph :3 | b5e4cabe4d | ||
Soph :3 | 2c6835e2b3 | ||
Soph :3 | 7159558e5e | ||
Soph :3 | ef586df9a6 | ||
8cd07e8521 | |||
4af20b4df5 | |||
c31b81133f | |||
4d69c4c696 | |||
5999e6b3f4 | |||
5e3306276a | |||
41a19c26a6 | |||
11700fe211 |
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,2 +1,7 @@
|
||||||
.env
|
.env
|
||||||
.vscode
|
.vscode
|
||||||
|
plugins/*
|
||||||
|
!plugins/commands.ts
|
||||||
|
!plugins/world.ts
|
||||||
|
worlds/*
|
||||||
|
node_modules
|
|
@ -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(
|
||||||
|
@ -301,7 +300,7 @@ export class PacketDefinitions {
|
||||||
.writeByte(0x07)
|
.writeByte(0x07)
|
||||||
.writeString(name)
|
.writeString(name)
|
||||||
.writeString(motd)
|
.writeString(motd)
|
||||||
.writeByte(0x00)
|
.writeByte(0x64)
|
||||||
.toPacket(),
|
.toPacket(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
import { Position, Rotation, World } from "./classes.ts";
|
import { Position, Rotation, World } from "./classes.ts";
|
||||||
import { PacketDefinitions, PacketWriter } from "./Packets.ts";
|
import { PacketDefinitions, PacketWriter } from "./Packets.ts";
|
||||||
import { 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;
|
||||||
ip: string;
|
ip: string;
|
||||||
id: number;
|
id: number;
|
||||||
world = "main";
|
world = config.main;
|
||||||
position: Position;
|
position: Position;
|
||||||
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,8 +26,8 @@ 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);
|
||||||
|
|
||||||
// reassigns ID until finds available one
|
// reassigns ID until finds available one
|
||||||
|
@ -37,10 +39,15 @@ export class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeToSocket(ar: Uint8Array) {
|
async writeToSocket(ar: Uint8Array) {
|
||||||
await this.socket.write(ar).catch((e) => {
|
try {
|
||||||
|
this.socket.write(ar)
|
||||||
|
} catch(e) {
|
||||||
log.critical(e);
|
log.critical(e);
|
||||||
this.server.removeUser(this.socket);
|
await this.server.removeUser(
|
||||||
});
|
this.socket,
|
||||||
|
"Write failed" + e.message.split("\n")[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message(text: string, id = 0) {
|
message(text: string, id = 0) {
|
||||||
|
@ -75,7 +82,5 @@ export class Player {
|
||||||
);
|
);
|
||||||
|
|
||||||
this.message("You have been moved.");
|
this.message("You have been moved.");
|
||||||
|
|
||||||
//await world.save(); TODO: this causes way too many issues
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,10 @@ import {
|
||||||
Plugin,
|
Plugin,
|
||||||
World,
|
World,
|
||||||
} from "./classes.ts";
|
} from "./classes.ts";
|
||||||
|
import { Socket, TCPSocketListener } from "bun"
|
||||||
|
|
||||||
import { config, crypto, log, s3, 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();
|
||||||
|
@ -28,38 +30,73 @@ export class Server {
|
||||||
[13, 65],
|
[13, 65],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
maxUsers = 69
|
maxUsers = config.maxUsers;
|
||||||
|
|
||||||
worlds: World[] = [new World({ x: 64, y: 64, z: 64 }, "main")];
|
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) {
|
async start(port: number) {
|
||||||
this.server = Deno.listen({ port: port });
|
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 {
|
try {
|
||||||
await s3.headBucket({
|
await fs.stat("worlds/");
|
||||||
Bucket: "cla66ic",
|
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", ""));
|
||||||
|
|
||||||
log.info("s3 bucket exists!");
|
|
||||||
} catch {
|
|
||||||
log.warning("s3 bucket does not exist.. Creating!");
|
|
||||||
|
|
||||||
await s3.createBucket({
|
|
||||||
Bucket: "cla66ic",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
(await s3.listObjects({
|
|
||||||
Bucket: "cla66ic",
|
|
||||||
})).Contents.forEach((e) => {
|
|
||||||
if (e.Key !== "main.buf") {
|
|
||||||
log.info(`Autoloaded a world from s3! ${e.Key}`);
|
|
||||||
|
|
||||||
const world = new World({ x: 0, y: 0, z: 0 }, e.Key!.split(".buf")[0]);
|
|
||||||
|
|
||||||
this.worlds.push(world);
|
this.worlds.push(world);
|
||||||
}
|
}
|
||||||
});
|
} catch {
|
||||||
|
await fs.mkdir("worlds")
|
||||||
|
}
|
||||||
|
|
||||||
if (config.onlineMode) {
|
if (config.onlineMode) {
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
|
@ -67,11 +104,11 @@ export class Server {
|
||||||
"https://www.classicube.net/heartbeat.jsp" +
|
"https://www.classicube.net/heartbeat.jsp" +
|
||||||
`?port=${config.port}` +
|
`?port=${config.port}` +
|
||||||
`&max=${this.maxUsers}` +
|
`&max=${this.maxUsers}` +
|
||||||
"&name=Cla66ic" +
|
`&name=${config.name}` +
|
||||||
"&public=True" +
|
"&public=True" +
|
||||||
"&software=Cla66ic" +
|
`&software=${config.software}` +
|
||||||
`&version=7&salt=${config.hash}` +
|
`&version=7&salt=${config.hash}` +
|
||||||
`&users=${this.players.length}`,
|
`&users=${[...new Set(this.players.map((obj) => obj.ip))].length}`,
|
||||||
);
|
);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
|
@ -80,9 +117,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) {
|
||||||
|
@ -104,13 +138,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,
|
||||||
),
|
),
|
||||||
|
@ -119,12 +153,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)(
|
||||||
|
@ -136,7 +170,7 @@ export class Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
removeUser(conn: Deno.Conn) {
|
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;
|
||||||
|
@ -144,12 +178,14 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
this.broadcast(`${player.username} has &cleft`);
|
this.broadcast(`${player.username} has &cleft&f, "${text}"`);
|
||||||
|
|
||||||
|
await this.worlds.find((e) => e.name == player.world)!.save();
|
||||||
|
|
||||||
this.broadcastPacket(
|
this.broadcastPacket(
|
||||||
(e) => PacketDefinitions.despawn(player.id, e),
|
(e) => PacketDefinitions.despawn(player.id, e),
|
||||||
|
@ -160,7 +196,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) {
|
||||||
|
@ -170,9 +206,8 @@ export class Server {
|
||||||
|
|
||||||
const verification = packet.readString();
|
const verification = packet.readString();
|
||||||
|
|
||||||
if(this.players.length >= this.maxUsers) {
|
if (this.players.length >= this.maxUsers) {
|
||||||
|
connection.end()
|
||||||
connection.close();
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -184,31 +219,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;
|
||||||
}
|
}
|
||||||
|
@ -219,7 +248,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,9 +258,10 @@ export class Server {
|
||||||
player.toWorld(this.worlds.find((e) => e.name == player.world)!);
|
player.toWorld(this.worlds.find((e) => e.name == player.world)!);
|
||||||
this.broadcast(`${player.username} has &ajoined`);
|
this.broadcast(`${player.username} has &ajoined`);
|
||||||
} else if (packetType == 0x08) {
|
} else if (packetType == 0x08) {
|
||||||
const player = this.players.find((e) => e.socket == connection)!;
|
const player = this.players.find((e) => e.socket == connection);
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
packet.readByte();
|
packet.readSByte();
|
||||||
player.position.x = packet.readShort();
|
player.position.x = packet.readShort();
|
||||||
player.position.y = packet.readShort();
|
player.position.y = packet.readShort();
|
||||||
player.position.z = packet.readShort();
|
player.position.z = packet.readShort();
|
||||||
|
@ -246,7 +276,8 @@ export class Server {
|
||||||
} else if (packetType == 0x0d) {
|
} else if (packetType == 0x0d) {
|
||||||
packet.readByte();
|
packet.readByte();
|
||||||
|
|
||||||
const player = this.players.find((e) => e.socket == connection)!;
|
const player = this.players.find((e) => e.socket == connection);
|
||||||
|
if (!player) return;
|
||||||
const message = packet.readString();
|
const message = packet.readString();
|
||||||
let playerColor = "[member] &b";
|
let playerColor = "[member] &b";
|
||||||
|
|
||||||
|
@ -270,19 +301,21 @@ export class Server {
|
||||||
}
|
}
|
||||||
this.broadcast(`${playerColor}${player.username}&f: ${message}`);
|
this.broadcast(`${playerColor}${player.username}&f: ${message}`);
|
||||||
} else if (packetType == 0x05) {
|
} else if (packetType == 0x05) {
|
||||||
const player = this.players.find((e) => e.socket == connection)!;
|
const player = this.players.find((e) => e.socket == connection);
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
const position = {
|
let position = {x:0,y:0,z:0}
|
||||||
x: packet.readShort(),
|
position.x = packet.readShort();
|
||||||
y: packet.readShort(),
|
position.y = packet.readShort();
|
||||||
z: packet.readShort(),
|
position.z = packet.readShort();
|
||||||
};
|
|
||||||
const mode = packet.readByte();
|
const mode = packet.readByte();
|
||||||
const block = packet.readByte();
|
const block = packet.readByte();
|
||||||
|
|
||||||
const id = mode ? block : 0;
|
const id = mode ? block : 0;
|
||||||
|
|
||||||
const world = this.worlds.find((e) => e.name == player.world)!;
|
const world = this.worlds.find((e) => e.name == player.world);
|
||||||
|
if (!world) return;
|
||||||
|
|
||||||
let pluginAnswer: boolean[] = [];
|
let pluginAnswer: boolean[] = [];
|
||||||
|
|
||||||
|
@ -305,51 +338,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 {
|
|
||||||
this.removeUser(connection); // TODO: add a reason to this
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packetIDReadAttempt) {
|
|
||||||
const packetLength = this.lengthMap.get(packetID[0]);
|
|
||||||
|
|
||||||
if (!packetLength) {
|
|
||||||
log.critical("Unknown Packet: " + packetID[0]);
|
|
||||||
this.removeUser(connection); // TODO: add a reason to this
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rawPacket = new Uint8Array(packetLength);
|
|
||||||
let packetReadAttempt;
|
|
||||||
|
|
||||||
try {
|
|
||||||
packetReadAttempt = await connection.read(rawPacket);
|
|
||||||
} catch {
|
|
||||||
this.removeUser(connection); // TODO: add a reason to this
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let fullRead = packetReadAttempt!;
|
|
||||||
|
|
||||||
while (fullRead < packetLength) {
|
|
||||||
const halfPacket = new Uint8Array(packetLength - fullRead);
|
|
||||||
rawPacket = new Uint8Array([...rawPacket, ...halfPacket]);
|
|
||||||
|
|
||||||
fullRead += (await connection.read(halfPacket))!;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handlePacket(rawPacket, packetID[0], connection);
|
|
||||||
} else {
|
|
||||||
this.removeUser(connection);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import { gzip, ungzip } from "https://cdn.skypack.dev/pako";
|
import { decode, encode } from "cbor-x";
|
||||||
import { s3 } from "../deps.ts";
|
|
||||||
import { Position } from "./classes.ts";
|
import { Position } from "./classes.ts";
|
||||||
|
|
||||||
|
import { unlink, readFile, writeFile} from "node:fs/promises";
|
||||||
|
|
||||||
export class World {
|
export class World {
|
||||||
size: Position;
|
size: Position;
|
||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
private dataView: DataView;
|
private dataView: DataView;
|
||||||
name: string;
|
name: string;
|
||||||
// deno-lint-ignore no-explicit-any
|
// deno-lint-ignore no-explicit-any
|
||||||
optionalJson: any = {};
|
metadata: any = {};
|
||||||
|
|
||||||
constructor(size: Position, name: string) {
|
constructor(size: Position, name: string) {
|
||||||
this.size = size;
|
this.size = size;
|
||||||
|
@ -33,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++) {
|
||||||
|
@ -72,39 +73,37 @@ export class World {
|
||||||
|
|
||||||
async delete() {
|
async delete() {
|
||||||
try {
|
try {
|
||||||
await s3.deleteObject({
|
await unlink(`worlds/${this.name}.buf`)
|
||||||
Bucket: "cla66ic",
|
|
||||||
Key: this.name + ".buf",
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
// doesn't exist, probably..
|
// gang
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async load() {
|
private async load() {
|
||||||
try {
|
try {
|
||||||
const head = await s3.headObject({
|
const ungziped = Bun.gunzipSync(
|
||||||
Bucket: "cla66ic",
|
await readFile(`worlds/${this.name}.buf`)
|
||||||
Key: this.name + ".buf",
|
|
||||||
});
|
|
||||||
|
|
||||||
const ungziped = ungzip(
|
|
||||||
(await s3.getObject({
|
|
||||||
Bucket: "cla66ic",
|
|
||||||
Key: this.name + ".buf",
|
|
||||||
})).Body,
|
|
||||||
);
|
);
|
||||||
if (!(ungziped instanceof Uint8Array)) return;
|
|
||||||
|
|
||||||
|
if (!(ungziped instanceof Uint8Array)) return;
|
||||||
|
const dv = new DataView(ungziped.buffer);
|
||||||
|
|
||||||
|
const cborSize = dv.getUint32(0);
|
||||||
|
this.metadata = decode(new Uint8Array(ungziped.buffer.slice(4, cborSize+4)));
|
||||||
|
|
||||||
this.size = {
|
this.size = {
|
||||||
x: +head.Metadata.x!,
|
x: this.metadata.x!,
|
||||||
y: +head.Metadata.y!,
|
y: this.metadata.y!,
|
||||||
z: +head.Metadata.z!,
|
z: this.metadata.z!,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.data = ungziped;
|
this.data = ungziped.slice(cborSize+4);
|
||||||
this.dataView = new DataView(this.data.buffer);
|
this.dataView = new DataView(this.data.buffer);
|
||||||
this.optionalJson = JSON.parse(head.Metadata.json || "{}");
|
|
||||||
} catch {
|
if(4 + this.size.x * this.size.y * this.size.z == this.data.length) {
|
||||||
|
console.log('[WARNING] Encoding was wrong somewhere!')
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
const layers = Math.floor(this.size.y / 2);
|
const layers = Math.floor(this.size.y / 2);
|
||||||
|
|
||||||
for (let i = 0; i < layers; i += 1) {
|
for (let i = 0; i < layers; i += 1) {
|
||||||
|
@ -118,16 +117,18 @@ export class World {
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
await s3.putObject({
|
const metadata = {
|
||||||
Bucket: "cla66ic",
|
x: this.size.x!,
|
||||||
Key: this.name + ".buf",
|
y: this.size.y!,
|
||||||
Body: gzip(this.data),
|
z: this.size.z!,
|
||||||
Metadata: {
|
...this.metadata
|
||||||
"x": this.size.x + "",
|
}
|
||||||
"y": this.size.y + "",
|
const cborData = encode(metadata);
|
||||||
"z": this.size.z + "",
|
const buffer = new Uint8Array(4 + cborData.byteLength + this.data.byteLength);
|
||||||
"json": JSON.stringify(this.optionalJson),
|
const dv = new DataView(buffer.buffer);
|
||||||
},
|
dv.setUint32(0, cborData.byteLength);
|
||||||
});
|
buffer.set(cborData, 4);
|
||||||
|
buffer.set(this.data, 4 + cborData.byteLength);
|
||||||
|
await writeFile(`worlds/${this.name}.buf`, Bun.gzipSync(buffer)!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ export abstract class Plugin extends EventEmitter<{
|
||||||
mode: number,
|
mode: number,
|
||||||
id: number,
|
id: number,
|
||||||
position: Position,
|
position: Position,
|
||||||
): boolean;
|
): Promise<boolean> | boolean;
|
||||||
|
|
||||||
stop(): void;
|
stop(): void;
|
||||||
}> {
|
}> {
|
||||||
|
|
46
deps.ts
46
deps.ts
|
@ -1,27 +1,29 @@
|
||||||
import "https://deno.land/x/dotenv@v3.2.0/load.ts";
|
|
||||||
|
|
||||||
import { ApiFactory } from "https://deno.land/x/aws_api@v0.7.0/client/mod.ts";
|
|
||||||
import { S3 } from "https://aws-api.deno.dev/v0.3/services/s3.ts";
|
|
||||||
|
|
||||||
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 = {
|
||||||
export const s3 = new ApiFactory({
|
info: (...a) => {
|
||||||
credentials: {
|
console.log("[INFO]", ...a);
|
||||||
awsAccessKeyId: Deno.env.get("S3_ACCESS_KEY_ID")!,
|
|
||||||
awsSecretKey: Deno.env.get("S3_SECRET_KEY")!,
|
|
||||||
},
|
},
|
||||||
fixedEndpoint: "https://s3.us-west-004.backblazeb2.com",
|
warning: (...a) => {
|
||||||
region: "us-west-004",
|
console.warn("[WARNING]", ...a);
|
||||||
}).makeNew(S3);
|
},
|
||||||
|
critical: (...a) => {
|
||||||
|
console.error("[ERROR]", ...a);
|
||||||
|
},
|
||||||
|
debug: (...a) => {
|
||||||
|
if(config.debug) {
|
||||||
|
console.log("[DEBUG]", ...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: process.env.MAIN || "main",
|
||||||
|
maxUsers: +(process.env.USERS || 24) > 255 ? 255 : +(process.env.USERS || 24),
|
||||||
|
software: process.env.SOFTWARE || "Custom Cla66ic",
|
||||||
|
name: process.env.NAME || "Cla66ic Server",
|
||||||
|
debug: process.env.DEBUG == "true",
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
12
package.json
Normal file
12
package.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "cla66ic",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cbor-x": "^1.5.9",
|
||||||
|
"@types/bun": "latest"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { Plugin } from "../classes/classes.ts";
|
import { PacketWriter, Player, Plugin } from "../classes/classes.ts";
|
||||||
import { Server } from "../classes/Server.ts";
|
import { Server } from "../classes/Server.ts";
|
||||||
import { config } from "../deps.ts";
|
import { config } from "../deps.ts";
|
||||||
|
|
||||||
|
@ -7,14 +7,36 @@ export default class CommandPlugin extends Plugin {
|
||||||
"help",
|
"help",
|
||||||
"reloadplugins",
|
"reloadplugins",
|
||||||
"clients",
|
"clients",
|
||||||
|
"tp",
|
||||||
|
"eval"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
async tp(from: Player, to: Player) {
|
||||||
|
if(to.world != from.world) {
|
||||||
|
from.toWorld(this.server.worlds.find((e) =>
|
||||||
|
e.name == to.world
|
||||||
|
)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
await from.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x08)
|
||||||
|
.writeSByte(255)
|
||||||
|
.writeShort(to.position.x)
|
||||||
|
.writeShort(to.position.y)
|
||||||
|
.writeShort(to.position.z)
|
||||||
|
.writeByte(to.rotation.yaw)
|
||||||
|
.writeByte(to.rotation.pitch)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(server: Server) {
|
constructor(server: Server) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
|
||||||
this.on("command", async (command, player) => {
|
this.on("command", async (command, player, args) => {
|
||||||
if (command == "help") {
|
if (command == "help") {
|
||||||
let allComamnds = "";
|
let allComamnds = "";
|
||||||
for (const [_k, v] of server.plugins) {
|
for (const [_k, v] of server.plugins) {
|
||||||
|
@ -38,7 +60,42 @@ export default class CommandPlugin extends Plugin {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
} else if(command == "eval") {
|
||||||
});
|
if(config.ops.includes(player.username)) {
|
||||||
|
|
||||||
|
server.broadcast(eval(args.join(" ")));
|
||||||
|
}
|
||||||
|
} else if (command == "tp") {
|
||||||
|
if(args.length == 1) {
|
||||||
|
const teleportTo = this.server.players.find((e) => args[0] === e.username)
|
||||||
|
|
||||||
|
if(teleportTo) {
|
||||||
|
await this.tp(player, teleportTo);
|
||||||
|
} else {
|
||||||
|
player.message("Player is missing")
|
||||||
|
}
|
||||||
|
} else if(args.length == 3) {
|
||||||
|
const x = +args[0]
|
||||||
|
const y = +args[1]
|
||||||
|
const z = +args[2]
|
||||||
|
|
||||||
|
if(isNaN(x) || isNaN(y) || isNaN(z)) {
|
||||||
|
player.message("invalid coords")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x08)
|
||||||
|
.writeSByte(255)
|
||||||
|
.writeShort(x * 32)
|
||||||
|
.writeShort(y * 32)
|
||||||
|
.writeShort(z * 32)
|
||||||
|
.writeByte(player.rotation.yaw)
|
||||||
|
.writeByte(player.rotation.pitch)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ export default class CommandPlugin extends Plugin {
|
||||||
"g",
|
"g",
|
||||||
"worlds",
|
"worlds",
|
||||||
"world",
|
"world",
|
||||||
|
"main",
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(server: Server) {
|
constructor(server: Server) {
|
||||||
|
@ -15,8 +16,8 @@ export default class CommandPlugin extends Plugin {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.on("setblock", (player, _mode, _id) => {
|
this.on("setblock", (player, _mode, _id) => {
|
||||||
const world = server.worlds.find((e) => e.name == player.world)!;
|
const world = server.worlds.find((e) => e.name == player.world)!;
|
||||||
if (!world.optionalJson?.builders?.includes("*")) {
|
if (!world.metadata?.builders?.includes("*")) {
|
||||||
if (!world.optionalJson?.builders?.includes(player.username)) {
|
if (!world.metadata?.builders?.includes(player.username)) {
|
||||||
player.message("You are %cnot allowed &fto build in this world!");
|
player.message("You are %cnot allowed &fto build in this world!");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -25,10 +26,19 @@ export default class CommandPlugin extends Plugin {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
this.on("command", async (command, player, args) => {
|
this.on("command", async (command, player, args) => {
|
||||||
if (command == "g") {
|
if (command == "main") {
|
||||||
|
await server.worlds.find((e) => e.name == player.world)!.save();
|
||||||
|
|
||||||
|
player.toWorld(
|
||||||
|
server.worlds.find((e) => e.name.toLowerCase() == config.main)!,
|
||||||
|
);
|
||||||
|
} else if (command == "g") {
|
||||||
|
await server.worlds.find((e) => e.name == player.world)!.save();
|
||||||
|
|
||||||
const requestedWorld = server.worlds.find((e) =>
|
const requestedWorld = server.worlds.find((e) =>
|
||||||
e.name.toLowerCase() == args.join(" ").toLowerCase()
|
e.name.toLowerCase() == args.join(" ").toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (requestedWorld) {
|
if (requestedWorld) {
|
||||||
player.toWorld(requestedWorld);
|
player.toWorld(requestedWorld);
|
||||||
} else {
|
} else {
|
||||||
|
@ -49,8 +59,8 @@ export default class CommandPlugin extends Plugin {
|
||||||
{ x: 64, y: 64, z: 64 },
|
{ x: 64, y: 64, z: 64 },
|
||||||
player.username,
|
player.username,
|
||||||
);
|
);
|
||||||
world.optionalJson.builders = [];
|
world.metadata.builders = [];
|
||||||
world.optionalJson.builders.push(player.username);
|
world.metadata.builders.push(player.username);
|
||||||
server.worlds.push(world);
|
server.worlds.push(world);
|
||||||
|
|
||||||
player.message(`&aWorld created!&f Use /g ${player.username}!`);
|
player.message(`&aWorld created!&f Use /g ${player.username}!`);
|
||||||
|
@ -95,11 +105,11 @@ export default class CommandPlugin extends Plugin {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!world.optionalJson?.builders) world.optionalJson.builders = [];
|
if (!world.metadata?.builders) world.metadata.builders = [];
|
||||||
|
|
||||||
if (subcategory == "add") {
|
if (subcategory == "add") {
|
||||||
const username = args[2];
|
const username = args[2];
|
||||||
world.optionalJson.builders.push(username);
|
world.metadata.builders.push(username);
|
||||||
player.message(
|
player.message(
|
||||||
`&a${username}&f sucesfully added as a builder to world &a${world.name}!`,
|
`&a${username}&f sucesfully added as a builder to world &a${world.name}!`,
|
||||||
);
|
);
|
||||||
|
@ -107,13 +117,13 @@ export default class CommandPlugin extends Plugin {
|
||||||
} else if (subcategory == "remove") {
|
} else if (subcategory == "remove") {
|
||||||
const username = args[2];
|
const username = args[2];
|
||||||
|
|
||||||
const before = world.optionalJson.builders.length;
|
const before = world.metadata.builders.length;
|
||||||
|
|
||||||
world.optionalJson.builders = world.optionalJson.builders.filter((
|
world.metadata.builders = world.metadata.builders.filter((
|
||||||
e: string,
|
e: string,
|
||||||
) => e !== username);
|
) => e !== username);
|
||||||
|
|
||||||
const after = world.optionalJson.builders.length;
|
const after = world.metadata.builders.length;
|
||||||
|
|
||||||
player.message(
|
player.message(
|
||||||
`Removed &a${
|
`Removed &a${
|
||||||
|
@ -124,7 +134,7 @@ export default class CommandPlugin extends Plugin {
|
||||||
} else if (subcategory == "list") {
|
} else if (subcategory == "list") {
|
||||||
player.message(
|
player.message(
|
||||||
`&a${world.name}&f's builders: &a${
|
`&a${world.name}&f's builders: &a${
|
||||||
world.optionalJson.builders.join(", ")
|
world.metadata.builders.join(", ")
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
26
readme.md
26
readme.md
|
@ -8,28 +8,27 @@
|
||||||
2. entierly cloud based (meaning you can host it anywhere)
|
2. entierly cloud based (meaning you can host it anywhere)
|
||||||
3. extremely extensive plugin system
|
3. extremely extensive plugin system
|
||||||
4. solid implementation of sockets in deno and how to patch them together
|
4. solid implementation of sockets in deno and how to patch them together
|
||||||
5. DENO!! It's not node, and a classic server.
|
5. Bun!! It's not node, and a classic server
|
||||||
|
6. very fast
|
||||||
|
|
||||||
### setup tutorial (be warned it's not the easiest)
|
### setup tutorial
|
||||||
|
|
||||||
1. make a backblaze b2 account, make a bucket, and get your keys from the bucket
|
1. configure .env file to look something like
|
||||||
2. configure .env file to look something like
|
|
||||||
|
|
||||||
```
|
```
|
||||||
PORT=6969
|
PORT=6969
|
||||||
HASH=RandomHashIlIke
|
HASH=RandomHashIlIke
|
||||||
OPS=["Me"]
|
OPS=["Me"]
|
||||||
ONLINEMODE=true
|
ONLINEMODE=true
|
||||||
|
MAIN=main
|
||||||
S3_ACCESS_KEY_ID="MyAccessKey"
|
HOST=0.0.0.0
|
||||||
S3_SECRET_KEY="SecretKey"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE: if you are running inside of a cloud provider, just set these as your
|
NOTE: if you are running inside of a cloud provider, just set these as your
|
||||||
environment variables
|
environment variables
|
||||||
|
|
||||||
3. install deno
|
2. install bun
|
||||||
4. run `deno run --allow-env --allow-net --allow-read index.ts`
|
3. run `bun index.ts`
|
||||||
|
|
||||||
### insipration taken from:
|
### insipration taken from:
|
||||||
|
|
||||||
|
@ -39,12 +38,13 @@ environment variables
|
||||||
|
|
||||||
### issues:
|
### issues:
|
||||||
|
|
||||||
|
1. ~~Properly queue up map saves instead of just blantantly saving whenever
|
||||||
1. Properly queue up map saves instead of just blantantly saving whenever possible
|
possible~~ it now saves to disk, IO is very fast and shouldn't cause issues anymore
|
||||||
2. massive performance issues, running more than 100 something accounts makes the server instead insane amounts of cpu (most likely multithreading needed)
|
2. ~~massive performance issues, running more than 100 something accounts makes
|
||||||
|
the server instead insane amounts of cpu (most likely multithreading needed)~~ the server is now async so it's way quicker (untested)
|
||||||
3. no cpe support! i want to get all of the above issues fixed before
|
3. no cpe support! i want to get all of the above issues fixed before
|
||||||
implementing CPE support
|
implementing CPE support
|
||||||
4. no IP cooldown connections (no block cooldown either), no anticheat, no unique IP heartbeats
|
4. no IP cooldown connections (no block cooldown either), no anticheat
|
||||||
5. proper rank support (implemented as plugin)
|
5. proper rank support (implemented as plugin)
|
||||||
6. no discord bridge (implemented as plugin)
|
6. no discord bridge (implemented as plugin)
|
||||||
7. no cla66ic/plugins repository
|
7. no cla66ic/plugins repository
|
||||||
|
|
Loading…
Reference in a new issue