fart
This commit is contained in:
parent
b73469acbe
commit
b8ca8c13d1
11 changed files with 1126 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
.vscode
|
309
classes/Packets.ts
Normal file
309
classes/Packets.ts
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
export class PacketReader {
|
||||||
|
buffer: Uint8Array;
|
||||||
|
private view: DataView;
|
||||||
|
pos: number;
|
||||||
|
totalPacketSize = 0;
|
||||||
|
|
||||||
|
constructor(buffer: Uint8Array) {
|
||||||
|
this.buffer = buffer;
|
||||||
|
this.view = new DataView(buffer.buffer);
|
||||||
|
this.pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
readByte(): number {
|
||||||
|
const x = this.buffer[this.pos];
|
||||||
|
this.pos += 1;
|
||||||
|
this.totalPacketSize += 1;
|
||||||
|
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
readShort(): number {
|
||||||
|
const x = this.view.getInt16(this.pos);
|
||||||
|
this.pos += 2;
|
||||||
|
this.totalPacketSize += 2;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
readSByte(): number {
|
||||||
|
const x = this.view.getInt8(this.pos);
|
||||||
|
this.pos += 1;
|
||||||
|
this.totalPacketSize += 1;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
readInt() {
|
||||||
|
const x = this.view.getInt32(this.pos);
|
||||||
|
this.pos += 4;
|
||||||
|
this.totalPacketSize += 4;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
readString(): string {
|
||||||
|
const x = this.buffer.subarray(this.pos, this.pos + 64);
|
||||||
|
this.pos += 64;
|
||||||
|
this.totalPacketSize += 64;
|
||||||
|
return new TextDecoder().decode(x).trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
readByteArray(): Uint8Array {
|
||||||
|
const x = this.buffer.subarray(this.pos, this.pos + 1024);
|
||||||
|
this.pos += 1024;
|
||||||
|
this.totalPacketSize += 1024;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PacketWriter {
|
||||||
|
private buffer: Uint8Array;
|
||||||
|
private view: DataView;
|
||||||
|
private pos: number;
|
||||||
|
|
||||||
|
constructor(lenght: number = 4096) {
|
||||||
|
this.buffer = new Uint8Array(lenght);
|
||||||
|
this.view = new DataView(this.buffer.buffer);
|
||||||
|
this.pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeByte(n: number) {
|
||||||
|
this.buffer[this.pos] = n;
|
||||||
|
this.pos += 1;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeShort(n: number) {
|
||||||
|
this.view.setInt16(this.pos, n);
|
||||||
|
this.pos += 2;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSByte(n: number) {
|
||||||
|
this.view.setInt8(this.pos, n);
|
||||||
|
this.pos += 1;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
writeInt(n: number) {
|
||||||
|
this.view.setInt32(this.pos, n);
|
||||||
|
this.pos += 4;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
writeString(n: string) {
|
||||||
|
const b = new TextEncoder().encode(n);
|
||||||
|
|
||||||
|
for (let x = 0; x < 64; x++) {
|
||||||
|
this.buffer[this.pos + x] = b[x] ?? 0x20;
|
||||||
|
}
|
||||||
|
this.pos += 64;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeByteArray(n: Uint8Array) {
|
||||||
|
for (let x = 0; x < 1024; x++) {
|
||||||
|
this.buffer[this.pos + x] = n[x] || 0;
|
||||||
|
}
|
||||||
|
this.pos += 1024;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toPacket() {
|
||||||
|
return this.buffer.subarray(0, this.pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import { gzip } from "https://cdn.skypack.dev/pako";
|
||||||
|
import { Position, World } from "./classes.ts";
|
||||||
|
import { Player } from "./Player.ts";
|
||||||
|
|
||||||
|
export class PacketDefinitions {
|
||||||
|
static async levelInit(player: Player) {
|
||||||
|
await player.writeToSocket(new Uint8Array([0x02]));
|
||||||
|
}
|
||||||
|
static async levelProgress(
|
||||||
|
length: number,
|
||||||
|
chunk: Uint8Array,
|
||||||
|
progress: number,
|
||||||
|
player: Player,
|
||||||
|
) {
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x03)
|
||||||
|
.writeShort(length)
|
||||||
|
.writeByteArray(chunk)
|
||||||
|
.writeByte(progress)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendPackets(player: Player, world: World) {
|
||||||
|
await PacketDefinitions.levelInit(player);
|
||||||
|
|
||||||
|
player.position = world.getSpawn();
|
||||||
|
|
||||||
|
const compressedMap = gzip(world.data)!;
|
||||||
|
|
||||||
|
for (let i = 0; i < compressedMap.length; i += 1024) {
|
||||||
|
const chunk = compressedMap.slice(
|
||||||
|
i,
|
||||||
|
Math.min(i + 1024, compressedMap.length),
|
||||||
|
);
|
||||||
|
await PacketDefinitions.levelProgress(chunk.length, chunk, 1, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
await PacketDefinitions.levelFinish(world.size, player);
|
||||||
|
|
||||||
|
await PacketDefinitions.spawn(player, -1, player);
|
||||||
|
|
||||||
|
}
|
||||||
|
static async levelFinish(size: Position, player: Player) {
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x04)
|
||||||
|
.writeShort(size.x)
|
||||||
|
.writeShort(size.y)
|
||||||
|
.writeShort(size.z)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static async defineBlock(block: {
|
||||||
|
blockID: number;
|
||||||
|
name: string;
|
||||||
|
solidity?: number;
|
||||||
|
movementSpeed?: number;
|
||||||
|
toptextureID: number;
|
||||||
|
sidetextureID: number;
|
||||||
|
bottomtextureID: number;
|
||||||
|
transmitsLight?: number;
|
||||||
|
walkSound: number;
|
||||||
|
shape?: number;
|
||||||
|
fullBright?: number;
|
||||||
|
blockDraw?: number;
|
||||||
|
fogDensity?: number;
|
||||||
|
fogR?: number;
|
||||||
|
fogG?: number;
|
||||||
|
fogB?: number;
|
||||||
|
}, player: Player) {
|
||||||
|
if (!block.solidity) block.solidity = 2;
|
||||||
|
if (!block.movementSpeed) block.movementSpeed = 128;
|
||||||
|
if (!block.transmitsLight) block.transmitsLight = 0;
|
||||||
|
if (!block.fullBright) block.fullBright = 0;
|
||||||
|
if (!block.shape) block.shape = 16;
|
||||||
|
if (!block.blockDraw) block.blockDraw = 0;
|
||||||
|
if (!block.fogDensity) block.fogDensity = 0;
|
||||||
|
if (!block.fogR) block.fogR = 0;
|
||||||
|
if (!block.fogG) block.fogG = 0;
|
||||||
|
if (!block.fogB) block.fogB = 0;
|
||||||
|
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x23)
|
||||||
|
.writeByte(block.blockID)
|
||||||
|
.writeString(block.name)
|
||||||
|
.writeByte(block.solidity)
|
||||||
|
.writeByte(block.movementSpeed)
|
||||||
|
.writeByte(block.toptextureID)
|
||||||
|
.writeByte(block.sidetextureID)
|
||||||
|
.writeByte(block.bottomtextureID)
|
||||||
|
.writeByte(block.transmitsLight)
|
||||||
|
.writeByte(block.walkSound)
|
||||||
|
.writeByte(block.fullBright)
|
||||||
|
.writeByte(block.shape)
|
||||||
|
.writeByte(block.blockDraw)
|
||||||
|
.writeByte(block.fogDensity)
|
||||||
|
.writeByte(block.fogR)
|
||||||
|
.writeByte(block.fogG)
|
||||||
|
.writeByte(block.fogB)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static async disconnect(reason: string, player: Player) {
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x0e)
|
||||||
|
.writeString(reason)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async spawn(player: Player, id: number, toplayer: Player) {
|
||||||
|
await toplayer.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x07)
|
||||||
|
.writeSByte(id)
|
||||||
|
.writeString(player.username)
|
||||||
|
.writeShort(player.position.x)
|
||||||
|
.writeShort(player.position.y)
|
||||||
|
.writeShort(player.position.z)
|
||||||
|
.writeByte(0)
|
||||||
|
.writeByte(0).toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async movement(player: Player, id: number, toplayer: Player) {
|
||||||
|
await toplayer.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x08)
|
||||||
|
.writeSByte(id)
|
||||||
|
.writeShort(player.position.x)
|
||||||
|
.writeShort(player.position.y)
|
||||||
|
.writeShort(player.position.z)
|
||||||
|
.writeByte(player.rotation.yaw)
|
||||||
|
.writeByte(player.rotation.pitch)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setBlock(position: Position, block: number, player: Player) {
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x06)
|
||||||
|
.writeShort(position.x)
|
||||||
|
.writeShort(position.y)
|
||||||
|
.writeShort(position.z)
|
||||||
|
.writeByte(block)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static async despawn(index: number, player: Player) {
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x0c)
|
||||||
|
.writeSByte(index)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async customblock(player: Player) {
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x013)
|
||||||
|
.writeByte(1)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static async changeModel(id: number, modelName: string, player: Player) {
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x1D)
|
||||||
|
.writeByte(id)
|
||||||
|
.writeString(modelName)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static async sendTexturePack(tx: string, player: Player) {
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x28)
|
||||||
|
.writeString(tx)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
static async ident(name: string, motd: string, player: Player) {
|
||||||
|
await player.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x00)
|
||||||
|
.writeByte(0x07)
|
||||||
|
.writeString(name)
|
||||||
|
.writeString(motd)
|
||||||
|
.writeByte(0x00)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
71
classes/Player.ts
Normal file
71
classes/Player.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { Position, Rotation, World } from "./classes.ts";
|
||||||
|
import { PacketDefinitions, PacketWriter } from "./Packets.ts";
|
||||||
|
import { log } from "../deps.ts";
|
||||||
|
import { Server } from "./Server.ts";
|
||||||
|
|
||||||
|
export class Player {
|
||||||
|
socket: Deno.Conn;
|
||||||
|
private server: Server;
|
||||||
|
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
world = "main";
|
||||||
|
position: Position;
|
||||||
|
rotation: Rotation = { yaw: 0, pitch: 0 };
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
socket: Deno.Conn,
|
||||||
|
username: string,
|
||||||
|
position: Position,
|
||||||
|
server: Server,
|
||||||
|
) {
|
||||||
|
this.socket = socket;
|
||||||
|
this.username = username;
|
||||||
|
this.position = position;
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeToSocket(ar: Uint8Array) {
|
||||||
|
await this.socket.write(ar).catch((e) => {
|
||||||
|
log.critical(e);
|
||||||
|
this.server.removeUser(this.socket);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
message(text: string, id = 0) {
|
||||||
|
text.replaceAll("%", "&").match(/.{1,64}/g)?.forEach(async (pic) => {
|
||||||
|
await this.writeToSocket(
|
||||||
|
new PacketWriter()
|
||||||
|
.writeByte(0x0d)
|
||||||
|
.writeSByte(id)
|
||||||
|
.writeString(pic)
|
||||||
|
.toPacket(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async toWorld(world: World) {
|
||||||
|
this.server.broadcastPacket(
|
||||||
|
(e) => PacketDefinitions.despawn(this.server.players.indexOf(this), e),
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.world = world.name;
|
||||||
|
|
||||||
|
PacketDefinitions.sendPackets(this, world);
|
||||||
|
|
||||||
|
this.server.broadcastPacket(
|
||||||
|
(e) =>
|
||||||
|
PacketDefinitions.spawn(this, this.server.players.indexOf(this), e),
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
this.server.broadcastPacket(
|
||||||
|
(e) => PacketDefinitions.spawn(e, this.server.players.indexOf(e), this),
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.message("You have been moved.");
|
||||||
|
|
||||||
|
await world.save();
|
||||||
|
}
|
||||||
|
}
|
307
classes/Server.ts
Normal file
307
classes/Server.ts
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
import {
|
||||||
|
PacketDefinitions,
|
||||||
|
PacketReader,
|
||||||
|
Player,
|
||||||
|
Plugin,
|
||||||
|
World,
|
||||||
|
} from "./classes.ts";
|
||||||
|
|
||||||
|
import { config, crypto, log, s3, toHexString } from "../deps.ts";
|
||||||
|
|
||||||
|
type PlayerFunction = (a: Player) => void;
|
||||||
|
|
||||||
|
interface PluginUpdateTime {
|
||||||
|
plugin: Plugin;
|
||||||
|
lastUpdated: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Server {
|
||||||
|
server!: Deno.Listener;
|
||||||
|
|
||||||
|
players: Player[] = [];
|
||||||
|
plugins: Map<string, PluginUpdateTime> = new Map();
|
||||||
|
lengthMap: Map<number, number> = new Map([[0, 131], [5, 9], [8, 10], [
|
||||||
|
13,
|
||||||
|
66,
|
||||||
|
]]);
|
||||||
|
worlds: World[] = [new World({ x: 64, y: 64, z: 64 }, "main")];
|
||||||
|
|
||||||
|
async start(port: number) {
|
||||||
|
this.server = Deno.listen({ port: port });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await s3.headBucket({
|
||||||
|
Bucket: "cla66ic",
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.onlineMode) {
|
||||||
|
setInterval(async () => {
|
||||||
|
await fetch(
|
||||||
|
"https://www.classicube.net/heartbeat.jsp" +
|
||||||
|
`?port=${config.port}` +
|
||||||
|
"&max=255" +
|
||||||
|
"&name=Cla66ic" +
|
||||||
|
"&public=True" +
|
||||||
|
`&version=7&salt=${config.hash}` +
|
||||||
|
`&users=${this.players.length}`,
|
||||||
|
);
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updatePlugins();
|
||||||
|
|
||||||
|
log.info(`Listening on port ${config.port}!`);
|
||||||
|
|
||||||
|
for await (const socket of this.server) {
|
||||||
|
this.startSocket(socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Deno.readDir("./plugins")) {
|
||||||
|
if (file.isFile) {
|
||||||
|
const name = file.name.split(".ts")[0];
|
||||||
|
|
||||||
|
if (!this.plugins.has(name)) {
|
||||||
|
this.plugins.set(name, {
|
||||||
|
lastUpdated: Deno.statSync(`./plugins/${file.name}`).mtime!,
|
||||||
|
plugin: new ((await import(`../plugins/${file.name}`)).default)(
|
||||||
|
this,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const plugin = this.plugins.get(name);
|
||||||
|
|
||||||
|
if (
|
||||||
|
Deno.statSync(`./plugins/${file.name}`).mtime!.getTime() !==
|
||||||
|
plugin?.lastUpdated.getTime()
|
||||||
|
) {
|
||||||
|
plugin?.plugin.emit("stop");
|
||||||
|
this.plugins.set(name, {
|
||||||
|
lastUpdated: Deno.statSync(`./plugins/${file.name}`).mtime!,
|
||||||
|
plugin:
|
||||||
|
new ((await import(`../plugins/${file.name}#` + Math.random()))
|
||||||
|
.default)(
|
||||||
|
this,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeUser(conn: Deno.Conn) {
|
||||||
|
const player = this.players.find((e) => e.socket == conn);
|
||||||
|
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
|
const index = this.players.indexOf(player);
|
||||||
|
|
||||||
|
this.players = this.players.filter((e) => e != player);
|
||||||
|
|
||||||
|
this.broadcast(`${player.username} has &cleft`);
|
||||||
|
this.worlds.find((e) => e.name == player.world)!.save();
|
||||||
|
|
||||||
|
this.broadcastPacket((e) => PacketDefinitions.despawn(index, e), player);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handlePacket(packet: PacketReader, connection: Deno.Conn) {
|
||||||
|
const packetType = packet.readByte();
|
||||||
|
if (packetType == 0x00) {
|
||||||
|
if (this.players.find((e) => e.socket == connection)) return;
|
||||||
|
packet.readByte();
|
||||||
|
const username = packet.readString();
|
||||||
|
|
||||||
|
const verification = packet.readString();
|
||||||
|
const player = new Player(
|
||||||
|
connection,
|
||||||
|
username,
|
||||||
|
this.worlds[0].getSpawn(),
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!verification) {
|
||||||
|
player.socket.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = toHexString(
|
||||||
|
new Uint8Array(
|
||||||
|
await crypto.subtle.digest(
|
||||||
|
"MD5",
|
||||||
|
new TextEncoder().encode(config.hash + player.username),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
config.onlineMode && verification != config.hash &&
|
||||||
|
!this.players.find((e) => e.socket == connection)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
str !== verification
|
||||||
|
) {
|
||||||
|
await PacketDefinitions.disconnect(
|
||||||
|
"Refresh your playerlist! Incorrect hash!",
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
|
||||||
|
player.socket.close();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.players.find((e) => e.username == player.username)) {
|
||||||
|
await PacketDefinitions.disconnect(
|
||||||
|
"Your name is already being used!",
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
player.socket.close();
|
||||||
|
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)!;
|
||||||
|
|
||||||
|
packet.readByte();
|
||||||
|
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,
|
||||||
|
this.players.indexOf(player),
|
||||||
|
e,
|
||||||
|
), player);
|
||||||
|
} else if (packetType == 0x0d) {
|
||||||
|
packet.readByte();
|
||||||
|
|
||||||
|
const player = this.players.find((e) => e.socket == connection)!;
|
||||||
|
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)!;
|
||||||
|
|
||||||
|
const position = {
|
||||||
|
x: packet.readShort(),
|
||||||
|
y: packet.readShort(),
|
||||||
|
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)!;
|
||||||
|
|
||||||
|
const before = world.getBlock(position);
|
||||||
|
|
||||||
|
this.worlds.find((e) => e.name == player.world)!.setBlock(position, id);
|
||||||
|
|
||||||
|
this.broadcastPacket(
|
||||||
|
(e) => PacketDefinitions.setBlock(position, id, e),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.plugins.forEach((value) => { // TODO: Rework this to work with proper block disabling (not resetting bullshit)
|
||||||
|
value.plugin.emit("setblock", player, mode, id, position, before);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.buffer.length - 1 >= packet.pos) { // TODO: This logic is wrong! Sometimes, TCP packets are still dropped. Need to rewrite this properly.
|
||||||
|
this.handlePacket(packet, connection);
|
||||||
|
|
||||||
|
packet.pos += packet.totalPacketSize;
|
||||||
|
packet.totalPacketSize = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startSocket(connection: Deno.Conn) {
|
||||||
|
const buffer = new Uint8Array(2048);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
let count;
|
||||||
|
try {
|
||||||
|
count = await connection.read(buffer);
|
||||||
|
} catch (e) {
|
||||||
|
count = 0;
|
||||||
|
log.critical(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!count) {
|
||||||
|
this.removeUser(connection);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
const packet = new PacketReader(buffer.subarray(0, count));
|
||||||
|
this.handlePacket(packet, connection);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
133
classes/World.ts
Normal file
133
classes/World.ts
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import { gzip, ungzip } from "https://cdn.skypack.dev/pako";
|
||||||
|
import { s3 } from "../deps.ts";
|
||||||
|
import { Position } from "./classes.ts";
|
||||||
|
|
||||||
|
export class World {
|
||||||
|
size: Position;
|
||||||
|
data: Uint8Array;
|
||||||
|
private dataView: DataView;
|
||||||
|
name: string;
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
optionalJson: any = {};
|
||||||
|
|
||||||
|
constructor(size: Position, name: string) {
|
||||||
|
this.size = size;
|
||||||
|
this.name = name;
|
||||||
|
|
||||||
|
this.data = new Uint8Array(4 + size.x * size.y * size.z);
|
||||||
|
this.dataView = new DataView(this.data.buffer);
|
||||||
|
|
||||||
|
this.dataView.setInt32(0, this.size.x * this.size.y * this.size.z, false);
|
||||||
|
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlock(pos: Position, block: number) {
|
||||||
|
const szz = this.sizeToWorldSize(pos);
|
||||||
|
|
||||||
|
this.dataView.setUint8(szz, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlock(pos: Position) {
|
||||||
|
return this.dataView.getUint8(this.sizeToWorldSize(pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
findID(block: number): Position[] {
|
||||||
|
const position = [];
|
||||||
|
for (let z = 0; z < this.size.z; z++) {
|
||||||
|
for (let y = 0; y < this.size.y; y++) {
|
||||||
|
for (let x = 0; x < this.size.x; x++) {
|
||||||
|
if (this.getBlock({ z, y, x }) == block) {
|
||||||
|
position.push({ z, y, x });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sizeToWorldSize(pos: Position): number {
|
||||||
|
return 4 + pos.x + this.size.z * (pos.z + this.size.x * pos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSpawn(): Position {
|
||||||
|
return {
|
||||||
|
x: Math.floor(this.size.x / 2) * 32,
|
||||||
|
y: (Math.floor(this.size.y / 2) * 32) + 32,
|
||||||
|
z: Math.floor(this.size.z / 2) * 32,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayer(y: number, type: number) {
|
||||||
|
for (let i = 0; i < this.size.z; i += 1) {
|
||||||
|
for (let b = 0; b < this.size.x; b += 1) {
|
||||||
|
this.setBlock({
|
||||||
|
x: b,
|
||||||
|
y,
|
||||||
|
z: i,
|
||||||
|
}, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete() {
|
||||||
|
try {
|
||||||
|
await s3.deleteObject({
|
||||||
|
Bucket: "cla66ic",
|
||||||
|
Key: this.name + ".buf",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// doesn't exist, probably..
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private async load() {
|
||||||
|
try {
|
||||||
|
const head = await s3.headObject({
|
||||||
|
Bucket: "cla66ic",
|
||||||
|
Key: this.name + ".buf",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ungziped = ungzip(
|
||||||
|
(await s3.getObject({
|
||||||
|
Bucket: "cla66ic",
|
||||||
|
Key: this.name + ".buf",
|
||||||
|
})).Body,
|
||||||
|
);
|
||||||
|
if (!(ungziped instanceof Uint8Array)) return;
|
||||||
|
|
||||||
|
this.size = {
|
||||||
|
x: +head.Metadata.x!,
|
||||||
|
y: +head.Metadata.y!,
|
||||||
|
z: +head.Metadata.z!,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.data = ungziped;
|
||||||
|
this.dataView = new DataView(this.data.buffer);
|
||||||
|
this.optionalJson = JSON.parse(head.Metadata.json || "{}");
|
||||||
|
} catch {
|
||||||
|
const layers = Math.floor(this.size.y / 2);
|
||||||
|
|
||||||
|
for (let i = 0; i < layers; i += 1) {
|
||||||
|
if (i === layers - 1) {
|
||||||
|
this.setLayer(layers - 1, 2);
|
||||||
|
} else {
|
||||||
|
this.setLayer(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
await s3.putObject({
|
||||||
|
Bucket: "cla66ic",
|
||||||
|
Key: this.name + ".buf",
|
||||||
|
Body: gzip(this.data),
|
||||||
|
Metadata: {
|
||||||
|
"x": this.size.x + "",
|
||||||
|
"y": this.size.y + "",
|
||||||
|
"z": this.size.z + "",
|
||||||
|
"json": JSON.stringify(this.optionalJson),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
40
classes/classes.ts
Normal file
40
classes/classes.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { EventEmitter } from "../deps.ts";
|
||||||
|
import { Player } from "./Player.ts";
|
||||||
|
import { Server } from "./Server.ts";
|
||||||
|
|
||||||
|
export { Player } from "./Player.ts";
|
||||||
|
export { World } from "./World.ts";
|
||||||
|
export { PacketDefinitions, PacketReader, PacketWriter } from "./Packets.ts";
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rotation {
|
||||||
|
yaw: number;
|
||||||
|
pitch: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Plugin extends EventEmitter<{
|
||||||
|
command(
|
||||||
|
command: string,
|
||||||
|
player: Player,
|
||||||
|
args: string[],
|
||||||
|
): void;
|
||||||
|
|
||||||
|
setblock(
|
||||||
|
player: Player,
|
||||||
|
mode: number,
|
||||||
|
id: number,
|
||||||
|
position: Position,
|
||||||
|
blockBefore: number,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
stop(): void;
|
||||||
|
}> {
|
||||||
|
commands: string[] = [];
|
||||||
|
|
||||||
|
server!: Server;
|
||||||
|
}
|
27
deps.ts
Normal file
27
deps.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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 "https://deno.land/x/eventemitter@1.2.1/mod.ts";
|
||||||
|
import "https://deno.land/x/dotenv@v3.2.0/load.ts";
|
||||||
|
export const toHexString = (bytes: Uint8Array) =>
|
||||||
|
bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
|
||||||
|
|
||||||
|
export const s3 = new ApiFactory({
|
||||||
|
credentials: {
|
||||||
|
awsAccessKeyId: Deno.env.get("S3_ACCESS_KEY_ID")!,
|
||||||
|
awsSecretKey: Deno.env.get("S3_SECRET_KEY")!,
|
||||||
|
},
|
||||||
|
fixedEndpoint: "https://s3.us-west-004.backblazeb2.com",
|
||||||
|
region: "us-west-004",
|
||||||
|
}).makeNew(S3);
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
ops: Deno.env.get("OPS") ? JSON.parse(Deno.env.get("OPS")!) : [],
|
||||||
|
port: +Deno.env.get("PORT")!,
|
||||||
|
hash: Deno.env.get("HASH"),
|
||||||
|
onlineMode: Deno.env.get("ONLINEMODE") == "true"
|
||||||
|
};
|
6
index.ts
Normal file
6
index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { config } from "./deps.ts";
|
||||||
|
import { Server } from "./classes/Server.ts";
|
||||||
|
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
await server.start(config.port);
|
44
plugins/commands.ts
Normal file
44
plugins/commands.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Plugin } from "../classes/classes.ts";
|
||||||
|
import { Server } from "../classes/Server.ts";
|
||||||
|
import { config } from "../deps.ts";
|
||||||
|
|
||||||
|
export default class CommandPlugin extends Plugin {
|
||||||
|
commands = [
|
||||||
|
"help",
|
||||||
|
"reloadplugins",
|
||||||
|
"clients",
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(server: Server) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.server = server;
|
||||||
|
|
||||||
|
this.on("command", async (command, player) => {
|
||||||
|
if (command == "help") {
|
||||||
|
let allComamnds = "";
|
||||||
|
for (const [_k, v] of server.plugins) {
|
||||||
|
allComamnds += `${v.plugin.commands.join(", ")}, `;
|
||||||
|
}
|
||||||
|
player.message(allComamnds.slice(0, -2));
|
||||||
|
} else if (command == "reloadplugins") {
|
||||||
|
if (config.ops.includes(player.username)) {
|
||||||
|
server.broadcast(
|
||||||
|
"&cRestarting plugins. &fServer will be &4unstable.",
|
||||||
|
);
|
||||||
|
await this.server.updatePlugins();
|
||||||
|
server.broadcast("&eFinished! Server should be now &amostly stable.");
|
||||||
|
}
|
||||||
|
} else if (command == "clients") {
|
||||||
|
this.server.worlds.forEach((e) => {
|
||||||
|
const players = this.server.players.filter((b) => e.name == b.world);
|
||||||
|
if (players.length != 0) {
|
||||||
|
player.message(
|
||||||
|
`&a${e.name}&f: &a${players.map((e) => e.username).join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
147
plugins/world.ts
Normal file
147
plugins/world.ts
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import { PacketDefinitions, Plugin, World } from "../classes/classes.ts";
|
||||||
|
import { Server } from "../classes/Server.ts";
|
||||||
|
import { config } from "../deps.ts";
|
||||||
|
|
||||||
|
export default class CommandPlugin extends Plugin {
|
||||||
|
commands = [
|
||||||
|
"g",
|
||||||
|
"worlds",
|
||||||
|
"world",
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(server: Server) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.server = server;
|
||||||
|
this.on("setblock", (player, _mode, _id, position, blockBefore) => {
|
||||||
|
const world = server.worlds.find((e) => e.name == player.world)!;
|
||||||
|
if (!world.optionalJson?.builders?.includes("*")) {
|
||||||
|
if (!world.optionalJson?.builders?.includes(player.username)) {
|
||||||
|
player.message("You are %cnot allowed &fto build in this world!");
|
||||||
|
world.setBlock(position, blockBefore);
|
||||||
|
|
||||||
|
server.players.forEach(async (e) => {
|
||||||
|
if (e.world == player.world) {
|
||||||
|
await PacketDefinitions.setBlock(position, blockBefore, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.on("command", async (command, player, args) => {
|
||||||
|
if (command == "g") {
|
||||||
|
const requestedWorld = server.worlds.find((e) =>
|
||||||
|
e.name.toLowerCase() == args.join(" ").toLowerCase()
|
||||||
|
);
|
||||||
|
if (requestedWorld) {
|
||||||
|
player.toWorld(requestedWorld);
|
||||||
|
} else {
|
||||||
|
player.message(`World ${args.join(" ")} does &4not exist!`);
|
||||||
|
}
|
||||||
|
} else if (command == "worlds") {
|
||||||
|
player.message(
|
||||||
|
`Available worlds (/g): &a${
|
||||||
|
server.worlds.map((e) => e.name).join(", ")
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
} else if (command == "world") {
|
||||||
|
const category = args[0];
|
||||||
|
|
||||||
|
if (category == "create") {
|
||||||
|
if (!server.worlds.find((e) => e.name == player.username)) {
|
||||||
|
const world = new World(
|
||||||
|
{ x: 64, y: 64, z: 64 },
|
||||||
|
player.username,
|
||||||
|
);
|
||||||
|
world.optionalJson.builders = [];
|
||||||
|
world.optionalJson.builders.push(player.username);
|
||||||
|
server.worlds.push(world);
|
||||||
|
|
||||||
|
player.message(`&aWorld created!&f Use /g ${player.username}!`);
|
||||||
|
} else {
|
||||||
|
player.message(
|
||||||
|
`&cYou already own a world!&f Use /g ${player.username}!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (category == "delete") {
|
||||||
|
const world = server.worlds.find((e) => e.name == player.username);
|
||||||
|
if (world) {
|
||||||
|
server.broadcastPacket((e) => e.toWorld(server.worlds[0]), player);
|
||||||
|
|
||||||
|
player.toWorld(server.worlds[0]);
|
||||||
|
|
||||||
|
await world.delete();
|
||||||
|
|
||||||
|
server.worlds = server.worlds.filter((e) =>
|
||||||
|
e.name !== player.username
|
||||||
|
);
|
||||||
|
|
||||||
|
player.message(`&cWorld deleted.`);
|
||||||
|
} else {
|
||||||
|
player.message(
|
||||||
|
`&cYou don't have a world.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (category == "builders") {
|
||||||
|
const subcategory = args[1];
|
||||||
|
let world = server.worlds.find((e) => e.name == player.username);
|
||||||
|
|
||||||
|
if (args[3] && config.ops.includes(player.username)) {
|
||||||
|
world = server.worlds.find((e) => e.name == args[3]);
|
||||||
|
|
||||||
|
player.message(
|
||||||
|
`&aOP Overwrite detected! Operating on world ${args[3]}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!world) {
|
||||||
|
player.message(`&cWorld does not exist/you do not own a world.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!world.optionalJson?.builders) world.optionalJson.builders = [];
|
||||||
|
|
||||||
|
if (subcategory == "add") {
|
||||||
|
const username = args[2];
|
||||||
|
world.optionalJson.builders.push(username);
|
||||||
|
player.message(
|
||||||
|
`&a${username}&f sucesfully added as a builder to world &a${world.name}!`,
|
||||||
|
);
|
||||||
|
await world.save();
|
||||||
|
} else if (subcategory == "remove") {
|
||||||
|
const username = args[2];
|
||||||
|
|
||||||
|
const before = world.optionalJson.builders.length;
|
||||||
|
|
||||||
|
world.optionalJson.builders = world.optionalJson.builders.filter((
|
||||||
|
e: string,
|
||||||
|
) => e !== username);
|
||||||
|
|
||||||
|
const after = world.optionalJson.builders.length;
|
||||||
|
|
||||||
|
player.message(
|
||||||
|
`Removed &a${
|
||||||
|
before - after
|
||||||
|
}&f builder/s with name &a${username}&f in world &a${world.name}!`,
|
||||||
|
);
|
||||||
|
await world.save();
|
||||||
|
} else if (subcategory == "list") {
|
||||||
|
player.message(
|
||||||
|
`&a${world.name}&f's builders: &a${
|
||||||
|
world.optionalJson.builders.join(", ")
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
player.message(
|
||||||
|
`&a/world builders [add/remove/list] USERNAME`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
player.message(
|
||||||
|
`&a/world [create/delete/builders]`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
40
readme.md
Normal file
40
readme.md
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# cla66ic
|
||||||
|
|
||||||
|
## sucessor of cla55ic
|
||||||
|
|
||||||
|
### features:
|
||||||
|
1. written in typescript (types and shit)
|
||||||
|
2. entierly cloud based (meaning you can host it anywhere)
|
||||||
|
3. extremely extensive plugin system
|
||||||
|
4. solid implementation of sockets in deno and how to patch them together
|
||||||
|
5. DENO!! It's not node, and a classic server.
|
||||||
|
|
||||||
|
### setup tutorial (be warned it's not the easiest)
|
||||||
|
1. make a backblaze b2 account, make a bucket, and get your keys from the bucket
|
||||||
|
2. configure .env file to look something like
|
||||||
|
```
|
||||||
|
PORT=6969
|
||||||
|
HASH=RandomHashIlIke
|
||||||
|
OPS=["Me"]
|
||||||
|
ONLINEMODE=true
|
||||||
|
|
||||||
|
S3_ACCESS_KEY_ID="MyAccessKey"
|
||||||
|
S3_SECRET_KEY="SecretKey"
|
||||||
|
```
|
||||||
|
NOTE: if you are running inside of a cloud provider, just set these as
|
||||||
|
your environment variables
|
||||||
|
|
||||||
|
3. install deno
|
||||||
|
4. run `deno run --allow-env --allow-net --allow-read index.ts`
|
||||||
|
### insipration taken from:
|
||||||
|
1. mcgalaxy (obviuouuusly!!)
|
||||||
|
2. https://github.com/Patbox/Cobblestone-Classic (some protocol information and worldhandling)
|
||||||
|
3. cla55ic (world data too)
|
||||||
|
|
||||||
|
### issues:
|
||||||
|
1. plugin system event handling is lackluster in some palces
|
||||||
|
2. tcp packet splitting fails sometimes
|
||||||
|
3. the player-id implementation totally sucks!! it sometimes merges players together and soforth
|
||||||
|
4. no cpe support! i want to get all of the above issues fixed before implementing CPE support
|
||||||
|
5. proper rank support (implemented as plugin)
|
||||||
|
6. no discord bridge (implemented as plugin)
|
Loading…
Reference in a new issue