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
}

8
.swcrc
View file

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

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
- Modular Minecraft Proxy
# 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.
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.

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",
"version": "1.0.0",
"description": "Lego Minecraft",
"description": "Modular Minecraft Proxy",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "scripty",
"watch:build": "scripty",
"copy": "scripty",
"watch:copy": "scripty",
"lint": "scripty",
"run": "scripty",
"watch:run": "scripty",
"watch": "scripty"
},
"keywords": [],
"author": "",
"author": "Dinhero Development Group",
"license": "MIT",
"type": "module",
"config": {
"scripty": {
"parallel": true,
"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": {
"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",
"chalk": "^5.3.0",
"minecraft-protocol": "^1.26.5",
"scripty": "^2.1.1"
"@types/node": "^22.5.1",
"eslint": "^9.9.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
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
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 { SERVER_OPTIONS } from './settings.js'
import { createServer } from 'minecraft-protocol'
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
Object.assign(Uint8Array.prototype, Buffer.prototype)
Object.assign(Uint8Array.prototype, Buffer.prototype);
export const server = createServer({
'online-mode': false,
...SERVER_OPTIONS,
keepAlive: false
})
keepAlive: false,
});
server.on('login', client => {
console.info(`${client.username} has connected!`)
server.on('login', (client) => {
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 { importModulesGenerator } from '../util/import-modules.js'
import { Channel } from '../util/channel.js'
import { TARGET_OPTIONS } from '../settings.js'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { Worker } from 'worker_threads'
import { createClient, type ServerClient } from 'minecraft-protocol'
import chalk from 'chalk'
import chalk from 'chalk';
import { createClient, type ServerClient } from 'minecraft-protocol';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { Worker } from 'worker_threads';
import { TARGET_OPTIONS } from '../settings.js';
import { Channel } from '../util/channel.js';
import { importModulesGenerator } from '../util/import-modules.js';
import { type Message } from '../worker/parent.js';
if (!('require' in globalThis)) {
globalThis.__filename = fileURLToPath(import.meta.url)
globalThis.__dirname = dirname(__filename)
globalThis.__filename = fileURLToPath(import.meta.url);
globalThis.__dirname = dirname(__filename);
}
const WORKER_PATH = resolve(__dirname, '../worker')
const MODULE_DIR_PATH = resolve(__dirname, '../module')
const WORKER_PATH = resolve(__dirname, '../worker');
const MODULE_DIR_PATH = resolve(__dirname, '../module');
export class Instance {
public readonly client
public readonly server
public readonly worker
public readonly client;
public readonly server;
public readonly worker;
constructor (client: ServerClient) {
this.client = client
constructor(client: ServerClient) {
this.client = client;
const target = createClient({
auth: 'offline',
username: client.username,
...TARGET_OPTIONS,
keepAlive: false
})
keepAlive: false,
});
this.server = target
this.server = target;
target.on('error', error => {
console.error('Target error:', error)
})
target.on('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)
this.worker = worker
const worker = new Worker(WORKER_PATH);
this.worker = worker;
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 => {
console.error('Worker error:', error)
})
worker.on('error', (error) => {
console.error('Worker error:', error);
});
void this._importModules()
void this._importModules();
}
private async _importModules (): Promise<void> {
console.group('Loading modules... (global)')
private async _importModules(): Promise<void> {
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(
MODULE_DIR_PATH,
'global.js',
{
pre (entry) {
const now = performance.now()
moduleStart = now
pre(entry) {
const now = performance.now();
moduleStart = now;
const module = entry.name
console.group(`Loading ${module}...`)
const module = entry.name;
console.group(`Loading ${module}...`);
},
post (entry) {
const now = performance.now()
const delta = now - moduleStart
post(_entry) {
const now = performance.now();
const delta = now - moduleStart;
console.groupEnd()
console.info(`took ${delta.toPrecision(2)}ms`)
console.groupEnd();
console.info(`took ${delta.toPrecision(2)}ms`);
},
error (error, entry) {
const module = entry.name
error(error, entry) {
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 (typeof module !== 'object') throw new Error('Expected module to be an object')
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 (!('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.info(`took ${delta.toFixed(2)}ms`)
console.groupEnd();
console.info(`took ${delta.toFixed(2)}ms`);
}
protected postMessage (channel: string, data: any): void {
protected postMessage(channel: string, data: any): void {
this.worker.postMessage({
channel,
data
} satisfies Message)
data,
} satisfies Message);
}
public createChannel<TSend, TReceive = TSend> (id: string): Channel<TSend, TReceive> {
const channel = new Channel<TSend, TReceive>(id)
public createChannel<TSend, TReceive = TSend>(
id: string,
): Channel<TSend, TReceive> {
const channel = new Channel<TSend, TReceive>(id);
channel._subscribe(data => {
this.postMessage(id, data)
})
channel._subscribe((data) => {
this.postMessage(id, data);
});
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 { type Message } from './shared.js'
import { type RawPacket } from '../../util/packet.js'
import { states } from 'minecraft-protocol'
import { states } from 'minecraft-protocol';
import type Instance from '../../instance/index.js';
import { type RawPacket } from '../../util/packet.js';
import { type Message } from './shared.js';
export default async function (instance: Instance): Promise<void> {
const clientQueue: RawPacket[] = []
const serverQueue: RawPacket[] = []
const clientQueue: RawPacket[] = [];
const serverQueue: RawPacket[] = [];
const channel = instance.createChannel<Message>('proxy')
const channel = instance.createChannel<Message>('proxy');
const client = instance.client
const server = instance.server
const worker = instance.worker
const client = instance.client;
const server = instance.server;
const worker = instance.worker;
client.on('end', reason => {
console.info('Client disconnected:', reason)
client.on('end', (reason) => {
console.info('Client disconnected:', reason);
server.end(reason)
server.end(reason);
void worker.terminate()
})
void worker.terminate();
});
server.on('end', reason => {
console.info('Target disconnected:', reason)
server.on('end', (reason) => {
console.info('Server disconnected:', reason);
client.end(reason)
client.end(reason);
void worker.terminate()
})
void worker.terminate();
});
client.on('error', error => {
console.error('Client error:', error)
})
client.on('error', (error) => {
console.error('Client error:', error);
});
server.on('error', error => {
console.error('Target error:', error)
})
server.on('error', (error) => {
console.error('Server error:', error);
});
client.on('packet', (data, meta) => {
if (meta.state !== states.PLAY) return
// if (meta.state !== states.PLAY) return;
channel.write({
side: 'client',
direction: 'downstream',
packet: {
name: meta.name,
data
}
})
})
...meta,
data,
},
});
});
server.on('packet', (data, meta) => {
if (meta.state !== states.PLAY) return
// if (meta.state !== states.PLAY) return;
channel.write({
side: 'server',
direction: 'upstream',
packet: {
name: meta.name,
data
}
})
})
...meta,
data,
},
});
});
client.on('state', state => {
if (state !== states.PLAY) return
// Flush packet queue on play state
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 => {
if (state !== states.PLAY) return
queue.length = 0;
});
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) {
case 'client':
writeClientPacket(packet)
break
case 'server':
writeServerPacket(packet)
break
case 'downstream':
writeClientPacket(packet);
break;
case 'upstream':
writeServerPacket(packet);
break;
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) {
clientQueue.push(packet)
clientQueue.push(packet);
return
return;
}
client.write(
packet.name,
packet.data
)
if (packet.state !== states.PLAY) return;
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) {
serverQueue.push(packet)
serverQueue.push(packet);
return
return;
}
server.write(
packet.name,
packet.data
)
if (packet.state !== states.PLAY) return;
server.write(packet.name, packet.data);
}
}

View file

@ -1,60 +1,75 @@
import { type Side, type Message } from './shared.js'
import { PublicEventHandler } from '../../util/events.js'
import { createChannel } from '../../worker/parent.js'
import { Packet, type RawPacket } from '../../util/packet.js'
import { type AsyncVoid } from '../../util/types.js'
import type { States } from 'minecraft-protocol';
import { states } from 'minecraft-protocol';
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
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({
side,
packet
})
direction,
packet,
});
}
export const proxy = {
client: new PublicEventHandler<PacketEventMap>(),
server: new PublicEventHandler<PacketEventMap>(),
writeClient (name: string, data: unknown): void {
write('client', { name, data })
writeDownstream(
name: string,
data: unknown,
state: States = states.PLAY,
): void {
write('downstream', { name, data, state });
},
writeServer (name: string, data: unknown): void {
write('server', { name, data })
}
} as const
writeUpstream(
name: string,
data: unknown,
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 () => {
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 emitter.emit(packet.name, packet)
await sourceHandler.emit('packet', packet);
await sourceHandler.emit(packet.name, packet);
if (packet.canceled) return
if (packet.canceled) return;
switch (side) {
case 'client':
side = 'server'
break
case 'server':
side = 'client'
break
switch (direction) {
case 'downstream':
direction = 'upstream';
break;
case 'upstream':
direction = 'downstream';
break;
default:
throw new Error(`Invalid side: ${side as any}`)
throw new Error(`Invalid direction: ${direction as any}`);
}
// Forward packet
channel.write({
side,
packet
})
})()
})
direction,
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 {
side: Side
packet: RawPacket
direction: Direction;
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'> {
username?: ClientOptions['username']
username?: ClientOptions['username'];
}
export const TARGET_OPTIONS: TargetOptions = {
host: 'kaboom.pw',
port: 25565,
host: TARGET_HOST,
port: TARGET_PORT,
// username: 'RealDinhero21',
// auth: 'microsoft',
version: VERSION
}
version: VERSION,
};
export const SERVER_OPTIONS: ServerOptions = {
host: '127.0.0.1',
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
export class Channel<TSend, TReceive = TSend> {
public readonly id
public readonly id;
constructor (id: string) {
this.id = id
constructor(id: string) {
this.id = id;
}
private readonly listeners = new Set<Listener<TReceive>>()
private readonly listeners = new Set<Listener<TReceive>>();
public subscribe (listener: Listener<TReceive>): void {
this.listeners.add(listener)
public subscribe(listener: Listener<TReceive>): void {
this.listeners.add(listener);
}
public unsubscribe (listener: Listener<TReceive>): void {
this.listeners.delete(listener)
public unsubscribe(listener: Listener<TReceive>): void {
this.listeners.delete(listener);
}
public write (data: TSend): void {
public write(data: TSend): void {
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 {
this._listeners.add(listener)
public _subscribe(listener: Listener<TSend>): void {
this._listeners.add(listener);
}
public _unsubscribe (listener: Listener<TSend>): void {
this._listeners.delete(listener)
public _unsubscribe(listener: Listener<TSend>): void {
this._listeners.delete(listener);
}
public _write (data: TReceive): void {
public _write(data: TReceive): void {
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>> {
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> {
const map = this.map
protected async _emit<E extends keyof T>(
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 {
const map = this.map
public on<E extends keyof T>(name: E, callback: T[E]): void {
const map = this.map;
const set = map.get(name) ?? new Set()
map.set(name, set)
const set = map.get(name) ?? new Set();
map.set(name, set);
set.add(callback)
set.add(callback);
}
public once<E extends keyof T>(name: E, callback: T[E]): void {
const original = callback
callback = function (this: ThisParameterType<T[E]>, ...args: Parameters<T[E]>): ReturnType<T[E]> {
const output = original.apply(this, args)
const original = callback;
callback = function (
this: ThisParameterType<T[E]>,
...args: Parameters<T[E]>
): ReturnType<T[E]> {
const output = original.apply(this, args);
return output as ReturnType<T[E]>
} as unknown as T[E]
return output as ReturnType<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 {
const map = this.map
public off<E extends keyof T>(name: E, callback: T[E]): void {
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 {
const map = this.map
public clear(): void {
const map = this.map;
map.clear()
map.clear();
}
}
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> {
await this._emit(name, ...data)
public async emit<E extends keyof T>(
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 { resolve } from 'path'
import fs, { access, constants } from 'fs/promises';
import { resolve } from 'path';
export async function * getAllFiles (dir: string): AsyncIterable<string> {
const entries = await fs.readdir(dir, { withFileTypes: true })
export async function* getAllFiles(dir: string): AsyncIterable<string> {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const res = resolve(dir, entry.name)
const res = resolve(dir, entry.name);
if (entry.isDirectory()) {
yield * getAllFiles(res)
yield* getAllFiles(res);
} 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)
.then(() => true)
.catch(() => false)
.catch(() => false);
}

View file

@ -4,47 +4,60 @@
// }
// }
import { exists } from './file.js'
import { type AsyncVoid } from './types.js'
import { type Dirent } from 'fs'
import { readdir } from 'fs/promises'
import { resolve } from 'path'
import { type Dirent } from 'fs';
import { readdir } from 'fs/promises';
import { resolve } from 'path';
import { exists } from './file.js';
import { type AsyncVoid } from './types.js';
export interface Callbacks {
pre?: (entry: Dirent) => AsyncVoid
post?: (entry: Dirent) => AsyncVoid
error?: (error: Error, entry: Dirent) => AsyncVoid
pre?: (entry: Dirent) => AsyncVoid;
post?: (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 })) {
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 {
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) {
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> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function importModules(
directory: string,
index: string,
callbacks?: Callbacks,
): Promise<void> {
for await (const _ of importModulesGenerator(directory, index, callbacks));
}

View file

@ -1,16 +1,19 @@
export interface RawPacket {
name: string
data: unknown
import type { PacketMeta, States } from 'minecraft-protocol';
import { states } from 'minecraft-protocol';
export interface RawPacket extends PacketMeta {
data: unknown;
}
export class Packet<Data = unknown> {
public name
public data
public name;
public state: States = states.PLAY;
public data;
public canceled: boolean = false
public canceled: boolean = false;
constructor (name: string, data: Data) {
this.name = name
this.data = data
constructor(name: string, data: Data) {
this.name = name;
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 { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import chalk from 'chalk'
import chalk from 'chalk';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { importModules } from '../util/import-modules.js';
if (!('require' in globalThis)) {
globalThis.__filename = fileURLToPath(import.meta.url)
globalThis.__dirname = dirname(__filename)
globalThis.__filename = fileURLToPath(import.meta.url);
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(
MODULE_DIR_PATH,
'local.js',
{
pre (entry) {
const module = entry.name
console.group(`Loading ${module}...`)
await importModules(MODULE_DIR_PATH, 'local.js', {
pre(entry) {
const module = entry.name;
console.group(`Loading ${module}...`);
const now = performance.now()
moduleStart = now
},
post (entry) {
const now = performance.now()
const delta = now - moduleStart
const now = performance.now();
moduleStart = now;
},
post(_entry) {
const now = performance.now();
const delta = now - moduleStart;
console.groupEnd()
console.info(`took ${delta.toFixed(2)}ms`)
},
error (error, entry) {
const module = entry.name
console.groupEnd();
console.info(`took ${delta.toFixed(2)}ms`);
},
error(error, entry) {
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.info(`took ${delta.toFixed(2)}ms`)
console.groupEnd();
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> {
channel: string
data: T
channel: string;
data: T;
}
function postMessage (channel: string, data: any): void {
function postMessage(channel: string, data: any): void {
port.postMessage({
channel,
data
})
data,
});
}
export function createChannel<TSend, TReceive = TSend> (id: string): Channel<TSend, TReceive> {
const channel = new Channel<TSend, TReceive>(id)
export function createChannel<TSend, TReceive = TSend>(
id: string,
): Channel<TSend, TReceive> {
const channel = new Channel<TSend, TReceive>(id);
channel._subscribe(message => {
postMessage(id, message)
})
channel._subscribe((message) => {
postMessage(id, message);
});
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. */
/* 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. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "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. */
/* 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. */
"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. */
// "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. */
@ -77,12 +77,12 @@
// "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. */
// "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. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* 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. */
// "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. */
@ -104,12 +104,8 @@
/* Completeness */
// "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": [
"src/**/*"
],
"exclude": [
"node_modules"
]
"include": ["src/**/*"],
"exclude": ["node_modules"]
}