Compare commits

...

19 commits
TCP ... main

Author SHA1 Message Date
Soph :3 5226d5652a
more debugging, and fixes 2024-05-03 21:09:22 +03:00
Soph :3 957249ed71
get rid of debug 2024-05-03 20:47:24 +03:00
Soph :3 888782c2a9
fix some bugs 2024-05-03 20:46:05 +03:00
Soph :3 59d2db1785
more debugging 2024-05-03 20:32:10 +03:00
Soph :3 c72b2c494e
debug 2024-05-03 20:29:22 +03:00
Soph :3 a83abf598d
test 2024-05-03 20:16:13 +03:00
Soph :3 6715d264e4
add default host 2024-05-03 20:08:09 +03:00
Soph :3 b5e4cabe4d
warning for incorrect decrypt size 2024-05-03 00:52:41 +03:00
Soph :3 2c6835e2b3
fix some bun related issues 2024-04-28 18:36:36 +03:00
Soph :3 7159558e5e
switch to Bun 2024-04-28 18:33:00 +03:00
Soph :3 ef586df9a6
get rid of s3 2024-04-28 15:32:04 +03:00
yourfriend 8cd07e8521
天上太阳红衫衫 2022-11-13 17:19:56 +02:00
yourfriend 4af20b4df5
make users OP by default, allow setblock to return promise 2022-07-14 18:08:12 +03:00
yourfriend c31b81133f
formatting + make so you can change the main world 2022-07-14 16:26:27 +03:00
yourfriend 4d69c4c696
Fixes #3 2022-07-10 22:24:30 +03:00
yourfriend 5999e6b3f4
спасибо Россия 2022-07-10 20:01:35 +03:00
yourfriend 5e3306276a
Fix error, make removeUser async, re-enable world saving 2022-07-10 18:19:17 +03:00
yourfriend 41a19c26a6
unique IP heartbeats 2022-07-10 17:54:16 +03:00
yourfriend 11700fe211
Merge pull request #2 from yourfriendoss/TCP
TCP Packet Splitting (fixes #1)
2022-07-10 17:32:49 +03:00
13 changed files with 296 additions and 219 deletions

5
.gitignore vendored
View file

@ -1,2 +1,7 @@
.env
.vscode
plugins/*
!plugins/commands.ts
!plugins/world.ts
worlds/*
node_modules

BIN
bun.lockb Executable file

Binary file not shown.

View file

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

View file

@ -1,21 +1,23 @@
import { Position, Rotation, World } from "./classes.ts";
import { PacketDefinitions, PacketWriter } from "./Packets.ts";
import { log } from "../deps.ts";
import { config, log } from "../deps.ts";
import { Server } from "./Server.ts";
import {Socket} from 'bun';
export class Player {
socket: Deno.Conn;
socket: Socket<{dataBuffer?: Buffer}>;
private server: Server;
username: string;
ip: string;
id: number;
world = "main";
world = config.main;
position: Position;
rotation: Rotation = { yaw: 0, pitch: 0 };
constructor(
socket: Deno.Conn,
socket: Socket<{dataBuffer?: Buffer}>,
username: string,
position: Position,
server: Server,
@ -24,7 +26,7 @@ export class Player {
this.username = username;
this.position = position;
this.server = server;
this.ip = (this.socket.remoteAddr as Deno.NetAddr).hostname;
this.ip = this.socket.remoteAddress;
let id = Math.floor(Math.random() * server.maxUsers);
@ -37,10 +39,15 @@ export class Player {
}
async writeToSocket(ar: Uint8Array) {
await this.socket.write(ar).catch((e) => {
try {
this.socket.write(ar)
} catch(e) {
log.critical(e);
this.server.removeUser(this.socket);
});
await this.server.removeUser(
this.socket,
"Write failed" + e.message.split("\n")[0],
);
}
}
message(text: string, id = 0) {
@ -75,7 +82,5 @@ export class Player {
);
this.message("You have been moved.");
//await world.save(); TODO: this causes way too many issues
}
}

View file

@ -5,8 +5,10 @@ import {
Plugin,
World,
} from "./classes.ts";
import { Socket, TCPSocketListener } from "bun"
import { config, crypto, log, s3, toHexString } from "../deps.ts";
import { config, log } from "../deps.ts";
import * as fs from "node:fs/promises"
type PlayerFunction = (a: Player) => void;
@ -16,7 +18,7 @@ interface PluginUpdateTime {
}
export class Server {
server!: Deno.Listener;
server!: TCPSocketListener;
players: Player[] = [];
plugins: Map<string, PluginUpdateTime> = new Map();
@ -28,38 +30,73 @@ export class Server {
[13, 65],
]);
maxUsers = 69
maxUsers = config.maxUsers;
worlds: World[] = [new World({ x: 64, y: 64, z: 64 }, "main")];
worlds: World[] = [new World({ x: 64, y: 64, z: 64 }, config.main), new World({ x: 256, y: 64, z: 256 }, "large")];
async start(port: number) {
this.server = Deno.listen({ port: port });
this.server = Bun.listen<{dataBuffer?: Buffer}>({
hostname: process.env.HOST || "127.0.0.1",
port: +process.env.PORT!,
socket: {
data: async (socket, data) => {
if(socket.data.dataBuffer) {
const newBuffer = Buffer.concat([socket.data.dataBuffer, data]);
socket.data.dataBuffer = newBuffer;
} else {
socket.data.dataBuffer = data;
}
log.debug("Socket", socket.remoteAddress, "has", socket.data.dataBuffer.length, "data for parsing.");
try {
await s3.headBucket({
Bucket: "cla66ic",
});
//if(config.debug) await new Promise(r => setTimeout(r, 300));
log.info("s3 bucket exists!");
} catch {
log.warning("s3 bucket does not exist.. Creating!");
const parseBuffer = () => {
if(!socket.data.dataBuffer) return;
if(socket.data.dataBuffer.length == 0) return;
const packetId = socket.data.dataBuffer.readUint8(0);
const packetLength = this.lengthMap.get(packetId);
if(!packetLength) {
log.debug("Incorrect packet ID", packetId, "packet length could not be found.")
return;
};
if(socket.data.dataBuffer.byteLength < packetLength) {
log.debug("not enough bytes for packet", packetId)
return;
};
await s3.createBucket({
Bucket: "cla66ic",
});
this.handlePacket(socket.data.dataBuffer.copyWithin(0, 1, packetLength), packetId, socket);
log.debug("Parsed packet", packetId, "with packet length", packetLength)
//console.log(socket.data.dataBuffer, socket.data.dataBuffer.copyWithin(0, 1, packetLength))
socket.data.dataBuffer = Uint8Array.prototype.slice.call(socket.data.dataBuffer, packetLength+1);
parseBuffer();
}
(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]);
parseBuffer();
},
open: (socket) => {
socket.data = {}
},
close: async (socket) => {
await this.removeUser(socket, "Disconnected");
},
drain(socket) {},
error(socket, error) {},
},
});
try {
await fs.stat("worlds/");
for await (const dirEntry of await fs.readdir("worlds/", {withFileTypes: true})) {
const world = new World({ x: 0, y: 0, z: 0 }, dirEntry.name.replace(".buf", ""));
this.worlds.push(world);
}
});
} catch {
await fs.mkdir("worlds")
}
if (config.onlineMode) {
setInterval(async () => {
@ -67,11 +104,11 @@ export class Server {
"https://www.classicube.net/heartbeat.jsp" +
`?port=${config.port}` +
`&max=${this.maxUsers}` +
"&name=Cla66ic" +
`&name=${config.name}` +
"&public=True" +
"&software=Cla66ic" +
`&software=${config.software}` +
`&version=7&salt=${config.hash}` +
`&users=${this.players.length}`,
`&users=${[...new Set(this.players.map((obj) => obj.ip))].length}`,
);
}, 10000);
}
@ -80,9 +117,6 @@ export class Server {
log.info(`Listening on port ${config.port}!`);
for await (const socket of this.server) {
this.startSocket(socket);
}
}
broadcast(text: string) {
@ -104,13 +138,13 @@ export class Server {
}
async updatePlugins() {
for await (const file of Deno.readDir("./plugins")) {
if (file.isFile) {
for await (const file of await fs.readdir("./plugins", {withFileTypes:true})) {
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!,
lastUpdated: (await fs.stat(`./plugins/${file.name}`)).mtime!,
plugin: new ((await import(`../plugins/${file.name}`)).default)(
this,
),
@ -119,12 +153,12 @@ export class Server {
const plugin = this.plugins.get(name);
if (
Deno.statSync(`./plugins/${file.name}`).mtime!.getTime() !==
(await fs.stat(`./plugins/${file.name}`)).mtime!.getTime() !==
plugin?.lastUpdated.getTime()
) {
plugin?.plugin.emit("stop");
this.plugins.set(name, {
lastUpdated: Deno.statSync(`./plugins/${file.name}`).mtime!,
lastUpdated: (await fs.stat(`./plugins/${file.name}`)).mtime!,
plugin:
new ((await import(`../plugins/${file.name}#` + Math.random()))
.default)(
@ -136,7 +170,7 @@ export class Server {
}
}
}
removeUser(conn: Deno.Conn) {
async removeUser(conn: Socket<{dataBuffer?: Buffer}>, text: string) {
const player = this.players.find((e) => e.socket == conn);
if (!player) return;
@ -144,12 +178,14 @@ export class Server {
this.players = this.players.filter((e) => e != player);
try {
conn.close();
conn.end();
} catch {
// whatever
}
this.broadcast(`${player.username} has &cleft`);
this.broadcast(`${player.username} has &cleft&f, "${text}"`);
await this.worlds.find((e) => e.name == player.world)!.save();
this.broadcastPacket(
(e) => PacketDefinitions.despawn(player.id, e),
@ -160,7 +196,7 @@ export class Server {
async handlePacket(
buffer: Uint8Array,
packetType: number,
connection: Deno.Conn,
connection: Socket<{dataBuffer?: Buffer}>,
) {
const packet = new PacketReader(buffer);
if (packetType == 0x00) {
@ -170,9 +206,8 @@ export class Server {
const verification = packet.readString();
if(this.players.length >= this.maxUsers) {
connection.close();
if (this.players.length >= this.maxUsers) {
connection.end()
return;
}
@ -184,31 +219,25 @@ export class Server {
);
if (!verification) {
player.socket.close();
player.socket.end();
return true;
}
const hasher = new Bun.CryptoHasher("md5");
hasher.update(config.hash + player.username);
const str = toHexString(
new Uint8Array(
await crypto.subtle.digest(
"MD5",
new TextEncoder().encode(config.hash + player.username),
),
),
);
if (
config.onlineMode && verification != config.hash &&
!this.players.find((e) => e.socket == connection)
) {
if (
str !== verification
hasher.digest("hex") !== verification
) {
await PacketDefinitions.disconnect(
"Refresh your playerlist! Incorrect hash!",
player,
);
player.socket.close();
player.socket.end();
return true;
}
@ -219,7 +248,7 @@ export class Server {
"Your name is already being used!",
player,
);
player.socket.close();
player.socket.end();
return true;
}
@ -229,9 +258,10 @@ export class Server {
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)!;
const player = this.players.find((e) => e.socket == connection);
if (!player) return;
packet.readByte();
packet.readSByte();
player.position.x = packet.readShort();
player.position.y = packet.readShort();
player.position.z = packet.readShort();
@ -246,7 +276,8 @@ export class Server {
} else if (packetType == 0x0d) {
packet.readByte();
const player = this.players.find((e) => e.socket == connection)!;
const player = this.players.find((e) => e.socket == connection);
if (!player) return;
const message = packet.readString();
let playerColor = "[member] &b";
@ -270,19 +301,21 @@ export class Server {
}
this.broadcast(`${playerColor}${player.username}&f: ${message}`);
} else if (packetType == 0x05) {
const player = this.players.find((e) => e.socket == connection)!;
const player = this.players.find((e) => e.socket == connection);
if (!player) return;
let position = {x:0,y:0,z:0}
position.x = packet.readShort();
position.y = packet.readShort();
position.z = packet.readShort();
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 world = this.worlds.find((e) => e.name == player.world);
if (!world) return;
let pluginAnswer: boolean[] = [];
@ -305,51 +338,4 @@ export class Server {
);
}
}
async startSocket(connection: Deno.Conn) {
while (true) {
const packetID = new Uint8Array(1);
let packetIDReadAttempt;
try {
packetIDReadAttempt = await connection.read(packetID);
} catch {
this.removeUser(connection); // TODO: add a reason to this
break;
}
if (packetIDReadAttempt) {
const packetLength = this.lengthMap.get(packetID[0]);
if (!packetLength) {
log.critical("Unknown Packet: " + packetID[0]);
this.removeUser(connection); // TODO: add a reason to this
break;
}
let rawPacket = new Uint8Array(packetLength);
let packetReadAttempt;
try {
packetReadAttempt = await connection.read(rawPacket);
} catch {
this.removeUser(connection); // TODO: add a reason to this
break;
}
let fullRead = packetReadAttempt!;
while (fullRead < packetLength) {
const halfPacket = new Uint8Array(packetLength - fullRead);
rawPacket = new Uint8Array([...rawPacket, ...halfPacket]);
fullRead += (await connection.read(halfPacket))!;
}
this.handlePacket(rawPacket, packetID[0], connection);
} else {
this.removeUser(connection);
break;
}
}
}
}

View file

@ -1,14 +1,15 @@
import { gzip, ungzip } from "https://cdn.skypack.dev/pako";
import { s3 } from "../deps.ts";
import { decode, encode } from "cbor-x";
import { Position } from "./classes.ts";
import { unlink, readFile, writeFile} from "node:fs/promises";
export class World {
size: Position;
data: Uint8Array;
private dataView: DataView;
name: string;
// deno-lint-ignore no-explicit-any
optionalJson: any = {};
metadata: any = {};
constructor(size: Position, name: string) {
this.size = size;
@ -33,7 +34,7 @@ export class World {
}
findID(block: number): Position[] {
const position = [];
const position: 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++) {
@ -72,39 +73,37 @@ export class World {
async delete() {
try {
await s3.deleteObject({
Bucket: "cla66ic",
Key: this.name + ".buf",
});
await unlink(`worlds/${this.name}.buf`)
} catch {
// doesn't exist, probably..
// gang
}
}
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,
const ungziped = Bun.gunzipSync(
await readFile(`worlds/${this.name}.buf`)
);
if (!(ungziped instanceof Uint8Array)) return;
const dv = new DataView(ungziped.buffer);
const cborSize = dv.getUint32(0);
this.metadata = decode(new Uint8Array(ungziped.buffer.slice(4, cborSize+4)));
this.size = {
x: +head.Metadata.x!,
y: +head.Metadata.y!,
z: +head.Metadata.z!,
x: this.metadata.x!,
y: this.metadata.y!,
z: this.metadata.z!,
};
this.data = ungziped;
this.data = ungziped.slice(cborSize+4);
this.dataView = new DataView(this.data.buffer);
this.optionalJson = JSON.parse(head.Metadata.json || "{}");
} catch {
if(4 + this.size.x * this.size.y * this.size.z == this.data.length) {
console.log('[WARNING] Encoding was wrong somewhere!')
}
} catch(e) {
const layers = Math.floor(this.size.y / 2);
for (let i = 0; i < layers; i += 1) {
@ -118,16 +117,18 @@ export class World {
}
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),
},
});
const metadata = {
x: this.size.x!,
y: this.size.y!,
z: this.size.z!,
...this.metadata
}
const cborData = encode(metadata);
const buffer = new Uint8Array(4 + cborData.byteLength + this.data.byteLength);
const dv = new DataView(buffer.buffer);
dv.setUint32(0, cborData.byteLength);
buffer.set(cborData, 4);
buffer.set(this.data, 4 + cborData.byteLength);
await writeFile(`worlds/${this.name}.buf`, Bun.gzipSync(buffer)!);
}
}

View file

@ -1,4 +1,4 @@
import { EventEmitter } from "../deps.ts";
import EventEmitter from "./../events";
import { Player } from "./Player.ts";
import { Server } from "./Server.ts";
@ -29,7 +29,7 @@ export abstract class Plugin extends EventEmitter<{
mode: number,
id: number,
position: Position,
): boolean;
): Promise<boolean> | boolean;
stop(): void;
}> {

46
deps.ts
View file

@ -1,27 +1,29 @@
import "https://deno.land/x/dotenv@v3.2.0/load.ts";
import { ApiFactory } from "https://deno.land/x/aws_api@v0.7.0/client/mod.ts";
import { S3 } from "https://aws-api.deno.dev/v0.3/services/s3.ts";
export * as log from "https://deno.land/std@0.136.0/log/mod.ts";
export { crypto } from "https://deno.land/std@0.136.0/crypto/mod.ts";
export { EventEmitter } from "./events.ts";
import "https://deno.land/x/dotenv@v3.2.0/load.ts";
export const toHexString = (bytes: Uint8Array) =>
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")!,
export const log = {
info: (...a) => {
console.log("[INFO]", ...a);
},
fixedEndpoint: "https://s3.us-west-004.backblazeb2.com",
region: "us-west-004",
}).makeNew(S3);
warning: (...a) => {
console.warn("[WARNING]", ...a);
},
critical: (...a) => {
console.error("[ERROR]", ...a);
},
debug: (...a) => {
if(config.debug) {
console.log("[DEBUG]", ...a)
}
}
}
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",
ops: process.env.OPS ? JSON.parse(process.env.OPS!) : [],
port: +process.env.PORT!,
hash: process.env.HASH,
onlineMode: process.env.ONLINEMODE == "true",
main: process.env.MAIN || "main",
maxUsers: +(process.env.USERS || 24) > 255 ? 255 : +(process.env.USERS || 24),
software: process.env.SOFTWARE || "Custom Cla66ic",
name: process.env.NAME || "Cla66ic Server",
debug: process.env.DEBUG == "true",
};

View file

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

12
package.json Normal file
View file

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

View file

@ -1,4 +1,4 @@
import { Plugin } from "../classes/classes.ts";
import { PacketWriter, Player, Plugin } from "../classes/classes.ts";
import { Server } from "../classes/Server.ts";
import { config } from "../deps.ts";
@ -7,14 +7,36 @@ export default class CommandPlugin extends Plugin {
"help",
"reloadplugins",
"clients",
"tp",
"eval"
];
async tp(from: Player, to: Player) {
if(to.world != from.world) {
from.toWorld(this.server.worlds.find((e) =>
e.name == to.world
)!);
}
await from.writeToSocket(
new PacketWriter()
.writeByte(0x08)
.writeSByte(255)
.writeShort(to.position.x)
.writeShort(to.position.y)
.writeShort(to.position.z)
.writeByte(to.rotation.yaw)
.writeByte(to.rotation.pitch)
.toPacket(),
);
}
constructor(server: Server) {
super();
this.server = server;
this.on("command", async (command, player) => {
this.on("command", async (command, player, args) => {
if (command == "help") {
let allComamnds = "";
for (const [_k, v] of server.plugins) {
@ -38,6 +60,41 @@ export default class CommandPlugin extends Plugin {
);
}
});
} else if(command == "eval") {
if(config.ops.includes(player.username)) {
server.broadcast(eval(args.join(" ")));
}
} else if (command == "tp") {
if(args.length == 1) {
const teleportTo = this.server.players.find((e) => args[0] === e.username)
if(teleportTo) {
await this.tp(player, teleportTo);
} else {
player.message("Player is missing")
}
} else if(args.length == 3) {
const x = +args[0]
const y = +args[1]
const z = +args[2]
if(isNaN(x) || isNaN(y) || isNaN(z)) {
player.message("invalid coords")
return;
}
await player.writeToSocket(
new PacketWriter()
.writeByte(0x08)
.writeSByte(255)
.writeShort(x * 32)
.writeShort(y * 32)
.writeShort(z * 32)
.writeByte(player.rotation.yaw)
.writeByte(player.rotation.pitch)
.toPacket(),
);
}
}
});
}

View file

@ -7,6 +7,7 @@ export default class CommandPlugin extends Plugin {
"g",
"worlds",
"world",
"main",
];
constructor(server: Server) {
@ -15,8 +16,8 @@ export default class CommandPlugin extends Plugin {
this.server = server;
this.on("setblock", (player, _mode, _id) => {
const world = server.worlds.find((e) => e.name == player.world)!;
if (!world.optionalJson?.builders?.includes("*")) {
if (!world.optionalJson?.builders?.includes(player.username)) {
if (!world.metadata?.builders?.includes("*")) {
if (!world.metadata?.builders?.includes(player.username)) {
player.message("You are %cnot allowed &fto build in this world!");
return true;
}
@ -25,10 +26,19 @@ export default class CommandPlugin extends Plugin {
return false;
});
this.on("command", async (command, player, args) => {
if (command == "g") {
if (command == "main") {
await server.worlds.find((e) => e.name == player.world)!.save();
player.toWorld(
server.worlds.find((e) => e.name.toLowerCase() == config.main)!,
);
} else if (command == "g") {
await server.worlds.find((e) => e.name == player.world)!.save();
const requestedWorld = server.worlds.find((e) =>
e.name.toLowerCase() == args.join(" ").toLowerCase()
);
if (requestedWorld) {
player.toWorld(requestedWorld);
} else {
@ -49,8 +59,8 @@ export default class CommandPlugin extends Plugin {
{ x: 64, y: 64, z: 64 },
player.username,
);
world.optionalJson.builders = [];
world.optionalJson.builders.push(player.username);
world.metadata.builders = [];
world.metadata.builders.push(player.username);
server.worlds.push(world);
player.message(`&aWorld created!&f Use /g ${player.username}!`);
@ -95,11 +105,11 @@ export default class CommandPlugin extends Plugin {
return;
}
if (!world.optionalJson?.builders) world.optionalJson.builders = [];
if (!world.metadata?.builders) world.metadata.builders = [];
if (subcategory == "add") {
const username = args[2];
world.optionalJson.builders.push(username);
world.metadata.builders.push(username);
player.message(
`&a${username}&f sucesfully added as a builder to world &a${world.name}!`,
);
@ -107,13 +117,13 @@ export default class CommandPlugin extends Plugin {
} else if (subcategory == "remove") {
const username = args[2];
const before = world.optionalJson.builders.length;
const before = world.metadata.builders.length;
world.optionalJson.builders = world.optionalJson.builders.filter((
world.metadata.builders = world.metadata.builders.filter((
e: string,
) => e !== username);
const after = world.optionalJson.builders.length;
const after = world.metadata.builders.length;
player.message(
`Removed &a${
@ -124,7 +134,7 @@ export default class CommandPlugin extends Plugin {
} else if (subcategory == "list") {
player.message(
`&a${world.name}&f's builders: &a${
world.optionalJson.builders.join(", ")
world.metadata.builders.join(", ")
}`,
);
} else {

View file

@ -8,28 +8,27 @@
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.
5. Bun!! It's not node, and a classic server
6. very fast
### setup tutorial (be warned it's not the easiest)
### setup tutorial
1. make a backblaze b2 account, make a bucket, and get your keys from the bucket
2. configure .env file to look something like
1. configure .env file to look something like
```
PORT=6969
HASH=RandomHashIlIke
OPS=["Me"]
ONLINEMODE=true
S3_ACCESS_KEY_ID="MyAccessKey"
S3_SECRET_KEY="SecretKey"
MAIN=main
HOST=0.0.0.0
```
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`
2. install bun
3. run `bun index.ts`
### insipration taken from:
@ -39,12 +38,13 @@ environment variables
### issues:
1. Properly queue up map saves instead of just blantantly saving whenever possible
2. massive performance issues, running more than 100 something accounts makes the server instead insane amounts of cpu (most likely multithreading needed)
1. ~~Properly queue up map saves instead of just blantantly saving whenever
possible~~ it now saves to disk, IO is very fast and shouldn't cause issues anymore
2. ~~massive performance issues, running more than 100 something accounts makes
the server instead insane amounts of cpu (most likely multithreading needed)~~ the server is now async so it's way quicker (untested)
3. no cpe support! i want to get all of the above issues fixed before
implementing CPE support
4. no IP cooldown connections (no block cooldown either), no anticheat, no unique IP heartbeats
4. no IP cooldown connections (no block cooldown either), no anticheat
5. proper rank support (implemented as plugin)
6. no discord bridge (implemented as plugin)
7. no cla66ic/plugins repository