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

7
.gitignore vendored
View file

@ -1,2 +1,7 @@
.env .env
.vscode .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 { Position, World } from "./classes.ts";
import { Player } from "./Player.ts"; import { Player } from "./Player.ts";
@ -137,7 +136,7 @@ export class PacketDefinitions {
player.position = world.getSpawn(); player.position = world.getSpawn();
const compressedMap = gzip(world.data)!; const compressedMap = Bun.gzipSync(world.data)!;
for (let i = 0; i < compressedMap.length; i += 1024) { for (let i = 0; i < compressedMap.length; i += 1024) {
const chunk = compressedMap.slice( const chunk = compressedMap.slice(
@ -301,7 +300,7 @@ export class PacketDefinitions {
.writeByte(0x07) .writeByte(0x07)
.writeString(name) .writeString(name)
.writeString(motd) .writeString(motd)
.writeByte(0x00) .writeByte(0x64)
.toPacket(), .toPacket(),
); );
} }

View file

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

View file

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

View file

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

View file

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

46
deps.ts
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) => export const toHexString = (bytes: Uint8Array) =>
bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
export const log = {
export const s3 = new ApiFactory({ info: (...a) => {
credentials: { console.log("[INFO]", ...a);
awsAccessKeyId: Deno.env.get("S3_ACCESS_KEY_ID")!,
awsSecretKey: Deno.env.get("S3_SECRET_KEY")!,
}, },
fixedEndpoint: "https://s3.us-west-004.backblazeb2.com", warning: (...a) => {
region: "us-west-004", console.warn("[WARNING]", ...a);
}).makeNew(S3); },
critical: (...a) => {
console.error("[ERROR]", ...a);
},
debug: (...a) => {
if(config.debug) {
console.log("[DEBUG]", ...a)
}
}
}
export const config = { export const config = {
ops: Deno.env.get("OPS") ? JSON.parse(Deno.env.get("OPS")!) : [], ops: process.env.OPS ? JSON.parse(process.env.OPS!) : [],
port: +Deno.env.get("PORT")!, port: +process.env.PORT!,
hash: Deno.env.get("HASH"), hash: process.env.HASH,
onlineMode: Deno.env.get("ONLINEMODE") == "true", onlineMode: process.env.ONLINEMODE == "true",
main: process.env.MAIN || "main",
maxUsers: +(process.env.USERS || 24) > 255 ? 255 : +(process.env.USERS || 24),
software: process.env.SOFTWARE || "Custom Cla66ic",
name: process.env.NAME || "Cla66ic Server",
debug: process.env.DEBUG == "true",
}; };

View file

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

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

View file

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

View file

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