diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2111fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +.vscode \ No newline at end of file diff --git a/classes/Packets.ts b/classes/Packets.ts new file mode 100644 index 0000000..638c19d --- /dev/null +++ b/classes/Packets.ts @@ -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(), + ); + } +} diff --git a/classes/Player.ts b/classes/Player.ts new file mode 100644 index 0000000..481c1dd --- /dev/null +++ b/classes/Player.ts @@ -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(); + } +} diff --git a/classes/Server.ts b/classes/Server.ts new file mode 100644 index 0000000..9740c1e --- /dev/null +++ b/classes/Server.ts @@ -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 = new Map(); + lengthMap: Map = 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); + + } + } + } +} diff --git a/classes/World.ts b/classes/World.ts new file mode 100644 index 0000000..78e4e01 --- /dev/null +++ b/classes/World.ts @@ -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), + }, + }); + } +} diff --git a/classes/classes.ts b/classes/classes.ts new file mode 100644 index 0000000..b0693ea --- /dev/null +++ b/classes/classes.ts @@ -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; +} diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..5e9d344 --- /dev/null +++ b/deps.ts @@ -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" +}; diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f51209e --- /dev/null +++ b/index.ts @@ -0,0 +1,6 @@ +import { config } from "./deps.ts"; +import { Server } from "./classes/Server.ts"; + +const server = new Server(); + +await server.start(config.port); diff --git a/plugins/commands.ts b/plugins/commands.ts new file mode 100644 index 0000000..b58e8bb --- /dev/null +++ b/plugins/commands.ts @@ -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(", ")}`, + ); + } + }); + } + }); + } +} diff --git a/plugins/world.ts b/plugins/world.ts new file mode 100644 index 0000000..18096a5 --- /dev/null +++ b/plugins/world.ts @@ -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]`, + ); + } + } + }); + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..dab8b77 --- /dev/null +++ b/readme.md @@ -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)