I actually wrote a package manager but then my shit got fucked and I lost motivation to do it again but now I have motivation again (thanks modern medicine)
This commit is contained in:
Dinhero21 2024-08-30 17:34:35 -03:00
parent 1a58d1c126
commit cc9c274325
30 changed files with 1569 additions and 2604 deletions

View file

@ -1,36 +0,0 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": [
"standard-with-typescript",
"plugin:require-extensions/recommended"
],
"plugins": [
"require-extensions"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "tsconfig.json"
},
"rules": {
"import/order": [
"error",
{
"groups": [
"type",
"index",
"sibling",
"parent",
"internal",
"builtin",
"external",
"object"
]
}
],
"no-case-declarations": "off"
}
}

3
.prettierrc Normal file
View file

@ -0,0 +1,3 @@
{
"singleQuote": true
}

6
.swcrc
View file

@ -14,11 +14,7 @@
"target": "esnext", "target": "esnext",
"loose": true "loose": true
}, },
"exclude": [ "exclude": ["\\.js$", "\\.d\\.ts$", "node_modules"],
"\\.js$",
"\\.d\\.ts$",
"node_modules"
],
"module": { "module": {
"type": "es6" "type": "es6"
}, },

View file

@ -1,5 +0,0 @@
{
"files.exclude": {
"src/module/*": false
}
}

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Dinhero21
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,4 +1,5 @@
# MMP # MMP
- Modular Minecraft Proxy - Modular Minecraft Proxy
# How does this differ from other proxies (such as SMP)? # How does this differ from other proxies (such as SMP)?
@ -13,7 +14,7 @@ No more cleint, sever or posiotin!
For every client that connects a new thread is created. For every client that connects a new thread is created.
While also offering minimal performance gains (as there probably isn't (and shouldn't) be ever more then one client) makes it so that program instances client-based instead of being shared between all clients. While also offering minimal performance gains (as there probably isn't (and shouldn't) be ever more then one client) makes it so that program instances client-based instead of being shared between all clients.
This makes a Singleton-like architecture possible without needing to worry about multiple instances of the same plugin and explicit declaration of dependencies. This makes a Singleton-like architecture possible without needing to worry about multiple instances of the same plugin and explicit declaration of dependencies.

6
TODO.md Normal file
View file

@ -0,0 +1,6 @@
- [x] TypeScript
- [x] Client
- [x] Server
- [x] Packet Forwarding
- [x] Module Loading
- [ ] Module Manager

46
eslint.config.mjs Normal file
View file

@ -0,0 +1,46 @@
// @ts-check
import eslint from '@eslint/js';
import prettier from 'eslint-config-prettier';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ languageOptions: { globals: globals.browser } },
eslint.configs.recommended,
...tseslint.configs.recommended,
{
plugins: {
'simple-import-sort': simpleImportSort,
},
rules: {
'simple-import-sort/exports': 'off',
'simple-import-sort/imports': 'error',
'no-case-declarations': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-constant-condition': ['error', { checkLoops: false }],
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'object-shorthand': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
},
languageOptions: {
parserOptions: {
project: './tsconfig.json',
},
},
},
prettier,
);

3176
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,47 +1,44 @@
{ {
"name": "modular-minecraft-proxy", "name": "modular-minecraft-proxy",
"version": "1.0.0", "version": "1.0.0",
"description": "Lego Minecraft", "description": "Modular Minecraft Proxy",
"main": "index.js", "main": "index.js",
"type": "module",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build": "scripty", "build": "scripty",
"watch:build": "scripty", "watch:build": "scripty",
"copy": "scripty", "lint": "scripty",
"watch:copy": "scripty",
"run": "scripty", "run": "scripty",
"watch:run": "scripty", "watch:run": "scripty",
"watch": "scripty" "watch": "scripty"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "Dinhero Development Group",
"license": "MIT", "license": "MIT",
"type": "module",
"config": { "config": {
"scripty": { "scripty": {
"parallel": true, "parallel": true,
"path": "script" "path": "script"
} }
}, },
"devDependencies": {
"@swc/cli": "^0.1.63",
"@swc/core": "^1.3.102",
"@types/node": "^20.16.2",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"eslint": "^8.56.0",
"eslint-config-standard-with-typescript": "^43.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.6.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-require-extensions": "^0.1.3",
"nodemon": "^3.0.2",
"typescript": "^5.3.3"
},
"dependencies": { "dependencies": {
"minecraft-protocol": "^1.47.0"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.7.22",
"@swc/helpers": "^0.5.12", "@swc/helpers": "^0.5.12",
"chalk": "^5.3.0", "@types/node": "^22.5.1",
"minecraft-protocol": "^1.26.5", "eslint": "^9.9.1",
"scripty": "^2.1.1" "eslint-config-prettier": "^9.1.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^15.9.0",
"nodemon": "^3.1.4",
"prettier": "^3.3.3",
"scripty": "^2.1.1",
"typescript": "^5.5.4",
"typescript-eslint": "^8.3.0"
} }
} }

View file

@ -1,3 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
swc src -d dist $@ # --delete-dir-on-start breaks nodemon
swc src -d dist --strip-leading-paths $@

View file

@ -1,11 +0,0 @@
#!/usr/bin/env bash
SOURCE="$1"
: ${SOURCE:=src}
TARGET="$2"
: ${TARGET:=dist}
rsync -rav --exclude="*.ts" $SOURCE/ $TARGET/

4
script/lint.sh Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
eslint --fix src
prettier --write src

View file

@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/usr/bin/env bash
node dist/index.js node dist/index.js $@

View file

@ -1,3 +0,0 @@
./script/copy.sh
nodemon --watch src --exec "./script/copy.sh $@"

View file

@ -1,19 +1,19 @@
import Instance from './instance/index.js' import { createServer } from 'minecraft-protocol';
import { SERVER_OPTIONS } from './settings.js'
import { createServer } from 'minecraft-protocol' import Instance from './instance/index.js';
import { SERVER_OPTIONS } from './settings.js';
// See: https://nodejs.org/api/worker_threads.html#considerations-when-transferring-typedarrays-and-buffers // See: https://nodejs.org/api/worker_threads.html#considerations-when-transferring-typedarrays-and-buffers
Object.assign(Uint8Array.prototype, Buffer.prototype) Object.assign(Uint8Array.prototype, Buffer.prototype);
export const server = createServer({ export const server = createServer({
'online-mode': false, 'online-mode': false,
...SERVER_OPTIONS, ...SERVER_OPTIONS,
keepAlive: false keepAlive: false,
}) });
server.on('login', client => { server.on('login', (client) => {
console.info(`${client.username} has connected!`) console.info(`${client.username} has connected!`);
// eslint-disable-next-line no-new new Instance(client);
new Instance(client) });
})

View file

@ -1,140 +1,147 @@
import { type Message } from '../worker/parent.js' import chalk from 'chalk';
import { importModulesGenerator } from '../util/import-modules.js' import { createClient, type ServerClient } from 'minecraft-protocol';
import { Channel } from '../util/channel.js' import { dirname, resolve } from 'path';
import { TARGET_OPTIONS } from '../settings.js' import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path' import { Worker } from 'worker_threads';
import { fileURLToPath } from 'url'
import { Worker } from 'worker_threads' import { TARGET_OPTIONS } from '../settings.js';
import { createClient, type ServerClient } from 'minecraft-protocol' import { Channel } from '../util/channel.js';
import chalk from 'chalk' import { importModulesGenerator } from '../util/import-modules.js';
import { type Message } from '../worker/parent.js';
if (!('require' in globalThis)) { if (!('require' in globalThis)) {
globalThis.__filename = fileURLToPath(import.meta.url) globalThis.__filename = fileURLToPath(import.meta.url);
globalThis.__dirname = dirname(__filename) globalThis.__dirname = dirname(__filename);
} }
const WORKER_PATH = resolve(__dirname, '../worker') const WORKER_PATH = resolve(__dirname, '../worker');
const MODULE_DIR_PATH = resolve(__dirname, '../module') const MODULE_DIR_PATH = resolve(__dirname, '../module');
export class Instance { export class Instance {
public readonly client public readonly client;
public readonly server public readonly server;
public readonly worker public readonly worker;
constructor (client: ServerClient) { constructor(client: ServerClient) {
this.client = client this.client = client;
const target = createClient({ const target = createClient({
auth: 'offline', auth: 'offline',
username: client.username, username: client.username,
...TARGET_OPTIONS, ...TARGET_OPTIONS,
keepAlive: false keepAlive: false,
}) });
this.server = target this.server = target;
target.on('error', error => { target.on('error', (error) => {
console.error('Target error:', error) console.error('Target error:', error);
}) });
console.info('Initializing worker... (local context)') console.info('Initializing worker... (local context)');
const start = performance.now() const start = performance.now();
const worker = new Worker(WORKER_PATH) const worker = new Worker(WORKER_PATH);
this.worker = worker this.worker = worker;
worker.on('online', () => { worker.on('online', () => {
const end = performance.now() const end = performance.now();
const delta = end - start const delta = end - start;
console.info(`Worker online! took ${delta.toFixed(2)}ms`) console.info(`Worker online! took ${delta.toFixed(2)}ms`);
}) });
worker.on('error', error => { worker.on('error', (error) => {
console.error('Worker error:', error) console.error('Worker error:', error);
}) });
void this._importModules() void this._importModules();
} }
private async _importModules (): Promise<void> { private async _importModules(): Promise<void> {
console.group('Loading modules... (global)') console.group('Loading modules... (global)');
const start = performance.now() const start = performance.now();
let moduleStart = NaN let moduleStart = NaN;
for await (const module of importModulesGenerator( for await (const module of importModulesGenerator(
MODULE_DIR_PATH, MODULE_DIR_PATH,
'global.js', 'global.js',
{ {
pre (entry) { pre(entry) {
const now = performance.now() const now = performance.now();
moduleStart = now moduleStart = now;
const module = entry.name const module = entry.name;
console.group(`Loading ${module}...`) console.group(`Loading ${module}...`);
}, },
post (entry) { post(_entry) {
const now = performance.now() const now = performance.now();
const delta = now - moduleStart const delta = now - moduleStart;
console.groupEnd() console.groupEnd();
console.info(`took ${delta.toPrecision(2)}ms`) console.info(`took ${delta.toPrecision(2)}ms`);
}, },
error (error, entry) { error(error, entry) {
const module = entry.name const module = entry.name;
error.stack += `\n while loading module ${JSON.stringify(module)} (local)` error.stack += `\n while loading module ${JSON.stringify(
module,
)} (local)`;
console.error(chalk.red(error.stack)) console.error(chalk.red(error.stack));
} },
} },
)) { )) {
if (module === null) throw new Error('Expected module not to be null') if (module === null) throw new Error('Expected module not to be null');
if (typeof module !== 'object') throw new Error('Expected module to be an object') if (typeof module !== 'object')
throw new Error('Expected module to be an object');
if (!('default' in module)) throw new Error('Expected default export') if (!('default' in module)) throw new Error('Expected default export');
const f = module.default const f = module.default;
if (typeof f !== 'function') throw new Error('Expected default export to be a function') if (typeof f !== 'function')
throw new Error('Expected default export to be a function');
await (f as (instance: Instance) => Promise<void>)(this) await (f as (instance: Instance) => Promise<void>)(this);
} }
const end = performance.now() const end = performance.now();
const delta = end - start const delta = end - start;
console.groupEnd() console.groupEnd();
console.info(`took ${delta.toFixed(2)}ms`) console.info(`took ${delta.toFixed(2)}ms`);
} }
protected postMessage (channel: string, data: any): void { protected postMessage(channel: string, data: any): void {
this.worker.postMessage({ this.worker.postMessage({
channel, channel,
data data,
} satisfies Message) } satisfies Message);
} }
public createChannel<TSend, TReceive = TSend> (id: string): Channel<TSend, TReceive> { public createChannel<TSend, TReceive = TSend>(
const channel = new Channel<TSend, TReceive>(id) id: string,
): Channel<TSend, TReceive> {
const channel = new Channel<TSend, TReceive>(id);
channel._subscribe(data => { channel._subscribe((data) => {
this.postMessage(id, data) this.postMessage(id, data);
}) });
this.worker.on('message', (message: Message<TReceive>) => { this.worker.on('message', (message: Message<TReceive>) => {
if (message.channel !== id) return if (message.channel !== id) return;
channel._write(message.data) channel._write(message.data);
}) });
return channel return channel;
} }
} }
export default Instance export default Instance;

View file

@ -1,122 +1,125 @@
import type Instance from '../../instance/index.js' import { states } from 'minecraft-protocol';
import { type Message } from './shared.js'
import { type RawPacket } from '../../util/packet.js' import type Instance from '../../instance/index.js';
import { states } from 'minecraft-protocol' import { type RawPacket } from '../../util/packet.js';
import { type Message } from './shared.js';
export default async function (instance: Instance): Promise<void> { export default async function (instance: Instance): Promise<void> {
const clientQueue: RawPacket[] = [] const clientQueue: RawPacket[] = [];
const serverQueue: RawPacket[] = [] const serverQueue: RawPacket[] = [];
const channel = instance.createChannel<Message>('proxy') const channel = instance.createChannel<Message>('proxy');
const client = instance.client const client = instance.client;
const server = instance.server const server = instance.server;
const worker = instance.worker const worker = instance.worker;
client.on('end', reason => { client.on('end', (reason) => {
console.info('Client disconnected:', reason) console.info('Client disconnected:', reason);
server.end(reason) server.end(reason);
void worker.terminate() void worker.terminate();
}) });
server.on('end', reason => { server.on('end', (reason) => {
console.info('Target disconnected:', reason) console.info('Server disconnected:', reason);
client.end(reason) client.end(reason);
void worker.terminate() void worker.terminate();
}) });
client.on('error', error => { client.on('error', (error) => {
console.error('Client error:', error) console.error('Client error:', error);
}) });
server.on('error', error => { server.on('error', (error) => {
console.error('Target error:', error) console.error('Server error:', error);
}) });
client.on('packet', (data, meta) => { client.on('packet', (data, meta) => {
if (meta.state !== states.PLAY) return // if (meta.state !== states.PLAY) return;
channel.write({ channel.write({
side: 'client', direction: 'downstream',
packet: { packet: {
name: meta.name, ...meta,
data data,
} },
}) });
}) });
server.on('packet', (data, meta) => { server.on('packet', (data, meta) => {
if (meta.state !== states.PLAY) return // if (meta.state !== states.PLAY) return;
channel.write({ channel.write({
side: 'server', direction: 'upstream',
packet: { packet: {
name: meta.name, ...meta,
data data,
} },
}) });
}) });
client.on('state', state => { // Flush packet queue on play state
if (state !== states.PLAY) return
const queue = clientQueue client.on('state', (state) => {
if (state !== states.PLAY) return;
for (const packet of queue) client.write(packet.name, packet.data) const queue = clientQueue;
queue.length = 0 for (const packet of queue) client.write(packet.name, packet.data);
})
server.on('state', state => { queue.length = 0;
if (state !== states.PLAY) return });
const queue = serverQueue server.on('state', (state) => {
if (state !== states.PLAY) return;
for (const packet of queue) server.write(packet.name, packet.data) const queue = serverQueue;
queue.length = 0 for (const packet of queue) server.write(packet.name, packet.data);
})
channel.subscribe(({ side, packet }) => { queue.length = 0;
});
channel.subscribe(({ direction: side, packet }) => {
switch (side) { switch (side) {
case 'client': case 'downstream':
writeClientPacket(packet) writeClientPacket(packet);
break break;
case 'server': case 'upstream':
writeServerPacket(packet) writeServerPacket(packet);
break break;
default: default:
throw new Error(`Invalid side: ${side as any}`) throw new Error(`Invalid side: ${side as any}`);
} }
}) });
function writeClientPacket (packet: RawPacket): void { function writeClientPacket(packet: RawPacket): void {
// wait until play state
if (client.state !== states.PLAY) { if (client.state !== states.PLAY) {
clientQueue.push(packet) clientQueue.push(packet);
return return;
} }
client.write( if (packet.state !== states.PLAY) return;
packet.name,
packet.data client.write(packet.name, packet.data);
)
} }
function writeServerPacket (packet: RawPacket): void { function writeServerPacket(packet: RawPacket): void {
// wait until play state
if (server.state !== states.PLAY) { if (server.state !== states.PLAY) {
serverQueue.push(packet) serverQueue.push(packet);
return return;
} }
server.write( if (packet.state !== states.PLAY) return;
packet.name,
packet.data server.write(packet.name, packet.data);
)
} }
} }

View file

@ -1,60 +1,75 @@
import { type Side, type Message } from './shared.js' import type { States } from 'minecraft-protocol';
import { PublicEventHandler } from '../../util/events.js' import { states } from 'minecraft-protocol';
import { createChannel } from '../../worker/parent.js'
import { Packet, type RawPacket } from '../../util/packet.js'
import { type AsyncVoid } from '../../util/types.js'
export type PacketEventMap = Record<string, (packet: Packet) => AsyncVoid> import { PublicEventHandler } from '../../util/events.js';
import { Packet, type RawPacket } from '../../util/packet.js';
import { type AsyncVoid } from '../../util/types.js';
import { createChannel } from '../../worker/parent.js';
import { type Direction as Direction, type Message } from './shared.js';
export type PacketEventMap = Record<string, (packet: Packet) => AsyncVoid>;
// ? Should I export the channel // ? Should I export the channel
export const channel = createChannel<Message>('proxy') export const channel = createChannel<Message>('proxy');
function write (side: Side, packet: RawPacket): void { function write(direction: Direction, packet: RawPacket): void {
channel.write({ channel.write({
side, direction,
packet packet,
}) });
} }
export const proxy = { export const proxy = {
client: new PublicEventHandler<PacketEventMap>(), client: new PublicEventHandler<PacketEventMap>(),
server: new PublicEventHandler<PacketEventMap>(), server: new PublicEventHandler<PacketEventMap>(),
writeClient (name: string, data: unknown): void { writeDownstream(
write('client', { name, data }) name: string,
data: unknown,
state: States = states.PLAY,
): void {
write('downstream', { name, data, state });
}, },
writeServer (name: string, data: unknown): void { writeUpstream(
write('server', { name, data }) name: string,
} data: unknown,
} as const state: States = states.PLAY,
): void {
write('upstream', { name, data, state });
},
} as const;
channel.subscribe(({ side, packet: raw }: Message) => { channel.subscribe(({ direction, packet: raw }: Message) => {
void (async () => { void (async () => {
const emitter = proxy[side] const sourceHandler = {
downstream: proxy.server,
upstream: proxy.client,
}[direction];
const packet = new Packet(raw.name, raw.data) const packet = new Packet(raw.name, raw.data);
await emitter.emit('packet', packet) await sourceHandler.emit('packet', packet);
await emitter.emit(packet.name, packet) await sourceHandler.emit(packet.name, packet);
if (packet.canceled) return if (packet.canceled) return;
switch (side) { switch (direction) {
case 'client': case 'downstream':
side = 'server' direction = 'upstream';
break break;
case 'server': case 'upstream':
side = 'client' direction = 'downstream';
break break;
default: default:
throw new Error(`Invalid side: ${side as any}`) throw new Error(`Invalid direction: ${direction as any}`);
} }
// Forward packet
channel.write({ channel.write({
side, direction,
packet packet,
}) });
})() })();
}) });
export default proxy export default proxy;

View file

@ -1,8 +1,8 @@
import { type RawPacket } from '../../util/packet.js' import { type RawPacket } from '../../util/packet.js';
export type Side = 'client' | 'server' export type Direction = 'upstream' | 'downstream';
export interface Message { export interface Message {
side: Side direction: Direction;
packet: RawPacket packet: RawPacket;
} }

View file

@ -1,21 +1,24 @@
import { type ClientOptions, type ServerOptions } from 'minecraft-protocol' import { type ClientOptions, type ServerOptions } from 'minecraft-protocol';
export const VERSION = '1.19.4' export const VERSION = '1.19.4';
export const TARGET_HOST = process.env.HOST;
export const TARGET_PORT = parseInt(process.env.PORT ?? '25565');
interface TargetOptions extends Omit<ClientOptions, 'username'> { interface TargetOptions extends Omit<ClientOptions, 'username'> {
username?: ClientOptions['username'] username?: ClientOptions['username'];
} }
export const TARGET_OPTIONS: TargetOptions = { export const TARGET_OPTIONS: TargetOptions = {
host: 'kaboom.pw', host: TARGET_HOST,
port: 25565, port: TARGET_PORT,
// username: 'RealDinhero21', // username: 'RealDinhero21',
// auth: 'microsoft', // auth: 'microsoft',
version: VERSION version: VERSION,
} };
export const SERVER_OPTIONS: ServerOptions = { export const SERVER_OPTIONS: ServerOptions = {
host: '127.0.0.1', host: '127.0.0.1',
port: 25565, port: 25565,
version: VERSION version: VERSION,
} };

View file

@ -1,43 +1,48 @@
export type Listener<T> = (data: T) => void // ==============================================================
// # This looks more and more like rx with each day that passes #
// ==============================================================
// https://rxjs.dev/
export type Listener<T> = (data: T) => void;
// _x -> x // _x -> x
// x -> _x // x -> _x
export class Channel<TSend, TReceive = TSend> { export class Channel<TSend, TReceive = TSend> {
public readonly id public readonly id;
constructor (id: string) { constructor(id: string) {
this.id = id this.id = id;
} }
private readonly listeners = new Set<Listener<TReceive>>() private readonly listeners = new Set<Listener<TReceive>>();
public subscribe (listener: Listener<TReceive>): void { public subscribe(listener: Listener<TReceive>): void {
this.listeners.add(listener) this.listeners.add(listener);
} }
public unsubscribe (listener: Listener<TReceive>): void { public unsubscribe(listener: Listener<TReceive>): void {
this.listeners.delete(listener) this.listeners.delete(listener);
} }
public write (data: TSend): void { public write(data: TSend): void {
for (const listener of this._listeners) { for (const listener of this._listeners) {
listener(data) listener(data);
} }
} }
private readonly _listeners = new Set<Listener<TSend>>() private readonly _listeners = new Set<Listener<TSend>>();
public _subscribe (listener: Listener<TSend>): void { public _subscribe(listener: Listener<TSend>): void {
this._listeners.add(listener) this._listeners.add(listener);
} }
public _unsubscribe (listener: Listener<TSend>): void { public _unsubscribe(listener: Listener<TSend>): void {
this._listeners.delete(listener) this._listeners.delete(listener);
} }
public _write (data: TReceive): void { public _write(data: TReceive): void {
for (const listener of this.listeners) { for (const listener of this.listeners) {
listener(data) listener(data);
} }
} }
} }

View file

@ -1,59 +1,73 @@
import { type AsyncVoid } from './types.js' // ==============================================================
// # This looks more and more like rx with each day that passes #
// ==============================================================
// https://rxjs.dev/
export type EventMap<T> = { [K in keyof T]: (...args: any[]) => AsyncVoid } import { type AsyncVoid } from './types.js';
export type EventMap<T> = { [K in keyof T]: (...args: any[]) => AsyncVoid };
export class EventHandler<T extends EventMap<T>> { export class EventHandler<T extends EventMap<T>> {
protected map = new Map<keyof T, Set<T[keyof T]>>() protected map = new Map<keyof T, Set<T[keyof T]>>();
protected async _emit<E extends keyof T> (name: E, ...data: Parameters<T[E]>): Promise<void> { protected async _emit<E extends keyof T>(
const map = this.map name: E,
...data: Parameters<T[E]>
): Promise<void> {
const map = this.map;
const set = map.get(name) const set = map.get(name);
if (set === undefined) return if (set === undefined) return;
for (const listener of set) await listener(...data) for (const listener of set) await listener(...data);
} }
public on<E extends keyof T> (name: E, callback: T[E]): void { public on<E extends keyof T>(name: E, callback: T[E]): void {
const map = this.map const map = this.map;
const set = map.get(name) ?? new Set() const set = map.get(name) ?? new Set();
map.set(name, set) map.set(name, set);
set.add(callback) set.add(callback);
} }
public once<E extends keyof T>(name: E, callback: T[E]): void { public once<E extends keyof T>(name: E, callback: T[E]): void {
const original = callback const original = callback;
callback = function (this: ThisParameterType<T[E]>, ...args: Parameters<T[E]>): ReturnType<T[E]> { callback = function (
const output = original.apply(this, args) this: ThisParameterType<T[E]>,
...args: Parameters<T[E]>
): ReturnType<T[E]> {
const output = original.apply(this, args);
return output as ReturnType<T[E]> return output as ReturnType<T[E]>;
} as unknown as T[E] } as unknown as T[E];
this.on(name, callback) this.on(name, callback);
} }
public off<E extends keyof T> (name: E, callback: T[E]): void { public off<E extends keyof T>(name: E, callback: T[E]): void {
const map = this.map const map = this.map;
const set = map.get(name) const set = map.get(name);
if (set === undefined) return if (set === undefined) return;
set.delete(callback) set.delete(callback);
} }
public clear (): void { public clear(): void {
const map = this.map const map = this.map;
map.clear() map.clear();
} }
} }
export class PublicEventHandler<T extends EventMap<T>> extends EventHandler<T> { export class PublicEventHandler<T extends EventMap<T>> extends EventHandler<T> {
public async emit<E extends keyof T> (name: E, ...data: Parameters<T[E]>): Promise<void> { public async emit<E extends keyof T>(
await this._emit(name, ...data) name: E,
...data: Parameters<T[E]>
): Promise<void> {
await this._emit(name, ...data);
} }
} }

View file

@ -1,22 +1,22 @@
import fs, { access, constants } from 'fs/promises' import fs, { access, constants } from 'fs/promises';
import { resolve } from 'path' import { resolve } from 'path';
export async function * getAllFiles (dir: string): AsyncIterable<string> { export async function* getAllFiles(dir: string): AsyncIterable<string> {
const entries = await fs.readdir(dir, { withFileTypes: true }) const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
const res = resolve(dir, entry.name) const res = resolve(dir, entry.name);
if (entry.isDirectory()) { if (entry.isDirectory()) {
yield * getAllFiles(res) yield* getAllFiles(res);
} else { } else {
yield res yield res;
} }
} }
} }
export async function exists (path: string): Promise<boolean> { export async function exists(path: string): Promise<boolean> {
return await access(path, constants.F_OK) return await access(path, constants.F_OK)
.then(() => true) .then(() => true)
.catch(() => false) .catch(() => false);
} }

View file

@ -4,47 +4,60 @@
// } // }
// } // }
import { exists } from './file.js' import { type Dirent } from 'fs';
import { type AsyncVoid } from './types.js' import { readdir } from 'fs/promises';
import { type Dirent } from 'fs' import { resolve } from 'path';
import { readdir } from 'fs/promises'
import { resolve } from 'path' import { exists } from './file.js';
import { type AsyncVoid } from './types.js';
export interface Callbacks { export interface Callbacks {
pre?: (entry: Dirent) => AsyncVoid pre?: (entry: Dirent) => AsyncVoid;
post?: (entry: Dirent) => AsyncVoid post?: (entry: Dirent) => AsyncVoid;
error?: (error: Error, entry: Dirent) => AsyncVoid error?: (error: Error, entry: Dirent) => AsyncVoid;
} }
export async function * importModulesGenerator (directory: string, index: string, callbacks?: Callbacks): AsyncGenerator<unknown> { // TODO: listr2?
// https://listr2.kilic.dev/
export async function* importModulesGenerator(
directory: string,
index: string,
callbacks?: Callbacks,
): AsyncGenerator<unknown> {
for (const entry of await readdir(directory, { withFileTypes: true })) { for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = resolve(entry.path, entry.name, index) const path = resolve(entry.path, entry.name, index);
if (!entry.isDirectory()) console.warn(`Expected ${entry.name} to be a directory (located at ${entry.path})`) if (!entry.isDirectory())
console.warn(
`Expected ${entry.name} to be a directory (located at ${entry.path})`,
);
if (!await exists(path)) continue if (!(await exists(path))) continue;
try { try {
const preCallback = callbacks?.pre const preCallback = callbacks?.pre;
if (preCallback !== undefined) await preCallback(entry) if (preCallback !== undefined) await preCallback(entry);
yield await import(path) yield await import(path);
const postCallback = callbacks?.post const postCallback = callbacks?.post;
if (postCallback !== undefined) await postCallback(entry) if (postCallback !== undefined) await postCallback(entry);
} catch (error) { } catch (error) {
const errorCallback = callbacks?.error const errorCallback = callbacks?.error;
if (errorCallback === undefined) throw error if (errorCallback === undefined) throw error;
await errorCallback(error as Error, entry) await errorCallback(error as Error, entry);
} }
} }
} }
export async function importModules (directory: string, index: string, callbacks?: Callbacks): Promise<void> { export async function importModules(
// eslint-disable-next-line @typescript-eslint/no-unused-vars directory: string,
index: string,
callbacks?: Callbacks,
): Promise<void> {
for await (const _ of importModulesGenerator(directory, index, callbacks)); for await (const _ of importModulesGenerator(directory, index, callbacks));
} }

View file

@ -1,16 +1,19 @@
export interface RawPacket { import type { PacketMeta, States } from 'minecraft-protocol';
name: string import { states } from 'minecraft-protocol';
data: unknown
export interface RawPacket extends PacketMeta {
data: unknown;
} }
export class Packet<Data = unknown> { export class Packet<Data = unknown> {
public name public name;
public data public state: States = states.PLAY;
public data;
public canceled: boolean = false public canceled: boolean = false;
constructor (name: string, data: Data) { constructor(name: string, data: Data) {
this.name = name this.name = name;
this.data = data this.data = data;
} }
} }

View file

@ -1 +1 @@
export type AsyncVoid = void | Promise<void> export type AsyncVoid = void | Promise<void>;

View file

@ -1,52 +1,51 @@
import { importModules } from '../util/import-modules.js' import chalk from 'chalk';
import { dirname, resolve } from 'path' import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url';
import chalk from 'chalk'
import { importModules } from '../util/import-modules.js';
if (!('require' in globalThis)) { if (!('require' in globalThis)) {
globalThis.__filename = fileURLToPath(import.meta.url) globalThis.__filename = fileURLToPath(import.meta.url);
globalThis.__dirname = dirname(__filename) globalThis.__dirname = dirname(__filename);
} }
const MODULE_DIR_PATH = resolve(__dirname, '../module') const MODULE_DIR_PATH = resolve(__dirname, '../module');
console.group('Loading modules... (local)') console.group('Loading modules... (local)');
const start = performance.now() const start = performance.now();
let moduleStart = NaN let moduleStart = NaN;
await importModules( await importModules(MODULE_DIR_PATH, 'local.js', {
MODULE_DIR_PATH, pre(entry) {
'local.js', const module = entry.name;
{ console.group(`Loading ${module}...`);
pre (entry) {
const module = entry.name
console.group(`Loading ${module}...`)
const now = performance.now() const now = performance.now();
moduleStart = now moduleStart = now;
}, },
post (entry) { post(_entry) {
const now = performance.now() const now = performance.now();
const delta = now - moduleStart const delta = now - moduleStart;
console.groupEnd() console.groupEnd();
console.info(`took ${delta.toFixed(2)}ms`) console.info(`took ${delta.toFixed(2)}ms`);
}, },
error (error, entry) { error(error, entry) {
const module = entry.name const module = entry.name;
error.stack += `\n while loading module ${JSON.stringify(module)} (local)` error.stack += `\n while loading module ${JSON.stringify(
module,
)} (local)`;
console.error(chalk.red(error.stack)) console.error(chalk.red(error.stack));
} },
} });
)
const end = performance.now() const end = performance.now();
const delta = end - start const delta = end - start;
console.groupEnd() console.groupEnd();
console.info(`took ${delta.toFixed(2)}ms`) console.info(`took ${delta.toFixed(2)}ms`);

View file

@ -1,34 +1,37 @@
import { Channel } from '../util/channel.js' import { parentPort } from 'worker_threads';
import { parentPort } from 'worker_threads'
if (parentPort === null) throw new Error('Must run in worker thread') import { Channel } from '../util/channel.js';
const port = parentPort if (parentPort === null) throw new Error('Must run in worker thread');
const port = parentPort;
export interface Message<T = any> { export interface Message<T = any> {
channel: string channel: string;
data: T data: T;
} }
function postMessage (channel: string, data: any): void { function postMessage(channel: string, data: any): void {
port.postMessage({ port.postMessage({
channel, channel,
data data,
}) });
} }
export function createChannel<TSend, TReceive = TSend> (id: string): Channel<TSend, TReceive> { export function createChannel<TSend, TReceive = TSend>(
const channel = new Channel<TSend, TReceive>(id) id: string,
): Channel<TSend, TReceive> {
const channel = new Channel<TSend, TReceive>(id);
channel._subscribe(message => { channel._subscribe((message) => {
postMessage(id, message) postMessage(id, message);
}) });
port.on('message', (message: Message<TReceive>) => { port.on('message', (message: Message<TReceive>) => {
if (message.channel !== id) return if (message.channel !== id) return;
channel._write(message.data) channel._write(message.data);
}) });
return channel return channel;
} }

View file

@ -11,7 +11,7 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
@ -25,9 +25,9 @@
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */ /* Modules */
"module": "nodenext", /* Specify what module code is generated. */ "module": "nodenext" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */ // "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
@ -77,12 +77,12 @@
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */ /* Type Checking */
"strict": true, /* Enable all strict type-checking options. */ "strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
@ -104,12 +104,8 @@
/* Completeness */ /* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */
}, },
"include": [ "include": ["src/**/*"],
"src/**/*" "exclude": ["node_modules"]
],
"exclude": [
"node_modules"
]
} }