diff --git a/.assets/crossplay2.png b/.assets/crossplay2.png new file mode 100644 index 0000000..3177d96 Binary files /dev/null and b/.assets/crossplay2.png differ diff --git a/.assets/screenshot2.png b/.assets/screenshot2.png deleted file mode 100644 index f8762b1..0000000 Binary files a/.assets/screenshot2.png and /dev/null differ diff --git a/.gitignore b/.gitignore index 40b878d..81578b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -node_modules/ \ No newline at end of file +node_modules/ +.auth_cache/ +*.cache \ No newline at end of file diff --git a/constants.js b/constants.js index 0551bf3..1716e81 100644 --- a/constants.js +++ b/constants.js @@ -1,20 +1,21 @@ /* --------------------------------------------------------------- */ /* constants.js */ /* --------------------------------------------------------------- */ -const GAME_PORT = 25565; // -const WIN64_LAN_DISCOVERY_PORT = 25566; // -const MINECRAFT_NET_VERSION = 560; // -const JAVA_SERVER_HOST = "localhost"; // The java edition server host that the proxy will connect to. -const JAVA_SERVER_PORT = 25564; // The java edition server port that the proxy will connect to (default is 25565). -const PROXY_NAME = JAVA_SERVER_HOST + ":" + JAVA_SERVER_PORT; // -module.exports = { // - GAME_PORT, // - WIN64_LAN_DISCOVERY_PORT, // - MINECRAFT_NET_VERSION, // - USE_LEGACY_USERNAME: false, // You can use your legacy edition username, or a custom username below. - CUSTOM_USERNAME: "LCE_Player", // The custom username used if USE_LEGACY_USERNAME is false. Ensure it's UNDER 16 characters. - JAVA_SERVER_HOST, // - JAVA_SERVER_PORT, // - PROXY_NAME, // -}; // -/* --------------------------------------------------------------- */ \ No newline at end of file +const GAME_PORT = 25565; +const WIN64_LAN_DISCOVERY_PORT = 25566; +const MINECRAFT_NET_VERSION = 560; +const JAVA_SERVER_HOST = "hypixel.net"; // The java edition server host that the proxy will connect to. +const JAVA_SERVER_PORT = 25565; // The java edition server port that the proxy will connect to. +const PROXY_NAME = JAVA_SERVER_HOST + ":" + JAVA_SERVER_PORT; +module.exports = { + GAME_PORT, + WIN64_LAN_DISCOVERY_PORT, + MINECRAFT_NET_VERSION, + USE_JAVA_ACCOUNT: true, // Allows use of a microsoft java account for online servers. (Disable this if you don't own java - you'll only be able to connect to cracked/offline servers.) + USE_LEGACY_USERNAME: false, // You can use your legacy edition username, or a custom username below. (offline servers only) + CUSTOM_USERNAME: "LCE_Player", // The custom username used if USE_LEGACY_USERNAME is false. Ensure it's UNDER 16 characters. (offline servers only) + JAVA_SERVER_HOST, + JAVA_SERVER_PORT, + PROXY_NAME, +}; +/* --------------------------------------------------------------- */ diff --git a/index.js b/index.js index ff6d991..25f7496 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,10 @@ process.on("warning", (warning) => { const LCEProxy = require("./src/lceproxy"); const proxy = new LCEProxy(); -proxy.start(); + +(async () => { + await proxy.start(); +})(); process.on("SIGINT", () => { proxy.stop(); diff --git a/package-lock.json b/package-lock.json index 42d0bf2..f44e714 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "minecraft-protocol": "^1.64.0" + "minecraft-protocol": "^1.64.0", + "prismarine-auth": "^2.7.0" } }, "node_modules/@azure/msal-common": { diff --git a/package.json b/package.json index c209be3..a6fc8f7 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "author": "", "license": "MIT", "dependencies": { - "minecraft-protocol": "^1.64.0" + "minecraft-protocol": "^1.64.0", + "prismarine-auth": "^2.7.0" } } diff --git a/src/auth.js b/src/auth.js new file mode 100644 index 0000000..e625043 --- /dev/null +++ b/src/auth.js @@ -0,0 +1,92 @@ +const { Authflow, Titles } = require("prismarine-auth"); +const fs = require("fs"); +const path = require("path"); + +const AUTH_CACHE_DIR = path.join(__dirname, "..", ".auth_cache"); + +class MicrosoftAuth { + constructor() { + this.authflow = null; + this.authenticated = false; + this.username = null; + this.uuid = null; + this.accessToken = null; + } + + ensureCacheDir() { + if (!fs.existsSync(AUTH_CACHE_DIR)) { + fs.mkdirSync(AUTH_CACHE_DIR, { recursive: true }); + } + } + + async authenticate() { + this.ensureCacheDir(); + + if (!this.authflow) { + const userIdentifier = "lce-proxy-user"; + this.authflow = new Authflow(userIdentifier, AUTH_CACHE_DIR); + } + + if (!this.authflow.onMsaCode) { + this.authflow.onMsaCode = (data) => { + console.log("\nYou have not yet logged into your Java account!"); + const url = `https://microsoft.com/link?otc=${data.userCode}`; + console.log(`enter the code ${data.userCode} at ${url}`); + console.log( + "If you enabled this by accident and would like to only join offline servers, set USE_JAVA_ACCOUNT to false in constants.js\n", + ); + + try { + const { exec } = require("child_process"); + const platform = process.platform; + const command = + platform === "win32" + ? `start ${url}` + : platform === "darwin" + ? `open ${url}` + : `xdg-open ${url}`; + exec(command); + } catch (err) { + /* do nothing */ + } + }; + } + + try { + const result = await this.authflow.getMinecraftJavaToken({ + fetchProfile: true, + }); + + if (!result.profile) { + throw new Error( + "Error logging into Java Edition account! Do you own Minecraft Java Edition?\n", + ); + } + + this.username = result.profile.name; + this.uuid = result.profile.id; + this.accessToken = result.token; + this.authenticated = true; + + return { + username: this.username, + uuid: this.uuid, + accessToken: this.accessToken, + }; + } catch (err) { + throw err; + } + } + + async getAuthData() { + return await this.authenticate(); + } + + isAuthenticated() { + return this.authenticated; + } +} + +const authInstance = new MicrosoftAuth(); + +module.exports = authInstance; diff --git a/src/javaclienthandler.js b/src/javaclienthandler.js index 1ea8c44..afd0453 100644 --- a/src/javaclienthandler.js +++ b/src/javaclienthandler.js @@ -5,9 +5,11 @@ const mc = require("minecraft-protocol"); const { JAVA_SERVER_HOST, JAVA_SERVER_PORT, + USE_JAVA_ACCOUNT, USE_LEGACY_USERNAME, CUSTOM_USERNAME, } = require("../constants"); +const msAuth = require("./auth"); const PacketWriter = require("./packetwriter"); const { mapJavaItemToLCE, mapJavaBlockToLCE } = require("./mappings"); const { mapJavaSoundToLCE } = require("./mappings/soundmapping"); @@ -15,8 +17,8 @@ const { parseChatComponent } = require("./utils/chat"); const { countSetBits, compressRLE } = require("./utils/chunk"); const zlib = require("zlib"); -function connectToJavaServer(proxy, client) { - const javaClient = mc.createClient({ +async function connectToJavaServer(proxy, client) { + let authConfig = { host: JAVA_SERVER_HOST, port: JAVA_SERVER_PORT, username: !USE_LEGACY_USERNAME @@ -24,7 +26,30 @@ function connectToJavaServer(proxy, client) { : client.username || "undefined", auth: "offline", version: "1.8.9", - }); + }; + + if (USE_JAVA_ACCOUNT) { + if (msAuth.isAuthenticated()) { + authConfig = { + host: JAVA_SERVER_HOST, + port: JAVA_SERVER_PORT, + username: msAuth.username, + session: { + accessToken: msAuth.accessToken, + selectedProfile: { + id: msAuth.uuid, + name: msAuth.username, + }, + }, + auth: "microsoft", + version: "1.8.9", + }; + } else { + console.error("Not authenticated! This should not happen."); + } + } + + const javaClient = mc.createClient(authConfig); client.javaClient = javaClient; client._lastJavaPackets = []; @@ -1000,138 +1025,38 @@ function connectToJavaServer(proxy, client) { }); javaClient.on("spawn_entity", (packet) => { - if (client.state === "play") { - let lceEntityType = packet.type; - - if (packet.type === 10) { - lceEntityType = 10; - const minecartSubtype = packet.intField || 0; - const minecartTypes = [ - "Rideable", - "Chest", - "Furnace", - "TNT", - "Spawner", - "Hopper", - ]; - const minecartType = minecartTypes[minecartSubtype] || "Unknown"; - } - - let fallingBlockData = null; - if (packet.type === 70) { - const javaBlockId = packet.intField & 0xfff; - const metadata = (packet.intField >> 12) & 0xf; - - const lceBlockId = proxy.mapJavaBlockToLCE(javaBlockId); - - fallingBlockData = lceBlockId | (metadata << 16); - } - - if (lceEntityType < 0 || lceEntityType > 200) { - return; - } - - const lceEntityId = proxy.mapJavaEntityIdToLce(client, packet.entityId); - - let velocityX = 0, - velocityY = 0, - velocityZ = 0; - if (packet.objectData) { - velocityX = packet.objectData.velocityX || 0; - velocityY = packet.objectData.velocityY || 0; - velocityZ = packet.objectData.velocityZ || 0; - } - - let arrowData = -1; - if (lceEntityType === 60) { - arrowData = lceEntityId; - const javaYaw = (packet.yaw / 256) * 360; - const javaPitch = (packet.pitch / 256) * 360; - } - - let fishingHookData = -1; - if (lceEntityType === 90) { - const javaOwnerId = packet.intField || packet.entityId; - - if (javaOwnerId === client.javaPlayerEntityId) { - fishingHookData = client.lcePlayerEntityId || 1; - } else { - fishingHookData = - proxy.mapJavaEntityIdToLce(client, javaOwnerId) || lceEntityId; + try { + if (client.state === "play") { + if ( + !packet || + packet.entityId === undefined || + packet.type === undefined + ) { + return; } - } - const entityInfo = { - entityId: lceEntityId, - javaEntityId: packet.entityId, - type: lceEntityType, - x: packet.x / 32, - y: packet.y / 32, - z: packet.z / 32, - yaw: lceEntityType === 60 ? 0 : (packet.yaw / 256) * 360, - pitch: lceEntityType === 60 ? 0 : (packet.pitch / 256) * 360, - data: - lceEntityType === 2 - ? 1 - : lceEntityType === 60 - ? arrowData - : lceEntityType === 90 - ? fishingHookData - : lceEntityType === 63 || lceEntityType === 64 - ? 0 - : lceEntityType === 70 - ? fallingBlockData - : lceEntityType === 10 - ? packet.intField || 0 - : packet.objectData?.intField || -1, - velocityX: velocityX, - velocityY: velocityY, - velocityZ: velocityZ, - }; - - entityInfo.spawnTime = Date.now(); - - client.javaEntities.set(packet.entityId, entityInfo); - - try { - proxy.sendAddEntityPacket(client, entityInfo); - } catch (err) { - return; - } - - if (lceEntityType === 2) { - entityInfo.waitingForItemData = true; - } - - if (lceEntityType === 10) { - const minecartMetadata = []; - const minecartSubtype = packet.intField || 0; - let displayBlockId = 0; - switch (minecartSubtype) { - case 1: - displayBlockId = 54; - break; //chest - case 2: - displayBlockId = 61; - break; //furnace - case 3: - displayBlockId = 46; - break; //tNT - case 5: - displayBlockId = 154; - break; //hopper - case 4: - displayBlockId = 52; - break; //spawner - default: - displayBlockId = 0; - break; //no display + if ( + !Number.isFinite(packet.x) || + !Number.isFinite(packet.y) || + !Number.isFinite(packet.z) + ) { + return; } - if (displayBlockId > 0) { - minecartMetadata.push({ key: 20, type: 2, value: displayBlockId }); - minecartMetadata.push({ key: 21, type: 2, value: 6 }); - minecartMetadata.push({ key: 22, type: 0, value: 1 }); - proxy.sendEntityMetadataPacket(client, entityInfo, minecartMetadata); + + if (!Number.isFinite(packet.yaw) || !Number.isFinite(packet.pitch)) { + return; + } + + let lceEntityType = packet.type; + + const unsupportedEntityTypes = [78]; + if (unsupportedEntityTypes.includes(lceEntityType)) { + return; + } + + if (packet.type === 10) { + lceEntityType = 10; + const minecartSubtype = packet.intField || 0; const minecartTypes = [ "Rideable", "Chest", @@ -1140,9 +1065,142 @@ function connectToJavaServer(proxy, client) { "Spawner", "Hopper", ]; - const cartType = minecartTypes[minecartSubtype] || "Unknown"; + const minecartType = minecartTypes[minecartSubtype] || "Unknown"; + } + + let fallingBlockData = null; + if (packet.type === 70) { + const javaBlockId = packet.intField & 0xfff; + const metadata = (packet.intField >> 12) & 0xf; + + const lceBlockId = proxy.mapJavaBlockToLCE(javaBlockId); + + fallingBlockData = lceBlockId | (metadata << 16); + } + + if (lceEntityType < 0 || lceEntityType > 200) { + return; + } + + const lceEntityId = proxy.mapJavaEntityIdToLce(client, packet.entityId); + + let velocityX = 0, + velocityY = 0, + velocityZ = 0; + if (packet.objectData) { + velocityX = packet.objectData.velocityX || 0; + velocityY = packet.objectData.velocityY || 0; + velocityZ = packet.objectData.velocityZ || 0; + } + + let arrowData = -1; + if (lceEntityType === 60) { + arrowData = lceEntityId; + const javaYaw = (packet.yaw / 256) * 360; + const javaPitch = (packet.pitch / 256) * 360; + } + + let fishingHookData = -1; + if (lceEntityType === 90) { + const javaOwnerId = packet.intField || packet.entityId; + + if (javaOwnerId === client.javaPlayerEntityId) { + fishingHookData = client.lcePlayerEntityId || 1; + } else { + fishingHookData = + proxy.mapJavaEntityIdToLce(client, javaOwnerId) || lceEntityId; + } + } + + const entityInfo = { + entityId: lceEntityId, + javaEntityId: packet.entityId, + type: lceEntityType, + x: packet.x / 32, + y: packet.y / 32, + z: packet.z / 32, + yaw: lceEntityType === 60 ? 0 : (packet.yaw / 256) * 360, + pitch: lceEntityType === 60 ? 0 : (packet.pitch / 256) * 360, + data: + lceEntityType === 2 + ? 1 + : lceEntityType === 60 + ? arrowData + : lceEntityType === 90 + ? fishingHookData + : lceEntityType === 63 || lceEntityType === 64 + ? 0 + : lceEntityType === 70 + ? fallingBlockData + : lceEntityType === 10 + ? packet.intField || 0 + : packet.objectData?.intField || -1, + velocityX: velocityX, + velocityY: velocityY, + velocityZ: velocityZ, + }; + + entityInfo.spawnTime = Date.now(); + + client.javaEntities.set(packet.entityId, entityInfo); + + try { + proxy.sendAddEntityPacket(client, entityInfo); + } catch (err) { + return; + } + + if (lceEntityType === 2) { + entityInfo.waitingForItemData = true; + } + + if (lceEntityType === 10) { + const minecartMetadata = []; + const minecartSubtype = packet.intField || 0; + let displayBlockId = 0; + switch (minecartSubtype) { + case 1: + displayBlockId = 54; + break; + case 2: + displayBlockId = 61; + break; + case 3: + displayBlockId = 46; + break; + case 5: + displayBlockId = 154; + break; + case 4: + displayBlockId = 52; + break; + default: + displayBlockId = 0; + break; //no display + } + if (displayBlockId > 0) { + minecartMetadata.push({ key: 20, type: 2, value: displayBlockId }); + minecartMetadata.push({ key: 21, type: 2, value: 6 }); + minecartMetadata.push({ key: 22, type: 0, value: 1 }); + proxy.sendEntityMetadataPacket( + client, + entityInfo, + minecartMetadata, + ); + const minecartTypes = [ + "Rideable", + "Chest", + "Furnace", + "TNT", + "Spawner", + "Hopper", + ]; + const cartType = minecartTypes[minecartSubtype] || "Unknown"; + } } } + } catch (err) { + /* do nothing */ } }); @@ -1514,114 +1572,123 @@ function connectToJavaServer(proxy, client) { }); javaClient.on("entity_metadata", (packet) => { - if (client.state === "play") { - let isPlayer = false; - for (const [uuid, player] of client.javaPlayers) { - if (player.javaEntityId === packet.entityId && player.spawned) { - //0x01=on fire, 0x02=sneaking, 0x04=unused, 0x08=sprinting, 0x10=eating, 0x20=invisible - if (packet.metadata && packet.metadata.length > 0) { - const sharedFlags = packet.metadata.find( - (m) => m.key === 0 && m.type === 0, - ); - if (sharedFlags !== undefined) { - const flags = sharedFlags.value & 0xff; - - const metaWriter = new PacketWriter(); - metaWriter.writeInt(player.entityId); - metaWriter.writeByte((0 << 5) | 0); - metaWriter.writeByte(flags); - metaWriter.writeByte(0x7f); - proxy.sendPacket(client, 0x28, metaWriter.toBuffer()); - } - } - isPlayer = true; - break; - } - } - - if (!isPlayer && client.javaEntities.has(packet.entityId)) { - const entity = client.javaEntities.get(packet.entityId); - entity.metadata = packet.metadata || []; - - if (entity.type === 90 && packet.metadata) { - const hookedEntityMeta = packet.metadata.find((m) => m.key === 6); - if (hookedEntityMeta) { - if (hookedEntityMeta.value > 0) { - const hookedJavaEntityId = hookedEntityMeta.value; - const hookedLceEntityId = proxy.mapJavaEntityIdToLce( - client, - hookedJavaEntityId, + try { + if (client.state === "play") { + let isPlayer = false; + for (const [uuid, player] of client.javaPlayers) { + if (player.javaEntityId === packet.entityId && player.spawned) { + //0x01=on fire, 0x02=sneaking, 0x04=unused, 0x08=sprinting, 0x10=eating, 0x20=invisible + if (packet.metadata && packet.metadata.length > 0) { + const sharedFlags = packet.metadata.find( + (m) => m.key === 0 && m.type === 0, ); - entity.hookedEntityId = hookedLceEntityId; + if (sharedFlags !== undefined) { + const flags = sharedFlags.value & 0xff; + + const metaWriter = new PacketWriter(); + metaWriter.writeInt(player.entityId); + metaWriter.writeByte((0 << 5) | 0); + metaWriter.writeByte(flags); + metaWriter.writeByte(0x7f); + proxy.sendPacket(client, 0x28, metaWriter.toBuffer()); + } } + isPlayer = true; + break; } } - //if (entity.type === 10) { - // console.log('minecart', entity.type, entity.entityId, JSON.stringify(packet.metadata)); - //} + if (!isPlayer && client.javaEntities.has(packet.entityId)) { + const entity = client.javaEntities.get(packet.entityId); + entity.metadata = packet.metadata || []; - if ( - entity.type === 2 && - entity.waitingForItemData && - packet.metadata && - packet.metadata.length > 0 - ) { - const itemMetadata = packet.metadata.find( - (m) => m.key === 10 && m.type === 5, - ); - if (itemMetadata && itemMetadata.value) { - proxy.sendSetItemDataPacket(client, entity, itemMetadata.value); - entity.waitingForItemData = false; - } - } - - const customNameMeta = packet.metadata.find( - (m) => m.key === 2 && m.type === 4, - ); - const customNameVisibleMeta = packet.metadata.find( - (m) => m.key === 3 && m.type === 0, - ); - - const lceMetadata = []; - - if (customNameMeta && customNameMeta.value) { - lceMetadata.push({ - key: 10, - type: 4, - value: customNameMeta.value, - }); - } - - if (customNameVisibleMeta !== undefined) { - lceMetadata.push({ - key: 11, - type: 0, - value: customNameVisibleMeta.value, - }); - } - - const relevantMetadata = packet.metadata.filter((m) => { - if (entity.type === 10) { - if ([20, 21, 22].includes(m.key)) { - return false; + if (entity.type === 90 && packet.metadata) { + const hookedEntityMeta = packet.metadata.find((m) => m.key === 6); + if (hookedEntityMeta) { + if (hookedEntityMeta.value > 0) { + const hookedJavaEntityId = hookedEntityMeta.value; + const hookedLceEntityId = proxy.mapJavaEntityIdToLce( + client, + hookedJavaEntityId, + ); + entity.hookedEntityId = hookedLceEntityId; + } } } - //0=shared flags (e.g. on fire, sneaking, sprinting) - //12=age - //13=skeleton type - //16=villager profession or slime/magma size - //17=batman hanging flag - 0x01 = hanging - return [0, 12, 13, 16, 17].includes(m.key) && m.type !== 5; - }); + //if (entity.type === 10) { + // console.log('minecart', entity.type, entity.entityId, JSON.stringify(packet.metadata)); + //} - const allMetadata = [...lceMetadata, ...relevantMetadata]; + if ( + entity.type === 2 && + entity.waitingForItemData && + packet.metadata && + packet.metadata.length > 0 + ) { + const itemMetadata = packet.metadata.find( + (m) => m.key === 10 && m.type === 5, + ); + if (itemMetadata && itemMetadata.value) { + proxy.sendSetItemDataPacket(client, entity, itemMetadata.value); + entity.waitingForItemData = false; + } + } - if (allMetadata.length > 0 && entity.type !== 2) { - proxy.sendEntityMetadataPacket(client, entity, allMetadata); + const customNameMeta = packet.metadata + ? packet.metadata.find((m) => m && m.key === 2 && m.type === 4) + : undefined; + const customNameVisibleMeta = packet.metadata + ? packet.metadata.find((m) => m && m.key === 3 && m.type === 0) + : undefined; + + const lceMetadata = []; + + if (customNameMeta && customNameMeta.value) { + lceMetadata.push({ + key: 10, + type: 4, + value: customNameMeta.value, + }); + } + + if (customNameVisibleMeta !== undefined) { + lceMetadata.push({ + key: 11, + type: 0, + value: customNameVisibleMeta.value, + }); + } + + const relevantMetadata = packet.metadata + ? packet.metadata.filter((m) => { + if (!m || m.key === undefined || m.type === undefined) { + return false; + } + + if (entity.type === 10) { + if ([20, 21, 22].includes(m.key)) { + return false; + } + } + + if (m.type === 5) { + return false; + } + + return [0, 12, 13, 16, 17].includes(m.key); + }) + : []; + + const allMetadata = [...lceMetadata, ...relevantMetadata]; + + if (allMetadata.length > 0 && entity.type !== 2) { + proxy.sendEntityMetadataPacket(client, entity, allMetadata); + } } } + } catch (err) { + /* do nothing */ } }); diff --git a/src/lceproxy.js b/src/lceproxy.js index d85f9f8..4d38c9b 100644 --- a/src/lceproxy.js +++ b/src/lceproxy.js @@ -8,13 +8,16 @@ const { GAME_PORT, WIN64_LAN_DISCOVERY_PORT, MINECRAFT_NET_VERSION, + USE_JAVA_ACCOUNT, USE_LEGACY_USERNAME, CUSTOM_USERNAME, PROXY_NAME, } = require("../constants"); +const msAuth = require("./auth"); const PacketWriter = require("./packetwriter"); const PacketReader = require("./packetreader"); const LANBroadcast = require("./lanbroadcast"); +const recipeData = require("./mappings/recipemapping"); const { createEntityTypeMapping, createItemMapping, @@ -54,6 +57,16 @@ class LCEProxy { this.javaBlockMapping = createBlockMapping(); this.javaItemMapping = createItemMapping(); this.javaEntityTypeMapping = createEntityTypeMapping(); + + this.validLCEPackets = new Set([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x3e, 0x46, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x82, 0x83, 0xc8, 0xc9, 0xca, 0xcb, + 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xfa, 0xfc, 0xfd, 0xfe, 0xff, + ]); } mapJavaBlockToLCE(javaBlockId) { @@ -163,7 +176,21 @@ class LCEProxy { return null; } - start() { + async start() { + if (USE_JAVA_ACCOUNT) { + try { + await msAuth.authenticate(); + console.log("Logged in as: " + msAuth.username); + } catch (err) { + console.log("\Failed to log into java account! (", err.message, ")"); + console.log( + "If you enabled this by accident and would like to only join offline servers for free,", + ); + console.log("set USE_JAVA_ACCOUNT to false in constants.js\n"); + process.exit(1); + } + } + this.startBroadcasting(); this.startTCPServer(); @@ -510,10 +537,11 @@ class LCEProxy { this.handleContainerAck(client, packetData); } break; - //maybe we can use a custom JavaCraftingPacket (0x9E) packet with - //crafting grid data because java edition doesn't craft with recipe ids - //case 0x96: - // break; + case 0x96: + if (client.state === "play") { + this.handleCraftItem(client, packetData); + } + break; case 0x97: //TradeItemPacket - 151 if (client.state === "play") { this.handleTradeItem(client, packetData); @@ -2185,10 +2213,25 @@ class LCEProxy { }, totalDelay + 100); } - //LCE uses recipe ids to craft, whilst java edition requires grid data - //we should write a server plugin that sends the recipe data when crafting because lazy handleCraftItem(client, data) { - /* do nothing */ + const reader = new PacketReader(data.slice(1)); + const uid = reader.readShort(); + const recipeId = reader.readInt(); + + const grid = recipeData[recipeId] || [ + "none", + "none", + "none", + "none", + "none", + "none", + "none", + "none", + "none", + ]; + + //console.log(`recipe id: ${recipeId}`); + //console.log(`${grid[0]}, ${grid[1]}, ${grid[2]}, ${grid[3]}, ${grid[4]}, ${grid[5]}, ${grid[6]}, ${grid[7]}, ${grid[8]}`); } handleJavaCrafting(client, data) { /* do nothing */ @@ -2526,13 +2569,35 @@ class LCEProxy { } sendPacket(client, packetId, payload) { - const packetData = Buffer.concat([Buffer.from([packetId]), payload]); - const lengthPrefix = Buffer.allocUnsafe(4); - lengthPrefix.writeUInt32BE(packetData.length, 0); + if (!this.validLCEPackets.has(packetId)) { + console.warn( + `[WARNING] Attempted to send invalid packet ID: 0x${packetId.toString(16).toUpperCase().padStart(2, "0")}`, + ); + return; + } - const fullPacket = Buffer.concat([lengthPrefix, packetData]); + if (!payload || !Buffer.isBuffer(payload)) { + console.warn( + `[WARNING] Invalid payload for packet 0x${packetId.toString(16).toUpperCase().padStart(2, "0")}`, + ); + return; + } - const success = client.socket.write(fullPacket); + if (client.socket.destroyed) { + return; + } + + try { + const packetData = Buffer.concat([Buffer.from([packetId]), payload]); + const lengthPrefix = Buffer.allocUnsafe(4); + lengthPrefix.writeUInt32BE(packetData.length, 0); + + const fullPacket = Buffer.concat([lengthPrefix, packetData]); + + client.socket.write(fullPacket); + } catch (err) { + /* do nothing */ + } } disconnectClient(client, reason = 0) { @@ -2602,8 +2667,8 @@ class LCEProxy { } } - connectToJavaServer(client) { - return connectToJavaServerFunc(this, client); + async connectToJavaServer(client) { + return await connectToJavaServerFunc(this, client); } stop() { diff --git a/src/mappings/recipemapping.js b/src/mappings/recipemapping.js new file mode 100644 index 0000000..ffa4990 --- /dev/null +++ b/src/mappings/recipemapping.js @@ -0,0 +1,586 @@ +//all of the crafting recipes that are sent from the client converted into a java recipe grid. +//this was absolute fucking torture to write, thanks mojang for not using recipe ids in java edition crafting until 1.12! + +//unfinised as of 3/9/2026 (MM/DD/YY) + +module.exports = { + //page 1 + 0: [ + "oak_log", "none", "none", + "none", "none", "none", + "none", "none", "none" + ], + 1: [ + "birch_log", "none", "none", + "none", "none", "none", + "none", "none", "none" + ], + 2: [ + "spruce_log", "none", "none", + "none", "none", "none", + "none", "none", "none" + ], + 3: [ + "jungle_log", "none", "none", + "none", "none", "none", + "none", "none", "none" + ], + + + + 4: [ + "any_plank", "none", "none", + "any_plank", "none", "none", + "none", "none", "none" + ], + + + + 40: [ + "sand", "sand", "none", + "sand", "sand", "none", + "none", "none", "none" + ], + 41: [ + "sandstone_slab", "sandstone_slab", "none", + "sandstone_slab", "sandstone_slab", "none", + "none", "none", "none" + ], + 42: [ + "quartz_slab", "none", "none", + "quartz_slab", "none", "none", + "none", "none", "none" + ], + 43: [ + "quartz_block", "none", "none", + "quartz_block", "none", "none", + "none", "none", "none" + ], + 49: [ + "stone", "stone", "none", + "stone", "stone", "none", + "none", "none", "none" + ], + 51: [ + "nether_brick", "nether_brick", "none", + "nether_brick", "nether_brick", "none", + "none", "none", "none" + ], + 156: [ + "snowball", "snowball", "none", + "snowball", "snowball", "none", + "none", "none", "none" + ], + 157: [ + "snowball", "snowball", "none", + "snowball", "snowball", "none", + "none", "none", "none" + ], + 158: [ + "clay", "clay", "none", + "clay", "clay", "none", + "none", "none", "none" + ], + 159: [ + "brick", "brick", "none", + "brick", "brick", "none", + "none", "none", "none" + ], + 203: [ + "nether_quartz", "nether_quartz", "none", + "nether_quartz", "nether_quartz", "none", + "none", "none", "none" + ], + + + + 44: [ + "any_plank", "any_plank", "none", + "any_plank", "any_plank", "none", + "none", "none", "none" + ], + 45: [ + "cobblestone", "cobblestone", "cobblestone", + "cobblestone", "none", "cobblestone", + "cobblestone", "cobblestone", "cobblestone" + ], + 55: [ + "any_plank", "book", "none", + "diamond", "obsidian", "diamond", + "obsidian", "obsidian", "obsidian" + ], + 56: [ + "iron_block", "iron_block", "iron_block", + "none", "iron_ingot", "none", + "iron_ingot", "iron_ingot", "iron_ingot" + ], + 227: [ + "none", "none", "none", + "none", "blaze_rod", "none", + "cobblestone", "cobblestone", "cobblestone" + ], + + + + 46: [ + "any_plank", "any_plank", "any_plank", + "any_plank", "none", "any_plank", + "any_plank", "any_plank", "any_plank" + ], + 47: [ + "chest", "tripwire_hook", "none", + "none", "none", "none", + "none", "none", "none" + ], + 48: [ + "obsidian", "obsidian", "obsidian", + "obsidian", "ender_eye", "obsidian", + "obsidian", "obsidian", "obsidian" + ], + + + + 54: [ + "none", "none", "none", + "any_wool", "any_wool", "any_wool", + "any_plank", "any_plank", "any_plank" + ], + + + + 57: [ + "stick", "none", "stick", + "stick", "stick", "stick", + "stick", "none", "stick" + ], + + + + 58: [ + "stick", "any_plank", "stick", + "stick", "any_plank", "stick", + "none", "none", "none" + ], + + + + 59: [ + "stick", "stick", "stick", + "stick", "stick", "stick", + "none", "none", "none" + ], + 60: [ + "nether_brick", "nether_brick", "nether_brick", + "nether_brick", "nether_brick", "nether_brick", + "none", "none", "none" + ], + 61: [ + "iron_ingot", "iron_ingot", "iron_ingot", + "iron_ingot", "iron_ingot", "iron_ingot", + "none", "none", "none" + ], + 62: [ + "cobblestone", "cobblestone", "cobblestone", + "cobblestone", "cobblestone", "cobblestone", + "none", "none", "none" + ], + 63: [ + "mossy_cobblestone", "mossy_cobblestone", "mossy_cobblestone", + "mossy_cobblestone", "mossy_cobblestone", "mossy_cobblestone", + "none", "none", "none" + ], + + + + 64: [ + "any_plank", "any_plank", "none", + "any_plank", "any_plank", "none", + "any_plank", "any_plank", "none" + ], + 65: [ + "iron_ingot", "iron_ingot", "none", + "iron_ingot", "iron_ingot", "none", + "iron_ingot", "iron_ingot", "none" + ], + 67: [ + "any_plank", "any_plank", "any_plank", + "any_plank", "any_plank", "any_plank", + "none", "none", "none" + ], + + + + 66: [ + "oak_plank", "none", "none", + "oak_plank", "oak_plank", "none", + "oak_plank", "oak_plank", "oak_plank" + ], + 68: [ + "cobblestone", "none", "none", + "cobblestone", "cobblestone", "none", + "cobblestone", "cobblestone", "cobblestone" + ], + 69: [ + "brick_block", "none", "none", + "brick_block", "brick_block", "none", + "brick_block", "brick_block", "brick_block" + ], + 70: [ + "stonebrick", "none", "none", + "stonebrick", "stonebrick", "none", + "stonebrick", "stonebrick", "stonebrick" + ], + 71: [ + "nether_brick", "none", "none", + "nether_brick", "nether_brick", "none", + "nether_brick", "nether_brick", "nether_brick" + ], + 72: [ + "sandstone", "none", "none", + "sandstone", "sandstone", "none", + "sandstone", "sandstone", "sandstone" + ], + 73: [ + "birch_planks", "none", "none", + "birch_planks", "birch_planks", "none", + "birch_planks", "birch_planks", "birch_planks" + ], + 74: [ + "spruce_planks", "none", "none", + "spruce_planks", "spruce_planks", "none", + "spruce_planks", "spruce_planks", "spruce_planks" + ], + 75: [ + "jungle_planks", "none", "none", + "jungle_planks", "jungle_planks", "none", + "jungle_planks", "jungle_planks", "jungle_planks" + ], + 76: [ + "quartz_block", "none", "none", + "quartz_block", "quartz_block", "none", + "quartz_block", "quartz_block", "quartz_block" + ], + //next: slabs + + + + + + //page 2 + 5: [ + "any_plank", "any_plank", "any_plank", + "none", "stick", "none", + "none", "stick", "none" + ], + 9: [ + "cobblestone", "cobblestone", "cobblestone", + "none", "stick", "none", + "none", "stick", "none" + ], + 13: [ + "iron_ingot", "iron_ingot", "iron_ingot", + "none", "stick", "none", + "none", "stick", "none" + ], + 17: [ + "diamond", "diamond", "diamond", + "none", "stick", "none", + "none", "stick", "none" + ], + 21: [ + "gold_ingot", "gold_ingot", "gold_ingot", + "none", "stick", "none", + "none", "stick", "none" + ], + + + + 6: [ + "none", "any_plank", "none", + "none", "stick", "none", + "none", "stick", "none" + ], + 10: [ + "none", "cobblestone", "none", + "none", "stick", "none", + "none", "stick", "none" + ], + 14: [ + "none", "iron_ingot", "none", + "none", "stick", "none", + "none", "stick", "none" + ], + 18: [ + "none", "diamond", "none", + "none", "stick", "none", + "none", "stick", "none" + ], + 22: [ + "none", "gold_ingot", "none", + "none", "stick", "none", + "none", "stick", "none" + ], + + + + 7: [ + "any_plank", "any_plank", "none", + "any_plank", "stick", "none", + "none", "stick", "none" + ], + 11: [ + "cobblestone", "cobblestone", "none", + "cobblestone", "stick", "none", + "none", "stick", "none" + ], + 15: [ + "iron_ingot", "iron_ingot", "none", + "iron_ingot", "stick", "none", + "none", "stick", "none" + ], + 19: [ + "diamond", "diamond", "none", + "diamond", "stick", "none", + "none", "stick", "none" + ], + 23: [ + "gold_ingot", "gold_ingot", "none", + "gold_ingot", "stick", "none", + "none", "stick", "none" + ], + + + + 8: [ + "any_plank", "any_plank", "none", + "none", "stick", "none", + "none", "stick", "none" + ], + 12: [ + "cobblestone", "cobblestone", "none", + "none", "stick", "none", + "none", "stick", "none" + ], + 16: [ + "iron_ingot", "iron_ingot", "none", + "none", "stick", "none", + "none", "stick", "none" + ], + 20: [ + "diamond", "diamond", "none", + "none", "stick", "none", + "none", "stick", "none" + ], + 24: [ + "gold_ingot", "gold_ingot", "none", + "none", "stick", "none", + "none", "stick", "none" + ], + + + + 25: [ + "iron_ingot", "none", "none", + "none", "iron_ingot", "none", + "none", "none", "none" + ], + 187: [ + "flint", "none", "none", + "none", "iron_ingot", "none", + "none", "none", "none" + ], + + + + 161: [ + "gunpowder", "sand", "gunpowder", + "sand", "gunpowder", "sand", + "gunpowder", "sand", "gunpowder" + ], + + + + 185: [ + "none", "none", "stick", + "none", "stick", "string", + "stick", "none", "string" + ], + 186: [ + "fishing_rod", "none", "none", + "none", "carrot", "none", + "none", "none", "none" + ], + + + + 189: [ + "none", "stick", "string", + "stick", "none", "string", + "none", "stick", "string" + ], + 190: [ + "flint", "none", "none", + "stick", "none", "none", + "feather", "none", "none" + ], + + + + 191: [ + "none", "any_plank", "none", + "none", "any_plank", "none", + "none", "stick", "none" + ], + 192: [ + "none", "cobblestone", "none", + "none", "cobblestone", "none", + "none", "stick", "none" + ], + 193: [ + "none", "iron_ingot", "none", + "none", "iron_ingot", "none", + "none", "stick", "none" + ], + 194: [ + "none", "diamond", "none", + "none", "diamond", "none", + "none", "stick", "none" + ], + 195: [ + "none", "gold_ingot", "none", + "none", "gold_ingot", "none", + "none", "stick", "none" + ], + + + + 196: [ + "none", "none", "none", + "iron_ingot", "none", "iron_ingot", + "none", "iron_ingot", "none" + ], + 197: [ + "any_plank", "none", "any_plank", + "none", "any_plank", "none", + "none", "none", "none" + ], + 198: [ + "glass", "none", "glass", + "none", "glass", "none", + "none", "none", "none" + ], + 226: [ + "iront_ingot", "none", "iront_ingot", + "iront_ingot", "none", "iront_ingot", + "iront_ingot", "iront_ingot", "iront_ingot" + ], + + + + 200: [ + "charcoal", "none", "none", + "stick", "none", "none", + "none", "none", "none" + ], + 201: [ + "coal", "none", "none", + "stick", "none", "none", + "none", "none", "none" + ], + 202: [ + "glowstone", "glowstone", "none", + "glowstone", "glowstone", "none", + "none", "none", "none" + ], + 213: [ + "gunpowder", "blaze_powder", "none", + "charcoal", "none", "none", + "none", "none", "none" + ], + 214: [ + "gunpowder", "blaze_powder", "none", + "coal", "none", "none", + "none", "none", "none" + ], + 228: [ + "pumpkin", "none", "none", + "torch", "none", "none", + "none", "none", "none" + ], + + + + 211: [ + "none", "gold_ingot", "none", + "gold_ingot", "redstone", "gold_ingot", + "none", "gold_ingot", "none" + ], + 212: [ + "ender_pearl", "blaze_powder", "none", + "none", "none", "none", + "none", "none", "none" + ], + 215: [ + "string", "string", "none", + "string", "slime_ball", "none", + "none", "none", "string" + ], + 216: [ + "none", "iron_ingot", "none", + "iron_ingot", "redstone", "iron_ingot", + "none", "iron_ingot", "none" + ], + 217: [ + "paper", "paper", "paper", + "paper", "compass", "paper", + "paper", "paper", "paper" + ], + + + + + + +//page 3 + 26: [ + "gold_ingot", "gold_ingot", "gold_ingot", + "gold_ingot", "apple", "gold_ingot", + "gold_ingot", "gold_ingot", "gold_ingot" + ], + 27: [ + "gold_block", "gold_block", "gold_block", + "gold_block", "apple", "gold_block", + "gold_block", "gold_block", "gold_block" + ], + 28: [ + "gold_nugget", "gold_nugget", "gold_nugget", + "gold_nugget", "melon", "gold_nugget", + "gold_nugget", "gold_nugget", "gold_nugget" + ], + 35: [ + "gold_nugget", "gold_nugget", "gold_nugget", + "gold_nugget", "carrot", "gold_nugget", + "gold_nugget", "gold_nugget", "gold_nugget" + ], + + + + 29: [ + "brown_mushroom", "red_mushroom", "none", + "bowl", "none", "none", + "none", "none", "none" + ], + + + + 30: [ + "wheat", "cocoa_beans", "wheat", + "none", "none", "none", + "none", "none", "none" + ], + 31: [ + "melon", "melon", "melon", + "melon", "melon", "melon", + "melon", "melon", "melon" + ], +}; diff --git a/src/packets/senders/index.js b/src/packets/senders/index.js index e125bfe..13f9cfb 100644 --- a/src/packets/senders/index.js +++ b/src/packets/senders/index.js @@ -3,7 +3,7 @@ /* --------------------------------------------------------------- */ const playerPackets = require("./playerpackets"); const entityPackets = require("./entitypackets"); -const worldPackets = require("./worldPackets"); +const worldPackets = require("./worldpackets"); const containerPackets = require("./containerpackets"); module.exports = {