Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
92 changes: 92 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/// <reference types="node" />
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;
}

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 NewPingResult {
forgeData: {
channels: FML3Channel[];
mods: FML3Mod[];
truncated: boolean;
fmlNetworkVersion: 3;
};
}

type PingResult =
| OldPingResult
| NewPingResult
| FMLPingResult
| FML2PingResult
| FML3PingResult
| FML3PingResultOverride;

export function ping(
options: PingOptions,
callback?: (error: Error, result: PingResult) => void
): Promise<PingResult>;
}
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
196 changes: 196 additions & 0 deletions src/ping.js
Original file line number Diff line number Diff line change
@@ -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
}
Loading