This commit is contained in:
yourfriend 2022-06-02 01:30:34 +03:00
parent b73469acbe
commit b8ca8c13d1
No known key found for this signature in database
GPG key ID: C28FFD8607DAC4DE
11 changed files with 1126 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.env
.vscode

309
classes/Packets.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)