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

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;
}