Compare commits

..

No commits in common. "main" and "TCP" have entirely different histories.
main ... TCP

13 changed files with 218 additions and 295 deletions

5
.gitignore vendored
View file

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

BIN
bun.lockb

Binary file not shown.

View file

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

View file

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

View file

@ -5,10 +5,8 @@ import {
Plugin,
World,
} from "./classes.ts";
import { Socket, TCPSocketListener } from "bun"
import { config, log } from "../deps.ts";
import * as fs from "node:fs/promises"
import { config, crypto, log, s3, toHexString } from "../deps.ts";
type PlayerFunction = (a: Player) => void;
@ -18,7 +16,7 @@ interface PluginUpdateTime {
}
export class Server {
server!: TCPSocketListener;
server!: Deno.Listener;
players: Player[] = [];
plugins: Map<string, PluginUpdateTime> = new Map();
@ -30,73 +28,38 @@ export class Server {
[13, 65],
]);
maxUsers = config.maxUsers;
maxUsers = 69
worlds: World[] = [new World({ x: 64, y: 64, z: 64 }, config.main), new World({ x: 256, y: 64, z: 256 }, "large")];
worlds: World[] = [new World({ x: 64, y: 64, z: 64 }, "main")];
async start(port: number) {
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) {},
},
});
this.server = Deno.listen({ port: port });
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", ""));
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);
}
} catch {
await fs.mkdir("worlds")
}
});
if (config.onlineMode) {
setInterval(async () => {
@ -104,11 +67,11 @@ export class Server {
"https://www.classicube.net/heartbeat.jsp" +
`?port=${config.port}` +
`&max=${this.maxUsers}` +
`&name=${config.name}` +
"&name=Cla66ic" +
"&public=True" +
`&software=${config.software}` +
"&software=Cla66ic" +
`&version=7&salt=${config.hash}` +
`&users=${[...new Set(this.players.map((obj) => obj.ip))].length}`,
`&users=${this.players.length}`,
);
}, 10000);
}
@ -117,6 +80,9 @@ export class Server {
log.info(`Listening on port ${config.port}!`);
for await (const socket of this.server) {
this.startSocket(socket);
}
}
broadcast(text: string) {
@ -138,13 +104,13 @@ export class Server {
}
async updatePlugins() {
for await (const file of await fs.readdir("./plugins", {withFileTypes:true})) {
if (file.isFile()) {
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: (await fs.stat(`./plugins/${file.name}`)).mtime!,
lastUpdated: Deno.statSync(`./plugins/${file.name}`).mtime!,
plugin: new ((await import(`../plugins/${file.name}`)).default)(
this,
),
@ -153,12 +119,12 @@ export class Server {
const plugin = this.plugins.get(name);
if (
(await fs.stat(`./plugins/${file.name}`)).mtime!.getTime() !==
Deno.statSync(`./plugins/${file.name}`).mtime!.getTime() !==
plugin?.lastUpdated.getTime()
) {
plugin?.plugin.emit("stop");
this.plugins.set(name, {
lastUpdated: (await fs.stat(`./plugins/${file.name}`)).mtime!,
lastUpdated: Deno.statSync(`./plugins/${file.name}`).mtime!,
plugin:
new ((await import(`../plugins/${file.name}#` + Math.random()))
.default)(
@ -170,7 +136,7 @@ export class Server {
}
}
}
async removeUser(conn: Socket<{dataBuffer?: Buffer}>, text: string) {
removeUser(conn: Deno.Conn) {
const player = this.players.find((e) => e.socket == conn);
if (!player) return;
@ -178,14 +144,12 @@ export class Server {
this.players = this.players.filter((e) => e != player);
try {
conn.end();
conn.close();
} catch {
// whatever
}
this.broadcast(`${player.username} has &cleft&f, "${text}"`);
await this.worlds.find((e) => e.name == player.world)!.save();
this.broadcast(`${player.username} has &cleft`);
this.broadcastPacket(
(e) => PacketDefinitions.despawn(player.id, e),
@ -196,7 +160,7 @@ export class Server {
async handlePacket(
buffer: Uint8Array,
packetType: number,
connection: Socket<{dataBuffer?: Buffer}>,
connection: Deno.Conn,
) {
const packet = new PacketReader(buffer);
if (packetType == 0x00) {
@ -206,8 +170,9 @@ export class Server {
const verification = packet.readString();
if (this.players.length >= this.maxUsers) {
connection.end()
if(this.players.length >= this.maxUsers) {
connection.close();
return;
}
@ -219,25 +184,31 @@ export class Server {
);
if (!verification) {
player.socket.end();
player.socket.close();
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 (
hasher.digest("hex") !== verification
str !== verification
) {
await PacketDefinitions.disconnect(
"Refresh your playerlist! Incorrect hash!",
player,
);
player.socket.end();
player.socket.close();
return true;
}
@ -248,7 +219,7 @@ export class Server {
"Your name is already being used!",
player,
);
player.socket.end();
player.socket.close();
return true;
}
@ -258,10 +229,9 @@ 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);
if (!player) return;
const player = this.players.find((e) => e.socket == connection)!;
packet.readSByte();
packet.readByte();
player.position.x = packet.readShort();
player.position.y = packet.readShort();
player.position.z = packet.readShort();
@ -276,8 +246,7 @@ export class Server {
} else if (packetType == 0x0d) {
packet.readByte();
const player = this.players.find((e) => e.socket == connection);
if (!player) return;
const player = this.players.find((e) => e.socket == connection)!;
const message = packet.readString();
let playerColor = "[member] &b";
@ -301,21 +270,19 @@ export class Server {
}
this.broadcast(`${playerColor}${player.username}&f: ${message}`);
} else if (packetType == 0x05) {
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 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);
if (!world) return;
const world = this.worlds.find((e) => e.name == player.world)!;
let pluginAnswer: boolean[] = [];
@ -338,4 +305,51 @@ 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,15 +1,14 @@
import { decode, encode } from "cbor-x";
import { gzip, ungzip } from "https://cdn.skypack.dev/pako";
import { s3 } from "../deps.ts";
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
metadata: any = {};
optionalJson: any = {};
constructor(size: Position, name: string) {
this.size = size;
@ -34,7 +33,7 @@ export class World {
}
findID(block: number): Position[] {
const position: 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++) {
@ -73,37 +72,39 @@ export class World {
async delete() {
try {
await unlink(`worlds/${this.name}.buf`)
await s3.deleteObject({
Bucket: "cla66ic",
Key: this.name + ".buf",
});
} catch {
// gang
// doesn't exist, probably..
}
}
private async load() {
try {
const ungziped = Bun.gunzipSync(
await readFile(`worlds/${this.name}.buf`)
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;
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: this.metadata.x!,
y: this.metadata.y!,
z: this.metadata.z!,
x: +head.Metadata.x!,
y: +head.Metadata.y!,
z: +head.Metadata.z!,
};
this.data = ungziped.slice(cborSize+4);
this.data = ungziped;
this.dataView = new DataView(this.data.buffer);
if(4 + this.size.x * this.size.y * this.size.z == this.data.length) {
console.log('[WARNING] Encoding was wrong somewhere!')
}
} catch(e) {
this.optionalJson = JSON.parse(head.Metadata.json || "{}");
} catch {
const layers = Math.floor(this.size.y / 2);
for (let i = 0; i < layers; i += 1) {
@ -117,18 +118,16 @@ export class World {
}
async save() {
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)!);
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),
},
});
}
}

View file

@ -1,4 +1,4 @@
import EventEmitter from "./../events";
import { EventEmitter } from "../deps.ts";
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,
): Promise<boolean> | boolean;
): boolean;
stop(): void;
}> {

46
deps.ts
View file

@ -1,29 +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 "./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 log = {
info: (...a) => {
console.log("[INFO]", ...a);
export const s3 = new ApiFactory({
credentials: {
awsAccessKeyId: Deno.env.get("S3_ACCESS_KEY_ID")!,
awsSecretKey: Deno.env.get("S3_SECRET_KEY")!,
},
warning: (...a) => {
console.warn("[WARNING]", ...a);
},
critical: (...a) => {
console.error("[ERROR]", ...a);
},
debug: (...a) => {
if(config.debug) {
console.log("[DEBUG]", ...a)
}
}
}
fixedEndpoint: "https://s3.us-west-004.backblazeb2.com",
region: "us-west-004",
}).makeNew(S3);
export const config = {
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",
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",
};

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: Timer | null;
let timeoutId: number | null;
let listener = (...args: any[]) => {
if (timeoutId !== null) clearTimeout(timeoutId);

View file

@ -1,12 +0,0 @@
{
"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 { PacketWriter, Player, Plugin } from "../classes/classes.ts";
import { Plugin } from "../classes/classes.ts";
import { Server } from "../classes/Server.ts";
import { config } from "../deps.ts";
@ -7,36 +7,14 @@ 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, args) => {
this.on("command", async (command, player) => {
if (command == "help") {
let allComamnds = "";
for (const [_k, v] of server.plugins) {
@ -60,41 +38,6 @@ 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,7 +7,6 @@ export default class CommandPlugin extends Plugin {
"g",
"worlds",
"world",
"main",
];
constructor(server: Server) {
@ -16,8 +15,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.metadata?.builders?.includes("*")) {
if (!world.metadata?.builders?.includes(player.username)) {
if (!world.optionalJson?.builders?.includes("*")) {
if (!world.optionalJson?.builders?.includes(player.username)) {
player.message("You are %cnot allowed &fto build in this world!");
return true;
}
@ -26,19 +25,10 @@ export default class CommandPlugin extends Plugin {
return false;
});
this.on("command", async (command, player, args) => {
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();
if (command == "g") {
const requestedWorld = server.worlds.find((e) =>
e.name.toLowerCase() == args.join(" ").toLowerCase()
);
if (requestedWorld) {
player.toWorld(requestedWorld);
} else {
@ -59,8 +49,8 @@ export default class CommandPlugin extends Plugin {
{ x: 64, y: 64, z: 64 },
player.username,
);
world.metadata.builders = [];
world.metadata.builders.push(player.username);
world.optionalJson.builders = [];
world.optionalJson.builders.push(player.username);
server.worlds.push(world);
player.message(`&aWorld created!&f Use /g ${player.username}!`);
@ -105,11 +95,11 @@ export default class CommandPlugin extends Plugin {
return;
}
if (!world.metadata?.builders) world.metadata.builders = [];
if (!world.optionalJson?.builders) world.optionalJson.builders = [];
if (subcategory == "add") {
const username = args[2];
world.metadata.builders.push(username);
world.optionalJson.builders.push(username);
player.message(
`&a${username}&f sucesfully added as a builder to world &a${world.name}!`,
);
@ -117,13 +107,13 @@ export default class CommandPlugin extends Plugin {
} else if (subcategory == "remove") {
const username = args[2];
const before = world.metadata.builders.length;
const before = world.optionalJson.builders.length;
world.metadata.builders = world.metadata.builders.filter((
world.optionalJson.builders = world.optionalJson.builders.filter((
e: string,
) => e !== username);
const after = world.metadata.builders.length;
const after = world.optionalJson.builders.length;
player.message(
`Removed &a${
@ -134,7 +124,7 @@ export default class CommandPlugin extends Plugin {
} else if (subcategory == "list") {
player.message(
`&a${world.name}&f's builders: &a${
world.metadata.builders.join(", ")
world.optionalJson.builders.join(", ")
}`,
);
} else {

View file

@ -8,27 +8,28 @@
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. Bun!! It's not node, and a classic server
6. very fast
5. DENO!! It's not node, and a classic server.
### setup tutorial
### setup tutorial (be warned it's not the easiest)
1. configure .env file to look something like
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
MAIN=main
HOST=0.0.0.0
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
2. install bun
3. run `bun index.ts`
3. install deno
4. run `deno run --allow-env --allow-net --allow-read index.ts`
### insipration taken from:
@ -38,13 +39,12 @@ environment variables
### issues:
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)
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)
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
4. no IP cooldown connections (no block cooldown either), no anticheat, no unique IP heartbeats
5. proper rank support (implemented as plugin)
6. no discord bridge (implemented as plugin)
7. no cla66ic/plugins repository