From 4452ca3b8bf0b99fe0c3d5bea248d3a1cacc1c2c Mon Sep 17 00:00:00 2001 From: SeaLoong Date: Mon, 14 Jul 2025 03:34:57 +0800 Subject: [PATCH 1/2] feat: support FML3 ping --- README.md | 1 + index.d.ts | 73 +++++++++++++++++++ index.js | 3 +- src/ping.js | 196 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 index.d.ts create mode 100644 src/ping.js diff --git a/README.md b/README.md index de47286..df02cff 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Adds FML/Forge support to [node-minecraft-protocol](https://github.com/Prismarin * Supports the `FML|HS` client handshake * Adds automatic Forge mod detection to node-minecraft-protocol's auto-versioning +* Adds ping implement for `FML3` `forgeData` to get Forge mods and channels ## Usage diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..4322f04 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,73 @@ +/// +import { PingOptions as VanillaPingOptions, OldPingResult, NewPingResult } from 'minecraft-protocol' + +declare module 'minecraft-protocol-forge' { + + export interface PingOptions extends VanillaPingOptions { + deserializeForgeData?: boolean + overrideForgeData?: boolean + } + + export interface FMLPingResult extends NewPingResult { + modinfo: { + type: string + modList: { + modid: string + version: string + }[] + } + } + + export interface FML2PingResult extends NewPingResult { + forgeData: { + channels: { + res: string + version: string + required: boolean + }[] + mods: { + modId: string + modmarker: string + }[] + fmlNetworkVersion: 2 + } + } + + interface FML3Mod { + modId: string + modVersion?: string + } + + interface FML3Channel { + channelName: string + channelVersion: string + requiredOnClient: boolean + } + + export interface FML3PingResult extends NewPingResult { + forgeData: { + channels: [] + mods: [] + truncated: boolean + fmlNetworkVersion: 3 + d: { + truncated: boolean + mods: (FML3Mod & { + channels: FML3Channel[] + })[] + nonModChannels: FML3Channel[] + } + } + } + + export interface FML3PingResultOverride extends FML3PingResult { + forgeData: { + channels: FML3Channel[] + mods: FML3Mod[] + } + } + + type PingResult = OldPingResult | NewPingResult | FMLPingResult | FML2PingResult | FML3PingResult | FML3PingResultOverride + + export function ping(options: PingOptions, callback?: (error: Error, result: PingResult) => void): Promise +} diff --git a/index.js b/index.js index 7ce2d78..e376e18 100644 --- a/index.js +++ b/index.js @@ -2,5 +2,6 @@ module.exports = { forgeHandshake: require('./src/client/forgeHandshake'), - autoVersionForge: require('./src/client/autoVersionForge') + autoVersionForge: require('./src/client/autoVersionForge'), + ping: require('./src/ping') } diff --git a/src/ping.js b/src/ping.js new file mode 100644 index 0000000..dd495f7 --- /dev/null +++ b/src/ping.js @@ -0,0 +1,196 @@ +const Buffer = require('buffer').Buffer +const ProtoDef = require('protodef').ProtoDef +const mc = require('minecraft-protocol') +const debug = require('debug')('minecraft-protocol-forge') + +module.exports = ping + +const proto = new ProtoDef(false) + +// copied from ../../dist/transforms/serializer.js +proto.addType('string', [ + 'pstring', + { + countType: 'varint' + } +]) + +// copied from node-minecraft-protocol +proto.addTypes({ + restBuffer: [ + (buffer, offset) => { + return { + value: buffer.slice(offset), + size: buffer.length - offset + } + }, + (value, buffer, offset) => { + value.copy(buffer, offset) + return offset + value.length + }, + (value) => { + return value.length + } + ] +}) + +proto.addTypes({ + resource_location: ['string'], + mod: [ + function (buffer, offset, typeArgs, context) { + let newOffset = offset + + const channelSizeAndVersionFlagResult = this.read(buffer, newOffset, 'varint', {}, context) + newOffset += channelSizeAndVersionFlagResult.size + const hasModVersion = (channelSizeAndVersionFlagResult.value & 0b1) === 0 + const channelSize = channelSizeAndVersionFlagResult.value >>> 1 + + const modIdResult = this.read(buffer, newOffset, 'string', {}, context) + newOffset += modIdResult.size + const modId = modIdResult.value + + let modVersion + if (hasModVersion) { + const modVersionResult = this.read(buffer, newOffset, 'string', {}, context) + newOffset += modVersionResult.size + modVersion = modVersionResult.value + } + + const channels = [] + for (let i = 0; i < channelSize; i++) { + const channelResult = this.read(buffer, newOffset, 'mod_channel', {}, context) + newOffset += channelResult.size + channels.push(channelResult.value) + } + return { + value: { + modId, + modVersion, + channels + }, + size: newOffset - offset + } + }, + function (value, buffer, offset, typeArgs, context) { + const channelSizeAndVersionFlag = (value.channels.length << 1) | (value.modVersion ? 0 : 1) + offset = this.write(channelSizeAndVersionFlag, buffer, offset, 'varint', {}, context) + + offset = this.write(value.modId, buffer, offset, 'string', {}, context) + + if (value.modVersion) { + offset = this.write(value.modVersion, buffer, offset, 'string', {}, context) + } + + for (const channel of (value.channels || [])) { + offset = this.write(channel, buffer, offset, 'mod_channel', {}, context) + } + + return offset + }, + function (value, typeArgs, context) { + let size = 0 + const channelSizeAndVersionFlag = (value.channels.length << 1) | (value.modVersion ? 0 : 1) + size += this.sizeOf(channelSizeAndVersionFlag, 'varint', {}, context) + size += this.sizeOf(value.modId, 'string', {}, context) + if (value.modVersion) { + size += this.sizeOf(value.modVersion, 'string', {}, context) + } + for (const channel of (value.channels || [])) { + size += this.sizeOf(channel, 'mod_channel', {}, context) + } + return size + } + ], + mod_channel: [ + 'container', + [ + { name: 'channelName', type: 'string' }, + { name: 'channelVersion', type: 'string' }, + { name: 'requiredOnClient', type: 'bool' } + ] + ], + non_mod_channel: [ + 'container', + [ + { name: 'channelName', type: 'resource_location' }, + { name: 'channelVersion', type: 'string' }, + { name: 'requiredOnClient', type: 'bool' } + ] + ], + forge_d: [ + 'container', + [ + { name: 'truncated', type: 'bool' }, + { name: 'modsSize', type: 'u16' }, + { name: 'mods', type: ['array', { count: 'modsSize', type: 'mod' }] }, + { name: 'nonModChannelCount', type: 'varint' }, + { name: 'nonModChannels', type: ['array', { count: 'nonModChannelCount', type: 'non_mod_channel' }] } + ] + ] +}) + +function ping (options, cb) { + return mc.ping(options).then((data) => { + if (options?.deserializeForgeData !== false && data.forgeData?.d) { + try { + const buf = decodeOptimized(data.forgeData.d) + const d = proto.parsePacketBuffer('forge_d', buf).data + if (options?.overrideForgeData !== false) { + data.forgeData.mods = d.mods.map((mod) => ({ + modId: mod.modId, + modVersion: mod.modVersion + })) + const modsChannels = d.mods.flatMap((mod) => mod.channels.map((ch) => ({ + channelName: `${mod.modId}:${ch.channelName}`, + channelVersion: ch.channelVersion, + requiredOnClient: ch.requiredOnClient + }))) + data.forgeData.channels = modsChannels.concat(d.nonModChannels) + delete data.forgeData.d + } else { + data.forgeData.d = d + } + } catch (e) { + debug('Failed to deserialize forgeData', e) + } + } + return data + }) +} + +/** + * @param {string} s + * @returns {Buffer} + */ +function decodeOptimized (s) { + const size0 = s.charCodeAt(0) + const size1 = s.charCodeAt(1) + const size = size0 | (size1 << 15) + + const buf = Buffer.alloc(size) + + let stringIndex = 2 + let buffer = 0 + let bitsInBuf = 0 + let bufOffset = 0 + + while (stringIndex < s.length) { + while (bitsInBuf >= 8 && bufOffset < size) { + buf[bufOffset++] = buffer & 0xff + buffer >>>= 8 + bitsInBuf -= 8 + } + const c = s.charCodeAt(stringIndex) + buffer |= (c & 0x7fff) << bitsInBuf + bitsInBuf += 15 + stringIndex++ + } + + // write any leftovers + while (bufOffset < size) { + buf[bufOffset++] = buffer & 0xff + buffer >>>= 8 + bitsInBuf -= 8 + } + return buf +} From b1c7c0e58310d8ddcaf1cdd96cbc61bb8374288f Mon Sep 17 00:00:00 2001 From: SeaLoong Date: Tue, 22 Jul 2025 06:07:20 +0800 Subject: [PATCH 2/2] fix: type of ping --- index.d.ts | 95 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4322f04..6fd992f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,73 +1,92 @@ /// -import { PingOptions as VanillaPingOptions, OldPingResult, NewPingResult } from 'minecraft-protocol' +import { + PingOptions as VanillaPingOptions, + OldPingResult as VanillaOldPingResult, + NewPingResult as VanillaNewPingResult, +} from 'minecraft-protocol'; declare module 'minecraft-protocol-forge' { + export type OldPingResult = VanillaOldPingResult + + export type NewPingResult = VanillaNewPingResult + export interface PingOptions extends VanillaPingOptions { - deserializeForgeData?: boolean - overrideForgeData?: boolean + deserializeForgeData?: boolean; + overrideForgeData?: boolean; } export interface FMLPingResult extends NewPingResult { modinfo: { - type: string + type: string; modList: { - modid: string - version: string - }[] - } + modid: string; + version: string; + }[]; + }; } export interface FML2PingResult extends NewPingResult { forgeData: { channels: { - res: string - version: string - required: boolean - }[] + res: string; + version: string; + required: boolean; + }[]; mods: { - modId: string - modmarker: string - }[] - fmlNetworkVersion: 2 - } + modId: string; + modmarker: string; + }[]; + fmlNetworkVersion: 2; + }; } interface FML3Mod { - modId: string - modVersion?: string + modId: string; + modVersion?: string; } interface FML3Channel { - channelName: string - channelVersion: string - requiredOnClient: boolean + channelName: string; + channelVersion: string; + requiredOnClient: boolean; } export interface FML3PingResult extends NewPingResult { forgeData: { - channels: [] - mods: [] - truncated: boolean - fmlNetworkVersion: 3 + channels: []; + mods: []; + truncated: boolean; + fmlNetworkVersion: 3; d: { - truncated: boolean + truncated: boolean; mods: (FML3Mod & { - channels: FML3Channel[] - })[] - nonModChannels: FML3Channel[] - } - } + channels: FML3Channel[]; + })[]; + nonModChannels: FML3Channel[]; + }; + }; } - export interface FML3PingResultOverride extends FML3PingResult { + export interface FML3PingResultOverride extends NewPingResult { forgeData: { - channels: FML3Channel[] - mods: FML3Mod[] - } + channels: FML3Channel[]; + mods: FML3Mod[]; + truncated: boolean; + fmlNetworkVersion: 3; + }; } - type PingResult = OldPingResult | NewPingResult | FMLPingResult | FML2PingResult | FML3PingResult | FML3PingResultOverride + type PingResult = + | OldPingResult + | NewPingResult + | FMLPingResult + | FML2PingResult + | FML3PingResult + | FML3PingResultOverride; - export function ping(options: PingOptions, callback?: (error: Error, result: PingResult) => void): Promise + export function ping( + options: PingOptions, + callback?: (error: Error, result: PingResult) => void + ): Promise; }