From d40e90240c545d1619479a108065bba261ed2a97 Mon Sep 17 00:00:00 2001 From: yourfriend Date: Fri, 3 Jun 2022 15:36:34 +0300 Subject: [PATCH] format everything + fix plugin events --- classes/Packets.ts | 1 - classes/Player.ts | 7 +- classes/Server.ts | 27 ++++-- classes/classes.ts | 3 +- deps.ts | 4 +- events.ts | 231 ++++++++++++++++++++++++++++++++++++++++++++ plugins/commands.ts | 2 +- plugins/world.ts | 14 +-- readme.md | 28 ++++-- 9 files changed, 280 insertions(+), 37 deletions(-) create mode 100644 events.ts diff --git a/classes/Packets.ts b/classes/Packets.ts index 638c19d..cd0d4ab 100644 --- a/classes/Packets.ts +++ b/classes/Packets.ts @@ -150,7 +150,6 @@ export class PacketDefinitions { await PacketDefinitions.levelFinish(world.size, player); await PacketDefinitions.spawn(player, -1, player); - } static async levelFinish(size: Position, player: Player) { await player.writeToSocket( diff --git a/classes/Player.ts b/classes/Player.ts index 9a7b33d..4d3aabd 100644 --- a/classes/Player.ts +++ b/classes/Player.ts @@ -28,7 +28,9 @@ export class Player { // reassigns ID until finds available one // if we reach 255 players this will loop forever - while(server.players.find(e => e.id == id)) id = Math.floor(Math.random() * 255); + while (server.players.find((e) => e.id == id)) { + id = Math.floor(Math.random() * 255); + } this.id = id; } @@ -63,8 +65,7 @@ export class Player { PacketDefinitions.sendPackets(this, world); this.server.broadcastPacket( - (e) => - PacketDefinitions.spawn(this, this.id, e), + (e) => PacketDefinitions.spawn(this, this.id, e), this, ); this.server.broadcastPacket( diff --git a/classes/Server.ts b/classes/Server.ts index 9b8b1ad..89efb6d 100644 --- a/classes/Server.ts +++ b/classes/Server.ts @@ -139,9 +139,12 @@ export class Server { this.broadcast(`${player.username} has &cleft`); this.worlds.find((e) => e.name == player.world)!.save(); - this.broadcastPacket((e) => PacketDefinitions.despawn(player.id, e), player); + this.broadcastPacket( + (e) => PacketDefinitions.despawn(player.id, e), + player, + ); } - + async handlePacket(packet: PacketReader, connection: Deno.Conn) { const packetType = packet.readByte(); if (packetType == 0x00) { @@ -258,18 +261,25 @@ export class Server { const world = this.worlds.find((e) => e.name == player.world)!; - const before = world.getBlock(position); + let pluginAnswer: boolean[] = []; - this.worlds.find((e) => e.name == player.world)!.setBlock(position, id); + for await (const [_k, v] of this.plugins) { + pluginAnswer = pluginAnswer.concat( + await v.plugin.emit("setblock", player, mode, id, position), + ); + } + + if (pluginAnswer.some((e) => e == true)) { + PacketDefinitions.setBlock(position, world.getBlock(position), player); + return; + } + + 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. @@ -298,7 +308,6 @@ export class Server { } else { const packet = new PacketReader(buffer.subarray(0, count)); this.handlePacket(packet, connection); - } } } diff --git a/classes/classes.ts b/classes/classes.ts index b0693ea..c77412c 100644 --- a/classes/classes.ts +++ b/classes/classes.ts @@ -29,8 +29,7 @@ export abstract class Plugin extends EventEmitter<{ mode: number, id: number, position: Position, - blockBefore: number, - ): void; + ): boolean; stop(): void; }> { diff --git a/deps.ts b/deps.ts index 5e9d344..98647eb 100644 --- a/deps.ts +++ b/deps.ts @@ -5,7 +5,7 @@ import { S3 } from "https://aws-api.deno.dev/v0.3/services/s3.ts"; export * as log from "https://deno.land/std@0.136.0/log/mod.ts"; export { crypto } from "https://deno.land/std@0.136.0/crypto/mod.ts"; -export { EventEmitter } from "https://deno.land/x/eventemitter@1.2.1/mod.ts"; +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"), ""); @@ -23,5 +23,5 @@ 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" + onlineMode: Deno.env.get("ONLINEMODE") == "true", }; diff --git a/events.ts b/events.ts new file mode 100644 index 0000000..6526a7a --- /dev/null +++ b/events.ts @@ -0,0 +1,231 @@ +// deno-lint-ignore-file + +/** The callback type. */ +type Callback = (...args: any[]) => any | Promise; + +/** A listener type. */ +type Listener = Callback & { __once__?: true }; + +/** The name of an event. */ +type EventName = string | number; + +type EventsType = + & { [key: string]: Callback } + & { [key: number]: Callback }; + +/** + * The event emitter. + */ +export class EventEmitter { + /** + * This is where the events and listeners are stored. + */ + private _events_: Map> = new Map(); + + /** + * Listen for a typed event. + * @param event The typed event name to listen for. + * @param listener The typed listener function. + */ + public on(event: K, listener: E[K]): this; + + /** + * Listen for an event. + * @param event The event name to listen for. + * @param listener The listener function. + */ + public on(event: EventName, listener: Callback): this { + if (!this._events_.has(event)) this._events_.set(event, new Set()); + this._events_.get(event)!.add(listener); + return this; + } + + /** + * Listen for a typed event once. + * @param event The typed event name to listen for. + * @param listener The typed listener function. + */ + public once(event: K, listener: E[K]): this; + + /** + * Listen for an event once. + * @param event The event name to listen for. + * @param listener The listener function. + */ + public once(event: EventName, listener: Callback): this { + const l: Listener = listener; + l.__once__ = true; + return this.on(event, l as any); + } + + /** + * Remove a specific listener in the event emitter on a specific + * typed event. + * @param event The typed event name. + * @param listener The typed event listener function. + */ + public off(event: K, listener: E[K]): this; + + /** + * Remove all listeners on a specific typed event. + * @param event The typed event name. + */ + public off(event: K): this; + + /** + * Remove all events from the event listener. + */ + public off(): this; + + /** + * Remove a specific listener on a specific event if both `event` + * and `listener` is defined, or remove all listeners on a + * specific event if only `event` is defined, or lastly remove + * all listeners on every event if `event` is not defined. + * @param event The event name. + * @param listener The event listener function. + */ + public off(event?: EventName, listener?: Callback): this { + if (!event && listener) { + throw new Error("Why is there a listener defined here?"); + } else if (!event && !listener) { + this._events_.clear(); + } else if (event && !listener) { + this._events_.delete(event); + } else if (event && listener && this._events_.has(event)) { + const _ = this._events_.get(event)!; + _.delete(listener); + if (_.size === 0) this._events_.delete(event); + } else { + throw new Error("Unknown action!"); + } + return this; + } + + /** + * Emit a typed event without waiting for each listener to + * return. + * @param event The typed event name to emit. + * @param args The arguments to pass to the typed listeners. + */ + public emitSync(event: K, ...args: Parameters): this; + + /** + * Emit an event without waiting for each listener to return. + * @param event The event name to emit. + * @param args The arguments to pass to the listeners. + */ + public emitSync(event: EventName, ...args: Parameters): this { + if (!this._events_.has(event)) return this; + const _ = this._events_.get(event)!; + for (let [, listener] of _.entries()) { + const r = listener(...args); + if (r instanceof Promise) r.catch(console.error); + if (listener.__once__) { + delete listener.__once__; + _.delete(listener); + } + } + if (_.size === 0) this._events_.delete(event); + return this; + } + + /** + * Emit a typed event and wait for each typed listener to return. + * @param event The typed event name to emit. + * @param args The arguments to pass to the typed listeners. + */ + public async emit( + event: K, + ...args: Parameters + ): Promise; + + /** + * Emit an event and wait for each listener to return. + * @param event The event name to emit. + * @param args The arguments to pass to the listeners. + */ + public async emit( + event: EventName, + ...args: Parameters + ): Promise { + let returns: any[] = []; + + if (!this._events_.has(event)) return returns; + const _ = this._events_.get(event)!; + for (let [, listener] of _.entries()) { + try { + returns.push(await listener(...args)); + if (listener.__once__) { + delete listener.__once__; + _.delete(listener); + } + } catch (error) { + console.error(error); + } + } + if (_.size === 0) this._events_.delete(event); + return returns; + } + + /** + * The same as emitSync, but wait for each typed listener to + * return before calling the next typed listener. + * @param event The typed event name. + * @param args The arguments to pass to the typed listeners. + */ + public queue(event: K, ...args: Parameters): this; + + /** + * The same as emitSync, but wait for each listener to return + * before calling the next listener. + * @param event The event name. + * @param args The arguments to pass to the listeners. + */ + public queue(event: EventName, ...args: Parameters): this { + (async () => await this.emit(event, ...args as any))().catch(console.error); + return this; + } + + /** + * Wait for a typed event to be emitted and return the arguments. + * @param event The typed event name to wait for. + * @param timeout An optional amount of milliseconds to wait + * before throwing. + */ + public pull( + event: K, + timeout?: number, + ): Promise>; + /** + * Wait for an event to be emitted and return the arguments. + * @param event The event name to wait for. + * @param timeout An optional amount of milliseconds to wait + * before throwing. + */ + public pull( + event: EventName, + timeout?: number, + ): Promise> { + return new Promise(async (resolve, reject) => { + let timeoutId: number | null; + + let listener = (...args: any[]) => { + if (timeoutId !== null) clearTimeout(timeoutId); + resolve(args); + }; + + timeoutId = typeof timeout !== "number" + ? null + : setTimeout(() => (this.off(event, listener as any), + reject( + new Error("Timed out!"), + )) + ); + + this.once(event, listener as any); + }); + } +} + +export default EventEmitter; diff --git a/plugins/commands.ts b/plugins/commands.ts index b58e8bb..6acd659 100644 --- a/plugins/commands.ts +++ b/plugins/commands.ts @@ -13,7 +13,7 @@ export default class CommandPlugin extends Plugin { super(); this.server = server; - + this.on("command", async (command, player) => { if (command == "help") { let allComamnds = ""; diff --git a/plugins/world.ts b/plugins/world.ts index 18096a5..1d396cd 100644 --- a/plugins/world.ts +++ b/plugins/world.ts @@ -1,4 +1,4 @@ -import { PacketDefinitions, Plugin, World } from "../classes/classes.ts"; +import { Plugin, World } from "../classes/classes.ts"; import { Server } from "../classes/Server.ts"; import { config } from "../deps.ts"; @@ -13,20 +13,16 @@ export default class CommandPlugin extends Plugin { super(); this.server = server; - this.on("setblock", (player, _mode, _id, position, blockBefore) => { + 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)) { player.message("You are %cnot allowed &fto build in this world!"); - world.setBlock(position, blockBefore); - - server.players.forEach(async (e) => { - if (e.world == player.world) { - await PacketDefinitions.setBlock(position, blockBefore, e); - } - }); + return true; } } + + return false; }); this.on("command", async (command, player, args) => { if (command == "g") { diff --git a/readme.md b/readme.md index 01a5b28..2aaf067 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,9 @@ # cla66ic -## sucessor of cla55ic +## sucessor of cla55ic ### features: + 1. written in typescript (types and shit) 2. entierly cloud based (meaning you can host it anywhere) 3. extremely extensive plugin system @@ -10,8 +11,10 @@ 5. DENO!! It's not node, and a classic server. ### setup tutorial (be warned it's not the easiest) + 1. make a backblaze b2 account, make a bucket, and get your keys from the bucket 2. configure .env file to look something like + ``` PORT=6969 HASH=RandomHashIlIke @@ -21,19 +24,24 @@ ONLINEMODE=true S3_ACCESS_KEY_ID="MyAccessKey" S3_SECRET_KEY="SecretKey" ``` -NOTE: if you are running inside of a cloud provider, just set these as -your environment variables -3. install deno +NOTE: if you are running inside of a cloud provider, just set these as your +environment variables + +3. install deno 4. run `deno run --allow-env --allow-net --allow-read index.ts` + ### insipration taken from: + 1. mcgalaxy (obviuouuusly!!) -2. https://github.com/Patbox/Cobblestone-Classic (some protocol information and worldhandling) +2. https://github.com/Patbox/Cobblestone-Classic (some protocol information and + worldhandling) 3. cla55ic (world data too) ### issues: -1. plugin system event handling is lackluster in some palces -2. tcp packet splitting fails sometimes -3. no cpe support! i want to get all of the above issues fixed before implementing CPE support -4. proper rank support (implemented as plugin) -5. no discord bridge (implemented as plugin) + +1. tcp packet splitting fails sometimes +2. no cpe support! i want to get all of the above issues fixed before + implementing CPE support +3. proper rank support (implemented as plugin) +4. no discord bridge (implemented as plugin)