Files
2026-04-01 20:17:24 -04:00

4202 lines
112 KiB
JavaScript

/* --------------------------------------------------------------- */
/* lceproxy.js */
/* --------------------------------------------------------------- */
//beware that there are a lot of magic numbers, need to do a refractor soon.
const dgram = require("dgram");
const net = require("net");
const mc = require("minecraft-protocol");
const {
GAME_PORT,
WIN64_LAN_DISCOVERY_PORT,
MINECRAFT_NET_VERSION,
LOBBY_NAME,
} = require("../constants");
const PacketWriter = require("./packetwriter");
const PacketReader = require("./packetreader");
const LANBroadcast = require("./lanbroadcast");
const recipeData = require("./mappings/recipemapping");
const {
createEntityTypeMapping,
createItemMapping,
createBlockMapping,
} = require("./mappings");
const { compressRLE } = require("./utils/chunk");
const { encodeVarInt } = require("./utils");
const { normalizeYaw } = require("./utils/rotation");
const { readItemInstance } = require("./utils/items");
const { readItem, writeItem, writeItemNBT } = require("./utils/items");
const packetSenders = require("./packets/senders");
const connectToJavaServerFunc = require("./javaclienthandler");
//please stfu node
process.removeAllListeners("warning");
process.on("warning", (warning) => {
if (warning.name === "DeprecationWarning") return;
console.warn(warning.name, warning.message);
});
const zlib = require("zlib");
const fs = require("fs");
const path = require("path");
class LCEProxy {
constructor() {
this.clients = [];
this.udpSocket = null;
this.tcpServers = [];
this.broadcastInterval = null;
this.broadcasts = [];
this.serverConfigs = [];
this.nextSmallId = 1;
this.usedSmallIds = new Set();
this.openChests = new Map();
this.clientChests = new Map();
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, 0x36, 0x3c, 0x3d, 0x3e, 0x46, 0x64,
0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x82, 0x83, 0x85,
0x96, 0x97, 0x98, 0x9a, 0x9d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce,
0xcf, 0xd0, 0xd1, 0xfa, 0xfc, 0xfd, 0xfe, 0xff, 0xaa, 0xac,
]);
}
mapJavaBlockToLCE(javaBlockId) {
if (this.javaBlockMapping[javaBlockId] !== undefined) {
return this.javaBlockMapping[javaBlockId];
}
return 1;
}
mapJavaParticleToLCE(particleId) {
const particleMap = {
0: "explode",
1: "largeexplode",
2: "hugeexplosion",
3: "fireworksSpark",
4: "bubble",
5: "splash",
6: "wake",
7: "suspended",
8: "depthsuspend",
9: "crit",
10: "magicCrit",
11: "smoke",
12: "largesmoke",
13: "spell",
14: "instantSpell",
15: "mobSpell",
16: "mobSpellAmbient",
17: "witchMagic",
18: "dripWater",
19: "dripLava",
20: "angryVillager",
21: "happyVillager",
22: "townaura",
23: "note",
24: "portal",
25: "enchantmenttable",
26: "flame",
27: "lava",
28: "footstep",
29: "cloud",
30: "reddust",
31: "snowballpoof",
32: "snowshovel",
33: "slime",
34: "heart",
35: "barrier",
36: "iconcrack",
37: "blockcrack",
38: "blockdust",
39: "droplet",
40: "take",
41: "mobappearance",
42: "dragonbreath",
43: "endrod",
44: "damageindicator",
45: "sweepattack",
};
return particleMap[particleId] || null;
}
mapJavaItemToLCE(javaItemId) {
if (javaItemId >= 0 && javaItemId <= 255) {
return this.mapJavaBlockToLCE(javaItemId);
}
if (this.javaItemMapping[javaItemId] !== undefined) {
return this.javaItemMapping[javaItemId];
}
return 1;
}
mapLCEItemToJava(lceItemId) {
if (lceItemId >= 0 && lceItemId <= 255) {
return this.mapLCEBlockToJava(lceItemId);
}
if (
(lceItemId >= 256 && lceItemId <= 421) ||
(lceItemId >= 2256 && lceItemId <= 2267)
) {
for (const [javaId, lceId] of Object.entries(this.javaItemMapping)) {
if (lceId === lceItemId) {
return parseInt(javaId);
}
}
return lceItemId;
}
return null;
}
mapLCEBlockToJava(lceBlockId) {
for (const [javaId, lceId] of Object.entries(this.javaBlockMapping)) {
if (lceId === lceBlockId) {
return parseInt(javaId);
}
}
if (lceBlockId >= 0 && lceBlockId <= 174) {
return lceBlockId;
}
return null;
}
async start() {
this.startBroadcasting();
this.startTCPServer();
console.log("\nProxy is now running!");
console.log("Press Ctrl+C to stop.\n");
}
startBroadcasting() {
this.udpSocket = dgram.createSocket("udp4");
this.udpSocket.bind(() => {
this.udpSocket.setBroadcast(true);
});
this.lobbyBroadcast = new LANBroadcast(LOBBY_NAME, GAME_PORT);
this.broadcastInterval = setInterval(() => {
this.lobbyBroadcast.setPlayerCount(Math.max(1, this.clients.length));
this.udpSocket.send(
this.lobbyBroadcast.buffer,
0,
this.lobbyBroadcast.buffer.length,
WIN64_LAN_DISCOVERY_PORT,
"255.255.255.255",
);
}, 1000);
}
startTCPServer() {
const tcpServer = net.createServer((socket) => {
socket.setNoDelay(true);
let smallId = 0;
while (this.usedSmallIds.has(smallId) && smallId <= 8) {
smallId++;
}
if (smallId > 8) {
socket.destroy();
return;
}
this.usedSmallIds.add(smallId);
const client = {
socket: socket,
smallId: smallId,
buffer: Buffer.alloc(0),
state: "prelogin",
javaClient: null,
breakingBlock: null,
javaPlayers: new Map(),
nextLceEntityId: 100,
javaToLceEntityId: new Map(),
javaEntities: new Map(),
serverConfig: null,
inLobby: true,
};
this.clients.push(client);
const smallIdBuffer = Buffer.from([smallId]);
socket.write(smallIdBuffer);
socket.on("data", (data) => {
this.handleClientData(client, data);
});
socket.on("end", () => {
this.removeClient(client);
});
socket.on("error", (err) => {
this.removeClient(client);
});
socket.on("close", () => {
if (this.clients.indexOf(client) > -1) {
this.removeClient(client);
}
});
});
tcpServer.listen(GAME_PORT, () => {
/* do nothing */
});
this.tcpServers.push(tcpServer);
}
getPacketDataSize(packetId, data, offset) {
switch (packetId) {
case 0x00:
return 0;
case 0x0a:
return 10;
case 0x0b:
let size = 4 + 1 + 4 + 1;
if (offset + size + 2 <= data.length) {
const itemId = data.readInt16BE(offset + size);
size += 2;
if (itemId !== -1) {
size += 1 + 2;
}
size += 4 + 4 + 4;
}
return size;
case 0x0c:
return data.length - offset >= 7 + 2 ? 7 : 7;
case 0x0d:
const remaining = data.length - offset;
if (remaining >= 41) return 41;
if (remaining >= 33) return 33;
if (remaining >= 9) return 9;
return remaining;
case 0x0e:
return 12;
case 0x07:
return 9;
//this retarded packet wasted hours of my life because it was being bundled with others...
//for some reason we don't receive this packet if the player is doing nothing besides hitting - 4J what the fuck did you do to this poor packet.
case 0x12:
return 5;
default:
return null;
}
}
//thanks 0x12
calculatePacketSize(packetId, data, offset) {
try {
switch (packetId) {
case 0x00:
return 0;
case 0x0a:
return 1 + 1;
case 0x0b:
return 1 + 33;
case 0x0c:
return 1 + 9;
case 0x0d:
if (offset >= data.length) return null;
const remainingInBundle = data.length - offset;
if (remainingInBundle < 42) return remainingInBundle;
return 42;
case 0x0e:
return 1 + 1 + 4 + 1 + 4 + 1;
case 0x07:
return 1 + 4 + 4 + 1;
case 0x12:
return 1 + 4 + 1;
case 0x0f:
let useItemSize = 1 + 4 + 1 + 4 + 1;
if (offset + useItemSize + 2 <= data.length) {
const itemId = data.readInt16BE(offset + useItemSize);
useItemSize += 2;
if (itemId !== -1) {
useItemSize += 1;
useItemSize += 2;
}
}
useItemSize += 1 + 1 + 1;
return useItemSize;
case 0x10:
return 1 + 2;
case 0x13:
return 1 + 9;
case 0x1b:
return 1 + 10;
case 0x66:
let containerClickSize = 1 + 1 + 2 + 1 + 2 + 1;
if (offset + containerClickSize + 2 <= data.length) {
const itemId = data.readInt16BE(offset + containerClickSize);
containerClickSize += 2;
if (itemId !== -1) {
containerClickSize += 1;
containerClickSize += 2;
}
}
return containerClickSize;
case 0x6a:
return 1 + 4;
case 0x6b:
return 1 + 8;
case 0x65:
return 1 + 1;
case 0xcd:
return 1 + 1;
case 0x98:
return 1 + 4;
case 0x82:
try {
let signSize = 1 + 4 + 2 + 4 + 1 + 1;
let pos = offset + 1 + 4 + 2 + 4 + 1 + 1;
for (let i = 0; i < 4; i++) {
if (pos + 2 > data.length) return null;
const strLen = data.readInt16BE(pos);
pos += 2;
signSize += 2 + strLen * 2;
pos += strLen * 2;
}
return signSize;
} catch (err) {
return null;
}
case 0x85:
return 1 + 1 + 4 + 4 + 4;
default:
return null;
}
} catch (err) {
return null;
}
}
handleClientData(client, data) {
try {
client.buffer = Buffer.concat([client.buffer, data]);
while (client.buffer.length >= 4) {
const packetLength = client.buffer.readUInt32BE(0);
if (client.buffer.length < 4 + packetLength) {
break;
}
const packetData = client.buffer.slice(4, 4 + packetLength);
client.buffer = client.buffer.slice(4 + packetLength);
const packetId = packetData[0];
const expectedSize = this.calculatePacketSize(packetId, packetData, 0);
if (expectedSize > 0 && packetData.length > expectedSize) {
let offset = 0;
while (offset < packetData.length) {
const bundledPacketId = packetData[offset];
const bundledSize = this.calculatePacketSize(
bundledPacketId,
packetData,
offset,
);
if (bundledSize <= 0 || offset + bundledSize > packetData.length) {
break;
}
const singlePacket = packetData.slice(offset, offset + bundledSize);
this.processPacket(
client,
bundledPacketId,
singlePacket,
offset > 0,
);
offset += bundledSize;
}
} else {
this.processPacket(client, packetId, packetData, false);
}
}
} catch (err) {
/* do nothing */
}
}
processPacket(client, packetId, packetData, isBundled = false) {
switch (packetId) {
case 0x02: //PreLoginPacket
this.handlePreLogin(client, packetData);
break;
case 0xab: //AuthResponsePacket (171)
if (client.state === "login") {
this.handleAuthResponse(client, packetData);
}
break;
case 0x01: //LoginPacket
this.handleLogin(client, packetData);
break;
case 0x00: //KeepAlive
if (client.state === "prelogin") {
this.handleKeepAlive(client, packetData);
}
break;
case 0x07: //InteractPacket - 7
if (client.state === "play") {
this.handleInteract(client, packetData);
}
break;
case 0x12: //AnimatePacket - 18
if (client.state === "play") {
this.handleAnimate(client, packetData, isBundled);
}
break;
case 0x0d: //MovePlayerPacket (pos & rot)
if (client.state === "play") {
this.handleMovePlayer(client, packetData);
}
break;
case 0x0a: //base (onGround/isFlying)
case 0x0b: //pos
case 0x0c: //rot
if (client.state === "play") {
this.handlePlayerMovement(client, packetId, packetData);
}
break;
case 0x03:
if (client.state === "play") {
this.handleChat(client, packetData);
}
break;
case 0x0e:
if (client.state === "play") {
this.handlePlayerAction(client, packetData, isBundled);
}
break;
case 0x0f: //UseItemPacket - 15
if (client.state === "play") {
this.handleUseItem(client, packetData);
}
break;
case 0x10: //SetCarriedItemPacket - 16
if (client.state === "play") {
this.handleSetCarriedItem(client, packetData);
}
break;
case 0x13: //PlayerCommandPacket - 19
if (client.state === "play") {
this.handlePlayerCommand(client, packetData);
}
break;
case 0x1b: //PlayerInputPacket - 27
if (client.state === "play") {
this.handlePlayerInput(client, packetData);
}
break;
case 0x65: //ContainerClosePacket - 101
if (client.state === "play") {
this.handleContainerClose(client, packetData);
}
break;
case 0x66: //ContainerClickPacket - 102
if (client.state === "play") {
this.handleContainerClick(client, packetData);
}
break;
case 0x6a: //ContainerAckPacket - 106
if (client.state === "play") {
this.handleContainerAck(client, packetData);
}
break;
case 0x96:
if (client.state === "play") {
this.handleCraftItem(client, packetData);
}
break;
case 0x97: //TradeItemPacket - 151
if (client.state === "play") {
this.handleTradeItem(client, packetData);
}
break;
case 0x98:
break;
//case 0x9E: //JavaCraftingPacket - 158
// if (client.state === 'play') {
// this.handleJavaCrafting(client, packetData);
// }
// break;
case 0xfa: //CustomPayloadPacket - 250
if (client.state === "play") {
this.handleCustomPayload(client, packetData);
}
break;
case 0x82:
if (client.state === "play") {
this.handleSignUpdate(client, packetData);
}
break;
case 0x6b: //SetCreativeModeSlotPacket - 107
if (client.state === "play") {
this.handleSetCreativeModeSlot(client, packetData);
}
break;
case 0xcd: //ClientCommandPacket - 205
if (client.state === "play") {
this.handleClientCommand(client, packetData);
}
break;
case 0xff: //DisconnectPacket - 255
this.handleDisconnect(client, packetData);
break;
default:
break;
}
}
handlePreLogin(client, data) {
const reader = new PacketReader(data.slice(1));
const netcodeVersion = reader.readShort();
const loginKey = reader.readString();
client.state = "login";
client._netcodeVersion = netcodeVersion;
const writer = new PacketWriter();
writer.writeShort(MINECRAFT_NET_VERSION);
writer.writeString("SESSION_ID_" + client.smallId);
writer.writeByte(0); //friendsOnly
writer.writeInt(0); //ugcPlayersVersion
writer.writeByte(0); //playerCount
for (let i = 0; i < 14; i++) writer.writeByte(0); //uniqueSaveName
let serverSettings = 0;
//difficulty
//0=Peaceful, 1=Easy, 2=Normal, 3=Hard
serverSettings |= 1;
//friends of friends (off)
//serverSettings |= 0x00000004;
//gamertags (on)
serverSettings |= 0x00000008;
//0=Survival, 1=Creative, 2=Adventure
//survival=leave bits clear (0)
//creative=serverSettings |= 0x00000010;
//level type (OFF=Default, ON=Flat)
//serverSettings |= 0x00000040; //uncomment for flat
//structures (on)
serverSettings |= 0x00000080;
//bonus chest (off)
//serverSettings |= 0x00000100;
//pvp (on)
serverSettings |= 0x00000400;
//trust players (on)
serverSettings |= 0x00000800;
//tnt (on)
serverSettings |= 0x00001000;
//spread fire (on)
serverSettings |= 0x00002000;
writer.writeInt(serverSettings);
writer.writeByte(0); //hostIndex
writer.writeInt(0); //texturePackId
this.sendPacket(client, 0x02, writer.toBuffer());
if (client._netcodeVersion >= 561) {
client.authState = "waitingResponse";
setImmediate(() => this._sendAuthSchemePacket(client));
} else {
client.authState = "done";
}
}
_sendAuthSchemePacket(client) {
const chars = "0123456789abcdef";
let serverId = "";
for (let i = 0; i < 20; i++)
serverId += chars[Math.floor(Math.random() * 16)];
client._authServerId = serverId;
const schemes = ["offline"];
client._authSchemes = schemes;
const writer = new PacketWriter();
writer.writeShort(schemes.length);
for (const s of schemes) writer.writeString(s);
writer.writeString(serverId);
this.sendPacket(client, 0xaa, writer.toBuffer());
}
handleAuthResponse(client, data) {
if (client.authState !== "waitingResponse") return;
const reader = new PacketReader(data.slice(1));
const chosenScheme = reader.readString();
const mojangUuid = reader.readString();
const username = reader.readString();
const validSchemes = client._authSchemes || ["mojang", "offline"];
if (!validSchemes.includes(chosenScheme)) {
this._sendAuthResult(client, false, "", "", "Invalid auth scheme");
return;
}
if (chosenScheme === "offline") {
const crypto = require("crypto");
const hash = crypto
.createHash("md5")
.update("OfflinePlayer:" + username)
.digest();
hash[6] = (hash[6] & 0x0f) | 0x30;
hash[8] = (hash[8] & 0x3f) | 0x80;
const h = hash.toString("hex");
const offlineUuid =
h.slice(0, 8) +
"-" +
h.slice(8, 12) +
"-" +
h.slice(12, 16) +
"-" +
h.slice(16, 20) +
"-" +
h.slice(20, 32);
client.authUuid = offlineUuid;
client.authUsername = username || "Player";
client.authState = "done";
this._sendAuthResult(client, true, offlineUuid, client.authUsername, "");
} else {
const msAuth = require("./auth");
const finish = () => {
const raw = msAuth.uuid || "";
const uuid = raw.includes("-")
? raw
: raw.slice(0, 8) +
"-" +
raw.slice(8, 12) +
"-" +
raw.slice(12, 16) +
"-" +
raw.slice(16, 20) +
"-" +
raw.slice(20, 32);
client.authUuid = uuid;
client.authUsername = msAuth.username;
client.authState = "done";
this._sendAuthResult(client, true, uuid, msAuth.username, "");
};
if (msAuth.isAuthenticated()) {
finish();
} else {
msAuth
.authenticate()
.then(finish)
.catch(() => {
this._sendAuthResult(
client,
false,
"",
"",
"Authentication failed",
);
});
}
}
}
_sendAuthResult(client, success, uuid, username, errorMessage) {
const writer = new PacketWriter();
writer.writeBoolean(success);
writer.writeString(uuid);
writer.writeString(username);
writer.writeString(errorMessage);
writer.writeString("");
writer.writeInt(0);
this.sendPacket(client, 0xac, writer.toBuffer());
if (!success) setTimeout(() => this.disconnectClient(client, 2), 200);
}
handleLogin(client, data) {
if (client.authState !== "done") {
this.disconnectClient(client, 2);
return;
}
const reader = new PacketReader(data.slice(1));
const clientVersion = reader.readInt();
const username = reader.readString();
client.state = "login";
client.username = username;
if (client.javaClient) {
const oldJavaClient = client.javaClient;
client.javaClient = null;
oldJavaClient.removeAllListeners();
oldJavaClient.on("error", () => {
/* do nothing */
});
try {
oldJavaClient.end();
} catch (err) {
/* do nothing */
}
}
this.enterLobby(client);
}
sendChatToClient(client, text) {
const writer = new PacketWriter();
writer.writeShort(0);
writer.writeShort(0x10);
writer.writeString(text);
this.sendPacket(client, 0x03, writer.toBuffer());
}
enterLobby(client, message) {
client.inLobby = true;
client.javaGameMode = 2;
client.javaDimension = 0;
client.javaSpawnX = 0.5;
client.javaSpawnY = 65.0;
client.javaSpawnZ = 0.5;
client.javaYaw = 0;
client.javaPitch = 0;
client.javaPlayers = new Map();
client.javaEntities = new Map();
client.javaToLceEntityId = new Map();
client.nextLceEntityId = 100;
client.loadedChunks = new Set();
client.heldSlot = 0;
client.inventory = {};
client._windowInventories = {};
client._currentChest = null;
client._deadEntities = new Set();
client._destroyedEntities = new Set();
client._dyingPlayers = new Map();
client._lastJavaPackets = [];
client._bookGUI = null;
client._craftingInProgress = false;
client._pendingAttacks = new Set();
client._lastAttacks = new Map();
client._itemUseState = {};
client.openWindowId = null;
client.openWindowType = null;
client.isRiding = false;
client.abilities = {};
client.serverConfig = null;
client.state = "play";
this.sendLCELoginResponse(client);
for (const other of this.clients) {
if (other === client || other.state !== "play" || other.inLobby) continue;
this.sendAddPlayerPacket(client, {
entityId: other.smallId,
name: other.username || "Player",
x: other.playerX || 0,
y: other.playerY || 65,
z: other.playerZ || 0,
yaw: other.playerYaw || 0,
pitch: other.playerPitch || 0,
});
}
for (const other of this.clients) {
if (other === client || other.state !== "play" || other.inLobby) continue;
this.sendAddPlayerPacket(other, {
entityId: client.smallId,
name: client.username || "Player",
x: 0.5,
y: 65,
z: 0.5,
yaw: 0,
pitch: 0,
});
}
for (let cx = -2; cx <= 2; cx++) {
for (let cz = -2; cz <= 2; cz++) {
this.sendFlatChunk(client, cx, cz);
}
}
const welcomeDelay = message ? 200 : 500;
if (message) {
setTimeout(() => {
this.sendChatToClient(client, message);
}, 200);
}
setTimeout(
() => {
this.sendChatToClient(
client,
"Welcome to LegacyCrossPlay - developed by @DeveloperExotic!",
);
},
message ? 600 : 500,
);
setTimeout(
() => {
this.sendChatToClient(
client,
"Type /lce connect <ip> to join a Java Edition server",
);
},
message ? 900 : 800,
);
}
sendLCELoginResponse(client) {
const writer = new PacketWriter();
writer.writeInt(client.smallId); //entityId
//we use these later
client.lcePlayerEntityId = client.smallId;
writer.writeString(""); //username
writer.writeString("default"); //levelTypeName
writer.writeLong(0); //seed
writer.writeInt(client.javaGameMode || 0); //gamemode (synced from java server)
const dimension = client.javaDimension || 0;
writer.writeByte(dimension & 0xff); //dimension
//nether/end use 128 height, overworld uses 256
const loginMapHeight = dimension === 0 ? 256 : 128;
writer.writeByte(loginMapHeight - 1); //mapHeight sent as (height - 1)
writer.writeByte(8); //maxPlayers
writer.writeLong(0); //offlineXuid
writer.writeLong(0); //onlineXuid
writer.writeBoolean(false); //friendsOnlyUGC
writer.writeInt(0); //ugcPlayersVersion
writer.writeByte(1); //difficulty (hardcoded to Easy)
writer.writeInt(0); //multiplayerInstanceId
writer.writeByte(client.smallId); //playerIndex
writer.writeInt(0); //playerSkinId
writer.writeInt(0); //playerCapeId
writer.writeBoolean(false); //isGuest
writer.writeBoolean(false); //newSeaLevel
writer.writeInt(0); //uiGamePrivileges
writer.writeShort(864); //xzSize
writer.writeByte(8); //hellScale
this.sendPacket(client, 0x01, writer.toBuffer());
this.sendPostLoginPackets(client);
}
sendPostLoginPackets(client) {
const spawnX = Math.floor(client.javaSpawnX || 0);
const spawnY = Math.floor(client.javaSpawnY || 65);
const spawnZ = Math.floor(client.javaSpawnZ || 0);
const spawnWriter = new PacketWriter();
spawnWriter.writeInt(spawnX);
spawnWriter.writeInt(spawnY);
spawnWriter.writeInt(spawnZ);
this.sendPacket(client, 6, spawnWriter.toBuffer());
const abilitiesWriter = new PacketWriter();
const invulnerable = client.abilities?.invulnerable || false;
const flying = client.abilities?.flying || false;
const canFly =
client.abilities?.canFly ||
client.gameMode === 1 ||
client.gameMode === 3;
const instabuild = client.gameMode === 1; //dunno why this controls the bow requiring arrows on LCE, but it does
let bitfield = 0;
if (invulnerable) bitfield |= 1;
if (flying) bitfield |= 2;
if (canFly) bitfield |= 4;
if (instabuild) bitfield |= 8;
abilitiesWriter.writeByte(bitfield);
abilitiesWriter.writeFloat(0.05); //flySpeed
abilitiesWriter.writeFloat(0.1); //walkSpeed
this.sendPacket(client, 202, abilitiesWriter.toBuffer());
const carriedWriter = new PacketWriter();
carriedWriter.writeShort(0); //selectedSlot
this.sendPacket(client, 16, carriedWriter.toBuffer());
const lceSpawnY = (client.javaSpawnY || 65.0) + 1.65; //lce has an eye offset of 1.62, added 3 so we don't spawn in the ground
const moveWriter = new PacketWriter();
moveWriter.writeDouble(client.javaSpawnX || 0.5);
moveWriter.writeDouble(lceSpawnY); //feet position with offset
moveWriter.writeDouble(lceSpawnY + 1.62); //eye height above feet
moveWriter.writeDouble(client.javaSpawnZ || 0.5);
moveWriter.writeFloat(client.javaYaw || 0.0);
moveWriter.writeFloat(client.javaPitch || 0.0);
moveWriter.writeByte(0x01); //onGround
const movePayload = moveWriter.toBuffer();
this.sendPacket(client, 0x0d, movePayload);
//translating between 1.8 and 1.9+ servers for viaversion requires a position packet from the
//client to generate the TeleportConfirm packet that 1.9+ servers expect before accepting movement.
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("position_look", {
x: client.javaSpawnX || 0.5,
y: client.javaSpawnY || 65.0,
z: client.javaSpawnZ || 0.5,
yaw: client.javaYaw || 0.0,
pitch: client.javaPitch || 0.0,
onGround: true,
});
} catch (err) {
/* do nothing */
}
}
const timeWriter = new PacketWriter();
timeWriter.writeLong(1000); //gameTime (world age)
timeWriter.writeLong(6000); //dayTime
this.sendPacket(client, 4, timeWriter.toBuffer());
//console.log("YEEEEEEEEEEEEEEEEEEE");
}
mapJavaEntityIdToLce(client, javaEntityId) {
if (!client.javaToLceEntityId.has(javaEntityId)) {
const lceEntityId = client.nextLceEntityId++;
if (lceEntityId > 32767) {
return 100;
}
client.javaToLceEntityId.set(javaEntityId, lceEntityId);
}
return client.javaToLceEntityId.get(javaEntityId);
}
sendMovePlayerPacket(client, x, y, z, yaw, pitch) {
const lceY = y + 1.65;
const moveWriter = new PacketWriter();
moveWriter.writeDouble(x);
moveWriter.writeDouble(lceY); //feet position with offset
moveWriter.writeDouble(lceY + 1.62); //eye height above feet
moveWriter.writeDouble(z);
moveWriter.writeFloat(yaw);
moveWriter.writeFloat(pitch);
moveWriter.writeByte(0x01); //onGround
this.sendPacket(client, 0x0d, moveWriter.toBuffer());
}
sendTexturePacket(client, textureName, textureData) {
if (!client || !client.socket || client.socket.destroyed) {
return;
}
const writer = new PacketWriter();
writer.writeUTF(textureName);
writer.writeShort(textureData.length);
writer.writeBytes(textureData);
this.sendPacket(client, 0x9a, writer.toBuffer());
}
sendTextureChangePacket(client, entityId, textureName, textureType = 0) {
if (!client || !client.socket || client.socket.destroyed) {
return;
}
const writer = new PacketWriter();
writer.writeInt(entityId);
writer.writeByte(textureType);
writer.writeUTF(textureName);
this.sendPacket(client, 0x9d, writer.toBuffer());
}
sendAddPlayerPacket(client, playerInfo) {
let playerName = playerInfo.name || "undefined";
if (playerInfo.isNPC) {
playerName = "";
}
if (playerName.length > 20) {
playerName = playerName.substring(0, 20);
}
if (playerName.length === 0 && !playerInfo.isNPC) {
playerName = "undefined";
}
const writer = new PacketWriter();
writer.writeInt(playerInfo.entityId);
writer.writeString(playerName);
//pos
writer.writeInt(Math.floor(playerInfo.x * 32));
writer.writeInt(Math.floor(playerInfo.y * 32));
writer.writeInt(Math.floor(playerInfo.z * 32));
//rot
const yRotByte = Math.floor(((playerInfo.yaw % 360) / 360) * 256) & 0xff;
const xRotByte = Math.floor(((playerInfo.pitch % 360) / 360) * 256) & 0xff;
writer.writeByte(yRotByte);
writer.writeByte(xRotByte);
writer.writeByte(yRotByte); //yHeadRot
writer.writeShort(0); //carriedItem
writer.writeLong(0); //xuid
writer.writeLong(0); //OnlineXuid
writer.writeByte(0); //playerIndex
const skinIdToSend = playerInfo.skinId || 0;
writer.writeInt(skinIdToSend); //skinId
writer.writeInt(0); //capeId
writer.writeInt(0); //uiGamePrivileges
const sharedFlagsHeader = (0 << 5) | 0;
writer.writeByte(sharedFlagsHeader);
writer.writeByte(0);
const airSupplyHeader = (1 << 5) | 1;
writer.writeByte(airSupplyHeader);
writer.writeShort(300);
const healthHeader = (3 << 5) | 6;
writer.writeByte(healthHeader);
writer.writeFloat(20.0);
const effectColorHeader = (2 << 5) | 7;
writer.writeByte(effectColorHeader);
writer.writeInt(0);
const effectAmbienceHeader = (0 << 5) | 8;
writer.writeByte(effectAmbienceHeader);
writer.writeByte(0);
const arrowCountHeader = (0 << 5) | 9;
writer.writeByte(arrowCountHeader);
writer.writeByte(0);
const playerFlagsHeader = (0 << 5) | 16;
writer.writeByte(playerFlagsHeader);
writer.writeByte(0);
const absorptionHeader = (3 << 5) | 17;
writer.writeByte(absorptionHeader);
writer.writeFloat(0.0);
const scoreHeader = (2 << 5) | 18;
writer.writeByte(scoreHeader);
writer.writeInt(0);
writer.writeByte(0x7f);
this.sendPacket(client, 0x14, writer.toBuffer());
if (playerInfo.customTextureName) {
this.sendTextureChangePacket(
client,
playerInfo.entityId,
playerInfo.customTextureName,
);
}
}
sendMoveEntityPacket(client, playerInfo) {
//MoveEntityPacket can only handle movements of ~4 blocks
//just teleport for simplicity
this.sendTeleportEntityPacket(client, playerInfo);
}
sendTeleportEntityPacket(client, playerInfo) {
const writer = new PacketWriter();
writer.writeShort(playerInfo.entityId);
//pos
writer.writeInt(Math.floor(playerInfo.x * 32));
writer.writeInt(Math.floor(playerInfo.y * 32));
writer.writeInt(Math.floor(playerInfo.z * 32));
//rot
const yRotByte = Math.floor(((playerInfo.yaw % 360) / 360) * 256) & 0xff;
const xRotByte = Math.floor(((playerInfo.pitch % 360) / 360) * 256) & 0xff;
writer.writeByte(yRotByte);
writer.writeByte(xRotByte);
this.sendPacket(client, 34, writer.toBuffer()); //TeleportEntityPacket
this.sendRotateHeadPacket(client, playerInfo);
}
sendRotateHeadPacket(client, playerInfo) {
const writer = new PacketWriter();
writer.writeInt(playerInfo.entityId);
const yHeadRotByte =
Math.floor(((playerInfo.yaw % 360) / 360) * 256) & 0xff;
writer.writeByte(yHeadRotByte);
this.sendPacket(client, 35, writer.toBuffer()); //RotateHeadPacket
}
getJavaEntityType(typeId) {
const typeMap = {
50: "Creeper",
51: "Skeleton",
52: "Spider",
53: "Giant",
54: "Zombie",
55: "Slime",
56: "Ghast",
57: "PigZombie",
58: "Enderman",
59: "CaveSpider",
60: "Silverfish",
61: "Blaze",
62: "LavaSlime",
63: "EnderDragon",
64: "WitherBoss",
65: "Bat",
66: "Witch",
90: "Pig",
91: "Sheep",
92: "Cow",
93: "Chicken",
94: "Squid",
101: "Rabbit",
95: "Wolf",
96: "MushroomCow",
97: "SnowMan",
98: "Ozelot",
99: "VillagerGolem",
100: "EntityHorse",
120: "Villager",
71: "ItemFrame",
};
return typeMap[typeId] || "Unknown";
}
sendAddMobPacket(client, entityInfo) {
return packetSenders.sendAddMobPacket(this, client, entityInfo);
}
sendAddEntityPacket(client, entityInfo) {
return packetSenders.sendAddEntityPacket(this, client, entityInfo);
}
sendMoveEntityPacketForEntity(client, entity, dX, dY, dZ, includeRotation) {
return packetSenders.sendMoveEntityPacketForEntity(
this,
client,
entity,
dX,
dY,
dZ,
includeRotation,
);
}
sendTeleportEntityPacketForEntity(client, entity) {
return packetSenders.sendTeleportEntityPacketForEntity(
this,
client,
entity,
);
}
sendRotateEntityPacketForEntity(client, entity) {
return packetSenders.sendRotateEntityPacketForEntity(this, client, entity);
}
sendRotateHeadPacketForEntity(client, entity) {
return packetSenders.sendRotateHeadPacketForEntity(this, client, entity);
}
sendEntityMetadataPacket(client, entity, metadata) {
return packetSenders.sendEntityMetadataPacket(
this,
client,
entity,
metadata,
);
}
sendSetEntityMotionPacket(client, entity) {
return packetSenders.sendSetEntityMotionPacket(this, client, entity);
}
sendEntityEventPacket(client, entity, eventId) {
return packetSenders.sendEntityEventPacket(this, client, entity, eventId);
}
sendSetEquippedItemPacket(client, entity, slot, item) {
return packetSenders.sendSetEquippedItemPacket(
this,
client,
entity,
slot,
item,
);
}
sendSetItemDataPacket(client, entity, itemData) {
return packetSenders.sendSetItemDataPacket(this, client, entity, itemData);
}
sendSetItemFrameDataPacket(client, entity, itemData, rotation) {
return packetSenders.sendSetItemFrameDataPacket(
this,
client,
entity,
itemData,
rotation,
);
}
sendSetEntityDataPacket(client, entity) {
return packetSenders.sendSetEntityDataPacket(this, client, entity);
}
sendUpdateAttributesPacket(client, entity) {
return packetSenders.sendUpdateAttributesPacket(this, client, entity);
}
sendSetEntityLinkPacket(client, sourceId, destId, linkType) {
return packetSenders.sendSetEntityLinkPacket(
this,
client,
sourceId,
destId,
linkType,
);
}
sendLevelSoundPacket(client, soundId, x, y, z, volume, pitch) {
return packetSenders.sendLevelSoundPacket(
this,
client,
soundId,
x,
y,
z,
volume,
pitch,
);
}
sendLevelParticlesPacket(
client,
name,
x,
y,
z,
xDist,
yDist,
zDist,
maxSpeed,
count,
) {
return packetSenders.sendLevelParticlesPacket(
this,
client,
name,
x,
y,
z,
xDist,
yDist,
zDist,
maxSpeed,
count,
);
}
sendTileUpdatePacket(client, x, y, z, blockId, metadata) {
return packetSenders.sendTileUpdatePacket(
this,
client,
x,
y,
z,
blockId,
metadata,
);
}
sendContainerOpenPacket(client, packet) {
return packetSenders.sendContainerOpenPacket(this, client, packet);
}
sendVillagerTrades(client, windowId, packetData) {
return packetSenders.sendVillagerTrades(this, client, windowId, packetData);
}
sendBookAsChestGUI(client, bookItem) {
if (!bookItem.nbt) {
return;
}
let bookTitle = "Book";
if (bookItem.nbt.value && bookItem.nbt.value.title) {
bookTitle = bookItem.nbt.value.title.value || "Book";
}
let pages = [];
if (bookItem.nbt.value && bookItem.nbt.value.pages) {
if (
bookItem.nbt.value.pages.value &&
bookItem.nbt.value.pages.value.value
) {
pages = bookItem.nbt.value.pages.value.value;
} else if (Array.isArray(bookItem.nbt.value.pages)) {
pages = bookItem.nbt.value.pages;
}
}
if (pages.length === 0) {
return;
}
if (!client._bookGUI) {
client._bookGUI = {
pages: pages,
currentPage: 0,
clickActions: {},
bookTitle: bookTitle,
};
} else {
client._bookGUI.clickActions = {};
client._bookGUI.bookTitle = bookTitle;
}
const windowId = 1;
client.openWindowId = windowId;
client.openWindowType = "minecraft:chest";
const fakeOpenPacket = {
windowId: windowId,
inventoryType: "minecraft:chest",
slotCount: 54,
windowTitle: JSON.stringify({
text: `Written Book (${bookTitle})`,
extra: [],
}),
};
this.sendContainerOpenPacket(client, fakeOpenPacket);
this.updateBookGUIContents(client, windowId);
}
updateBookGUIContents(client, windowId) {
if (!client._bookGUI) return;
const currentPage = client._bookGUI.currentPage;
const pages = client._bookGUI.pages;
const pageContent = pages[currentPage];
const items = [];
for (let i = 0; i < 54; i++) {
items.push({ blockId: -1 });
}
if (currentPage > 0) {
items[48] = {
blockId: 35,
itemCount: 1,
itemDamage: 14,
displayName: "Previous Page",
};
}
if (currentPage < pages.length - 1) {
items[50] = {
blockId: 35,
itemCount: 1,
itemDamage: 5,
displayName: "Next Page",
};
}
try {
const pageText = JSON.parse(pageContent);
let lines = [];
if (Array.isArray(pageText)) {
lines = pageText;
} else if (pageText.extra) {
lines = pageText.extra;
} else {
lines = [pageText];
}
let slotIndex = 0;
for (let i = 0; i < lines.length && slotIndex < 36; i++) {
const line = lines[i];
let text = "";
let clickAction = null;
if (typeof line === "string") {
text = line;
} else if (line && typeof line === "object") {
if (line.text) {
text = line.text;
}
if (line.extra && Array.isArray(line.extra)) {
for (const extraPart of line.extra) {
if (typeof extraPart === "string") {
text += extraPart;
} else if (extraPart.text) {
text += extraPart.text;
}
if (extraPart.clickEvent) {
clickAction = {
type: extraPart.clickEvent.action,
value: extraPart.clickEvent.value,
};
}
}
}
}
text = text.replace(/§[0-9a-fk-or]/gi, "").replace(/\\n/g, "\n");
const textLines = text
.split("\n")
.map((l) => l.trim())
.filter((l) => l.length > 0 && l !== " ");
for (const textLine of textLines) {
if (slotIndex >= 36) break;
if (textLine.length > 0 && textLine !== " ") {
let itemId = 339;
let itemDamage = 0;
let isEnchanted = false;
if (clickAction) {
if (clickAction.type === "open_url") {
itemId = 345;
itemDamage = 0;
} else if (clickAction.type === "run_command") {
itemId = 340;
itemDamage = 0;
}
isEnchanted = true;
}
const chunks = [];
for (let pos = 0; pos < textLine.length; pos += 32) {
chunks.push(textLine.substring(pos, pos + 32));
}
for (const chunk of chunks) {
if (slotIndex >= 36) break;
items[slotIndex] = {
blockId: itemId,
itemCount: 1,
itemDamage: itemDamage,
displayName: chunk,
enchanted: isEnchanted,
};
if (clickAction) {
client._bookGUI.clickActions[slotIndex] = clickAction;
}
slotIndex++;
}
}
}
}
} catch (err) {
items[0] = {
blockId: 339,
itemCount: 1,
itemDamage: 0,
displayName: "Error loading page",
};
}
let itemCount = 0;
for (let i = 0; i < items.length; i++) {
if (items[i].blockId !== -1) {
itemCount++;
}
}
const PacketWriter = require("./packetwriter");
const invWriter = new PacketWriter();
invWriter.writeByte(windowId);
invWriter.writeShort(items.length);
for (const item of items) {
this.writeItem(invWriter, item);
}
this.sendPacket(client, 0x68, invWriter.toBuffer());
}
//debug
sendFlatChunk(client, chunkX, chunkZ) {
const xs = 16;
const ys = 256;
const zs = 16;
const blockCount = xs * ys * zs;
const halfBlockCount = blockCount / 2;
const biomeSize = xs * zs;
const rawSize = blockCount + 3 * halfBlockCount + biomeSize;
const rawData = Buffer.alloc(rawSize);
let offset = 0;
for (let y = 0; y < ys; y++) {
for (let z = 0; z < zs; z++) {
for (let x = 0; x < xs; x++) {
let blockId = 0;
if (y < 65) {
blockId = 1;
}
if (y === 64) {
blockId = 2;
}
rawData[offset++] = blockId;
}
}
}
const metadataOffset = offset;
for (let x = 0; x < xs; x++) {
for (let z = 0; z < zs; z++) {
for (let yPair = 0; yPair < ys / 2; yPair++) {
rawData[offset++] = 0;
}
}
}
for (let x = 0; x < xs; x++) {
for (let z = 0; z < zs; z++) {
for (let yPair = 0; yPair < ys / 2; yPair++) {
rawData[offset++] = 0;
}
}
}
for (let x = 0; x < xs; x++) {
for (let z = 0; z < zs; z++) {
for (let yPair = 0; yPair < ys / 2; yPair++) {
rawData[offset++] = 0xff;
}
}
}
for (let i = 0; i < biomeSize; i++) {
rawData[offset++] = 1;
}
const rleCompressed = compressRLE(rawData);
const compressed = zlib.deflateSync(rleCompressed, { level: 6 });
const writer = new PacketWriter();
writer.writeByte(0x01);
const worldX = chunkX * 16;
const worldY = 0;
const worldZ = chunkZ * 16;
writer.writeInt(worldX);
writer.writeShort(worldY);
writer.writeInt(worldZ);
writer.writeByte(xs - 1);
writer.writeByte(ys - 1);
writer.writeByte(zs - 1);
const levelIdx = 0;
const sizeAndLevel = compressed.length | (levelIdx << 30);
writer.writeInt(sizeAndLevel);
const payload = writer.toBuffer();
const fullPayload = Buffer.concat([payload, compressed]);
const visWriter = new PacketWriter();
visWriter.writeInt(chunkX);
visWriter.writeInt(chunkZ);
visWriter.writeByte(1);
this.sendPacket(client, 0x32, visWriter.toBuffer());
const chunkKey = `${chunkX},${chunkZ}`;
if (client.loadedChunks) {
client.loadedChunks.add(chunkKey);
}
this.sendPacket(client, 0x33, fullPayload);
}
handleKeepAlive(client, data) {
const reader = new PacketReader(data.slice(1));
const keepAliveId = reader.readInt();
const writer = new PacketWriter();
writer.writeInt(keepAliveId);
this.sendPacket(client, 0x00, writer.toBuffer());
}
handleInteract(client, packetData) {
const reader = new PacketReader(packetData.slice(1));
const sourceId = reader.readInt();
const targetId = reader.readInt();
const action = reader.readByte();
if (action === 0) {
const heldSlot = client.heldSlot || 0;
const heldItem = client.inventory
? client.inventory[36 + heldSlot]
: null;
const heldItemId = heldItem ? heldItem.blockId : -1;
const nonInteractiveItems = [
346, //fishing rod
261, //bow
//foods
260,
297,
282,
350,
349,
360,
363,
365,
366,
320,
357,
391,
392,
393,
394,
//potions
373,
438,
//buckets (except milk)
326,
327,
//ender pearl, snowball, egg
368,
332,
344,
];
if (nonInteractiveItems.includes(heldItemId)) {
return;
}
}
if (client._destroyedEntities && client._destroyedEntities.has(targetId)) {
return;
}
if (action === 1) {
const now = Date.now();
if (!client._lastAttacks) client._lastAttacks = new Map();
const lastAttackTime = client._lastAttacks.get(targetId) || 0;
if (now - lastAttackTime < 500) {
return;
}
client._lastAttacks.set(targetId, now);
if (client._lastAttacks.size > 100) {
const oldestEntry = Array.from(client._lastAttacks.entries()).sort(
(a, b) => a[1] - b[1],
)[0];
client._lastAttacks.delete(oldestEntry[0]);
}
}
const targetEntity = Array.from(client.javaEntities.values()).find(
(e) => e.entityId === targetId,
);
const entityType = targetEntity
? `${targetEntity.javaType}(${targetEntity.type})`
: "UNKNOWN";
let targetJavaId = -1;
for (const [javaId, entity] of client.javaEntities) {
if (entity.entityId === targetId) {
if (client._deadEntities && client._deadEntities.has(javaId)) {
return;
}
targetJavaId = javaId;
break;
}
}
for (const [uuid, player] of client.javaPlayers) {
if (player.entityId === targetId && player.spawned) {
targetJavaId = player.javaEntityId;
break;
}
}
if (targetJavaId !== -1 && client.javaClient) {
let entityExists = client.javaEntities.has(targetJavaId);
if (!entityExists) {
for (const [uuid, player] of client.javaPlayers) {
if (player.javaEntityId === targetJavaId && player.spawned) {
entityExists = true;
break;
}
}
}
if (!entityExists) {
return;
}
if (action === 1) {
if (!client._pendingAttacks) client._pendingAttacks = new Set();
if (client._pendingAttacks.has(targetJavaId)) {
return;
}
client._pendingAttacks.add(targetJavaId);
setTimeout(() => {
if (client._pendingAttacks) {
client._pendingAttacks.delete(targetJavaId);
}
}, 150);
client.javaClient.write("use_entity", {
target: targetJavaId,
mouse: 1,
sneaking: false,
});
} else {
client.javaClient.write("use_entity", {
target: targetJavaId,
mouse: 0,
sneaking: false,
});
}
} else {
const allEntities = Array.from(client.javaEntities.values()).map(
(e) => `LCE:${e.entityId}=Java:${e.javaEntityId}(${e.javaType})`,
);
}
}
handleAnimate(client, packetData, isBundled = false) {
try {
const reader = new PacketReader(packetData.slice(1));
const entityId = reader.readInt();
const action = reader.readByte();
if (client.javaClient && action === 1) {
client.javaClient.write("arm_animation", {
hand: 0,
});
const targetEntity = this.raycastEntity(client);
if (targetEntity) {
const heldSlot = client.heldSlot || 0;
const heldItem = client.inventory
? client.inventory[36 + heldSlot]
: null;
const heldItemId = heldItem ? heldItem.blockId : -1;
const useItems = [
346, //fishing rod
261, //bow
];
if (useItems.includes(heldItemId)) {
return;
}
const entity = client.javaEntities.get(targetEntity);
let isPlayer = false;
if (!entity) {
for (const [uuid, player] of client.javaPlayers) {
if (player.javaEntityId === targetEntity && player.spawned) {
isPlayer = true;
break;
}
}
if (!isPlayer) {
return;
}
} else {
if (!entity.spawnTime) {
return;
}
const now = Date.now();
const timeSinceSpawn = now - entity.spawnTime;
if (timeSinceSpawn < 500) {
return;
}
}
if (client._deadEntities && client._deadEntities.has(targetEntity)) {
return;
}
if (!client._pendingAttacks) client._pendingAttacks = new Set();
if (client._pendingAttacks.has(targetEntity)) {
return;
}
client._pendingAttacks.add(targetEntity);
setTimeout(() => {
if (client._pendingAttacks) {
client._pendingAttacks.delete(targetEntity);
}
}, 150);
client.javaClient.write("use_entity", {
target: targetEntity,
mouse: 1,
sneaking: false,
});
}
}
} catch (err) {
/* do nothing */
}
}
raycastEntity(client) {
if (!client.playerX || !client.playerYaw || !client.playerPitch) {
return null;
}
const playerX = client.playerX;
const playerY = client.playerY + 1.62; //eye height
const playerZ = client.playerZ;
const yaw = client.playerYaw;
const pitch = client.playerPitch;
const yawRad = ((yaw + 90) * Math.PI) / 180;
const pitchRad = (pitch * Math.PI) / 180;
const dirX = Math.cos(pitchRad) * Math.cos(yawRad);
const dirY = -Math.sin(pitchRad);
const dirZ = Math.cos(pitchRad) * Math.sin(yawRad);
//max hit distance
const maxDistance = 10.0;
let closestEntity = null;
let closestDistance = maxDistance;
for (const [javaId, entity] of client.javaEntities) {
if (!entity.x || !entity.y || !entity.z) continue;
const isLivingEntity =
(entity.type >= 48 && entity.type <= 66) || //monsters
(entity.type >= 90 && entity.type <= 100) || //animals
entity.type === 120; //villagers
if (!isLivingEntity) continue;
const entityX = entity.x;
const entityY = entity.y + 1.0; //probably the center
const entityZ = entity.z;
const toEntityX = entityX - playerX;
const toEntityY = entityY - playerY;
const toEntityZ = entityZ - playerZ;
const distance = Math.sqrt(
toEntityX * toEntityX + toEntityY * toEntityY + toEntityZ * toEntityZ,
);
if (distance > maxDistance || distance > closestDistance) continue;
const dot =
(toEntityX * dirX + toEntityY * dirY + toEntityZ * dirZ) / distance;
//hit fov
if (dot > 0.97) {
closestEntity = javaId;
closestDistance = distance;
}
}
for (const [uuid, player] of client.javaPlayers) {
if (!player.spawned || !player.x || !player.y || !player.z) continue;
if (!player.javaEntityId) continue;
const playerEntityX = player.x;
const playerEntityY = player.y + 1.0; //probably the center
const playerEntityZ = player.z;
const toPlayerX = playerEntityX - playerX;
const toPlayerY = playerEntityY - playerY;
const toPlayerZ = playerEntityZ - playerZ;
const distance = Math.sqrt(
toPlayerX * toPlayerX + toPlayerY * toPlayerY + toPlayerZ * toPlayerZ,
);
if (distance > maxDistance || distance > closestDistance) continue;
const dot =
(toPlayerX * dirX + toPlayerY * dirY + toPlayerZ * dirZ) / distance;
//hit fov
if (dot > 0.97) {
closestEntity = player.javaEntityId;
closestDistance = distance;
}
}
return closestEntity;
}
handleMovePlayer(client, data) {
const reader = new PacketReader(data.slice(1));
const dataSize = data.length - 1;
let x, y, yView, z, yRot, xRot, onGround;
if (dataSize >= 33) {
x = reader.readDouble();
y = reader.readDouble();
yView = reader.readDouble();
z = reader.readDouble();
if (dataSize >= 41) {
yRot = reader.readFloat();
xRot = reader.readFloat();
} else {
yRot = client.lastYaw || 0;
xRot = client.lastPitch || 0;
}
onGround = reader.readByte();
} else if (dataSize >= 9) {
yRot = reader.readFloat();
xRot = reader.readFloat();
onGround = reader.readByte();
x = client.playerX || 0;
y = client.playerY || 0;
yView = y;
z = client.playerZ || 0;
} else {
return;
}
const normalizedYaw = normalizeYaw(yRot);
client.playerX = x;
client.playerY = y;
client.playerZ = z;
client.playerYaw = normalizedYaw;
client.playerPitch = xRot;
client.lastYaw = normalizedYaw;
client.lastPitch = xRot;
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("position_look", {
x: x,
y: y,
z: z,
yaw: normalizedYaw,
pitch: xRot,
onGround: onGround !== 0,
});
} catch (err) {
/* do nothing */
}
}
}
handleUseItem(client, data) {
const reader = new PacketReader(data.slice(1));
const x = reader.readInt();
const y = reader.readByte();
const z = reader.readInt();
const face = reader.readByte();
const itemId = reader.readShort();
let itemCount = 0;
let itemAux = 0;
if (itemId !== -1) {
itemCount = reader.readByte();
itemAux = reader.readShort();
}
const clickX = reader.readByte() / 32.0;
const clickY = reader.readByte() / 32.0;
const clickZ = reader.readByte() / 32.0;
if (x !== -1 && y !== -1 && z !== -1 && face >= 0 && face <= 5) {
client._lastBlockClicked = { x, y, z };
if (client.javaClient && client.javaClient.write) {
try {
let javaItemId = -1;
if (itemId !== -1) {
javaItemId = this.mapLCEItemToJava(itemId);
}
let heldItem;
if (javaItemId !== -1 && javaItemId !== 0) {
heldItem = {
blockId: javaItemId,
itemCount: itemCount || 1,
itemDamage: itemAux || 0,
};
} else {
heldItem = {
blockId: -1,
itemCount: 0,
itemDamage: 0,
};
}
const javaFace = face;
const cursorX = Math.floor(clickX * 16);
const cursorY = Math.floor(clickY * 16);
const cursorZ = Math.floor(clickZ * 16);
client.javaClient.write("block_place", {
location: { x: x, y: y, z: z },
direction: javaFace,
heldItem: heldItem,
cursorX: cursorX,
cursorY: cursorY,
cursorZ: cursorZ,
});
} catch (err) {
/* do nothing */
}
}
} else {
//LCE also sends InteractPacket separately for entity interaction, so we ignore that
if (client.javaClient) {
if (itemId !== -1) {
client._lastHeldItem = {
itemId: itemId,
itemCount: itemCount,
itemAux: itemAux,
};
}
const useItemId =
itemId !== -1
? itemId
: client._lastHeldItem
? client._lastHeldItem.itemId
: -1;
const useItemCount =
itemId !== -1
? itemCount
: client._lastHeldItem
? client._lastHeldItem.itemCount
: 0;
const useItemAux =
itemId !== -1
? itemAux
: client._lastHeldItem
? client._lastHeldItem.itemAux
: 0;
if (useItemId !== -1) {
const javaItemId = this.mapLCEItemToJava(useItemId);
if (javaItemId !== -1 && javaItemId !== 0) {
const continuousUseItems = { 261: "bow", 346: "fishing_rod" };
const itemName = continuousUseItems[javaItemId];
if (itemName) {
if (!client._itemUseState) client._itemUseState = {};
const now = Date.now();
const lastUse = client._itemUseState[javaItemId] || 0;
if (now - lastUse > 100) {
const heldItem = {
blockId: javaItemId,
itemCount: useItemCount || 1,
itemDamage: useItemAux || 0,
};
client.javaClient.write("block_place", {
location: { x: -1, y: 255, z: -1 },
direction: -1,
heldItem: heldItem,
cursorX: 0,
cursorY: 0,
cursorZ: 0,
});
client._itemUseState[javaItemId] = now;
}
} else {
const heldItem = {
blockId: javaItemId,
itemCount: useItemCount || 1,
itemDamage: useItemAux || 0,
};
try {
const blockPlacePacket = {
location: { x: -1, y: 255, z: -1 },
direction: -1,
heldItem: heldItem,
cursorX: 0,
cursorY: 0,
cursorZ: 0,
};
client.javaClient.write("block_place", blockPlacePacket);
} catch (err) {
/* do nothing */
}
}
}
}
}
}
}
handleChat(client, data) {
const reader = new PacketReader(data.slice(1));
const messageType = reader.readShort();
const packedCounts = reader.readShort();
const stringCount = (packedCounts >> 4) & 0xf;
const intCount = (packedCounts >> 0) & 0xf;
if (stringCount > 0) {
const message = reader.readString();
if (message && message.startsWith("/lce ")) {
this.handleCommand(client, message);
return;
}
if (message && message.startsWith("/")) {
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("chat", { message });
} catch (err) {
/* do nothing */
}
}
return;
}
if (
client.javaClient &&
client.javaClient.write &&
message &&
message.length > 0
) {
try {
client.javaClient.write("chat", { message });
} catch (err) {
/* do nothing */
}
}
}
}
handleCommand(client, message) {
const parts = message.trim().split(/\s+/);
const cmd = parts[1]?.toLowerCase();
if (cmd === "connect") {
if (parts.length !== 3) {
this.sendChatToClient(client, "Command not valid");
return;
}
const arg = parts[2];
const colonCount = (arg.match(/:/g) || []).length;
if (colonCount > 1) {
this.sendChatToClient(client, "Command not valid");
return;
}
let host, port;
if (colonCount === 1) {
const split = arg.split(":");
host = split[0];
port = parseInt(split[1], 10);
if (!host || isNaN(port) || port < 1 || port > 65535) {
this.sendChatToClient(client, "Command not valid");
return;
}
} else {
host = arg;
port = 25565;
}
const displayAddr = colonCount === 1 ? arg : `${host}:${port}`;
this.sendChatToClient(client, `Connecting to ${displayAddr}`);
if (client.javaClient) {
const old = client.javaClient;
client.javaClient = null;
old.removeAllListeners();
old.on("error", () => {
/* do nothing */
});
try {
old.end();
} catch (e) {
/* do nothing */
}
}
client.inLobby = false;
client.state = "login";
client.serverConfig = {
host,
port,
name: displayAddr,
cracked: !(
client.useLinkedAccount !== false &&
require("./auth").hasCachedAccount()
),
};
this.connectToJavaServer(client);
} else if (cmd === "disconnect") {
if (parts.length !== 2) {
this.sendChatToClient(client, "Command not valid");
return;
}
if (client.inLobby || !client.javaClient) {
this.sendChatToClient(client, "Command not valid");
return;
}
const old = client.javaClient;
client.javaClient = null;
old.removeAllListeners();
old.on("error", () => {
/* do nothing */
});
try {
old.end();
} catch (e) {
/* do nothing */
}
this.enterLobby(client, "Disconnected from server");
} else if (cmd === "link") {
if (parts.length !== 2) {
this.sendChatToClient(client, "Usage: /lce link");
return;
}
const msAuth = require("./auth");
if (msAuth.hasCachedAccount()) {
this.sendChatToClient(client, "You already have an account linked!");
return;
}
this.sendChatToClient(client, "Starting Microsoft authentication...");
let linkInterval = null;
msAuth
.authenticate((data) => {
const code =
data.userCode ||
data.user_code ||
(data.message && data.message.match(/[A-Z0-9]{8,}/)?.[0]) ||
"???";
const msg = `To link your account, head to https://www.microsoft.com/link and enter code ${code}`;
this.sendChatToClient(client, msg);
linkInterval = setInterval(
() => this.sendChatToClient(client, msg),
1000,
);
})
.then(() => {
if (linkInterval) {
clearInterval(linkInterval);
linkInterval = null;
}
this.sendChatToClient(
client,
`Account successfully linked as ${msAuth.username}, you may now reconnect to the server`,
);
this.sendChatToClient(
client,
"Use /lce unlink to unlink your account",
);
this.sendChatToClient(
client,
"Or use /lce uselink <true/false> to toggle using your linked account",
);
})
.catch((err) => {
if (linkInterval) {
clearInterval(linkInterval);
linkInterval = null;
}
this.sendChatToClient(
client,
`Failed to link account: ${err.message || err}`,
);
});
} else if (cmd === "unlink") {
if (parts.length !== 2) {
this.sendChatToClient(client, "Usage: /lce unlink");
return;
}
const msAuth = require("./auth");
msAuth.unlink();
this.sendChatToClient(client, "Java Edition account unlinked.");
} else if (cmd === "uselink") {
if (parts.length !== 3 || (parts[2] !== "true" && parts[2] !== "false")) {
this.sendChatToClient(client, "Usage: /lce uselink <true/false>");
return;
}
client.useLinkedAccount = parts[2] === "true";
this.sendChatToClient(
client,
`Using linked account: ${client.useLinkedAccount}`,
);
} else {
this.sendChatToClient(
client,
"Command not valid. Use /lce connect <ip> or /lce disconnect",
);
}
}
handlePlayerAction(client, data, isBundled = false) {
const reader = new PacketReader(data.slice(1));
const action = reader.readByte();
const x = reader.readInt();
const y = reader.readByte();
const z = reader.readInt();
const face = reader.readByte();
//0=START_DESTROY_BLOCK
//1=ABORT_DESTROY_BLOCK
//2=STOP_DESTROY_BLOCK
//3=DROP_ALL_ITEMS
//4=DROP_ITEM
//5=RELEASE_USE_ITEM
if (action === 0) {
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("block_dig", {
status: 0,
location: { x: x, y: y, z: z },
face: face >= 0 ? face : 1,
});
} catch (err) {
/* do nothing */
}
}
} else if (action === 1) {
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("block_dig", {
status: 1,
location: { x: x, y: y, z: z },
face: face >= 0 ? face : 1,
});
} catch (err) {
/* do nothing */
}
}
} else if (action === 2) {
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("block_dig", {
status: 2,
location: { x: x, y: y, z: z },
face: face >= 0 ? face : 1,
});
} catch (err) {
/* do nothing */
}
}
} else if (action === 3 || action === 4) {
if (client.javaClient && client.javaClient.write) {
try {
const javaStatus = action === 4 ? 4 : 3;
const packet = {
status: javaStatus,
location: { x: 0, y: 0, z: 0 },
face: 0,
};
client.javaClient.write("block_dig", packet);
} catch (err) {
/* do nothing */
}
}
} else if (action === 5) {
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("block_dig", {
status: 5,
location: { x: x, y: y, z: z },
face: face,
});
} catch (err) {
/* do nothing */
}
}
}
}
handleSetCarriedItem(client, data) {
const reader = new PacketReader(data.slice(1));
const slot = reader.readShort();
if (slot < 0 || slot > 8) {
return;
}
client.heldSlot = slot;
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("held_item_slot", {
slotId: slot,
});
} catch (err) {
/* do nothing */
}
}
}
handlePlayerInput(client, data) {
const reader = new PacketReader(data.slice(1));
const xxa = reader.readFloat();
const yya = reader.readFloat();
const isJumping = reader.readBoolean();
const isSneaking = reader.readBoolean();
client.isSneaking = isSneaking;
//broken
if (isSneaking && client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("steer_vehicle", {
sideways: 0,
forward: 0,
jump: false,
unmount: true,
});
} catch (err) {
/* do nothing */
}
}
}
handlePlayerCommand(client, data) {
const reader = new PacketReader(data.slice(1));
const entityId = reader.readInt();
const action = reader.readByte();
const actionData = reader.readInt();
if (client.javaClient && client.javaClient.write) {
try {
let javaAction = -1;
if (action === 1) {
javaAction = 0;
if (client.isRiding) {
client.javaClient.write("steer_vehicle", {
sideways: 0,
forward: 0,
jump: false,
unmount: true,
});
}
} else if (action === 2) {
javaAction = 1; //java: STOP_SNEAKING
} else if (action === 3) {
javaAction = 2; //java: LEAVE_BED
} else if (action === 4) {
javaAction = 3; //java: START_SPRINTING
} else if (action === 5) {
javaAction = 4; //java: STOP_SPRINTING
} else if (action === 8) {
javaAction = 5; //java: RIDING_JUMP (open horse inventory)
} else if (action === 9) {
javaAction = 6; //java: OPEN_INVENTORY
}
if (javaAction !== -1) {
client.javaClient.write("entity_action", {
entityId: entityId,
actionId: javaAction,
jumpBoost: actionData || 0,
});
}
} catch (err) {
/* do nothing */
}
}
}
handlePlayerInput(client, data) {
const reader = new PacketReader(data.slice(1));
const xxa = reader.readFloat();
const yya = reader.readFloat();
const isJumping = reader.readBoolean();
const isSneaking = reader.readBoolean();
if (client.javaClient && client.javaClient.write) {
try {
if (client.isRiding) {
if (isSneaking && !client.sentSneak) {
client.sentSneak = true;
client.javaClient.write("entity_action", {
entityId: client.javaPlayerEntityId,
actionId: 0,
jumpBoost: 0,
});
return;
}
if (!isSneaking) {
client.sentSneak = false;
}
if (xxa !== 0 || yya !== 0 || isJumping) {
client.javaClient.write("steer_vehicle", {
sideways: xxa,
forward: yya,
jump: isJumping,
unmount: false,
});
}
}
} catch (err) {
/* do nothing */
}
}
}
handleContainerClose(client, data) {
const reader = new PacketReader(data.slice(1));
const containerId = reader.readByte();
if (client.openWindowId === containerId) {
client.openWindowType = null;
client.openWindowId = null;
}
if (client._currentChest && client._currentChest.windowId === containerId) {
const posKey = client._currentChest.posKey;
const chestData = this.openChests.get(posKey);
if (chestData) {
chestData.clients.delete(client);
if (chestData.clients.size === 0) {
this.openChests.delete(posKey);
}
}
if (this.clientChests.has(client)) {
this.clientChests.get(client).delete(posKey);
}
delete client._currentChest;
}
if (client._bookGUI && containerId === client.openWindowId) {
delete client._bookGUI;
return;
}
//LCE uses special window IDs
//-1=crafting table in inventory
//-2=creative inventory
//java doesn't send close packets for these, so we'll just skip them
if (containerId < 0) {
return;
}
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("close_window", {
windowId: containerId,
});
} catch (err) {
/* do nothing */
}
}
}
handleCustomPayload(client, data) {
const reader = new PacketReader(data.slice(1));
const identifier = reader.readString();
const length = reader.readShort();
const stringByteLength = identifier.length * 2;
const offset = 1 + 2 + stringByteLength + 2;
const payloadData = data.slice(offset, offset + length);
if (identifier === "MC|ItemName") {
const nameLength = payloadData.readUInt16BE(0);
const nameData = payloadData.slice(2, 2 + nameLength);
const itemName = nameData.toString("utf8");
if (client.javaClient && client.javaClient.write) {
try {
const stringBytes = Buffer.from(itemName, "utf8");
const varintLength = encodeVarInt(stringBytes.length);
const stringBuffer = Buffer.concat([varintLength, stringBytes]);
client._anvilItemName = itemName;
try {
const payloadPacket = {
channel: "MC|ItemName",
data: stringBuffer,
};
client.javaClient.write("custom_payload", payloadPacket);
} catch (writeErr) {
throw writeErr;
}
} catch (err) {
/* do nothing */
}
}
} else if (identifier === "MC|TrSel") {
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("custom_payload", {
channel: "MC|TrSel",
data: payloadData,
});
} catch (err) {
/* do nothing */
}
}
} else if (identifier === "MC|BEdit" || identifier === "MC|BSign") {
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("custom_payload", {
channel: identifier,
data: payloadData,
});
} catch (err) {
/* do nothing */
}
}
}
}
placeTradeItems(client, windowId, tradeInfo, offerIndex) {
//very broken, java requires you to send clicks with the current item in the slot
//we have to track inventory and update it after each click, but this is complicated
if (!client.javaClient) return;
if (!client._windowInventories || !client._windowInventories[windowId]) {
return;
}
const inventory = client._windowInventories[windowId];
const findItem = (requiredItem) => {
for (let slot = 3; slot <= 38; slot++) {
const item = inventory[slot];
if (item && item.blockId === requiredItem.blockId) {
if (item.itemCount >= requiredItem.itemCount) {
return slot;
}
}
}
return null;
};
const input1Slot = tradeInfo.input1 ? findItem(tradeInfo.input1) : null;
const input2Slot = tradeInfo.input2 ? findItem(tradeInfo.input2) : null;
if (tradeInfo.input1 && !input1Slot) {
console.error(0);
return;
}
if (tradeInfo.input2 && !input2Slot) {
console.error(1);
return;
}
const clickSequence = [];
if (input1Slot) {
const item1 = inventory[input1Slot];
const hasRemainder = item1.itemCount > tradeInfo.input1.itemCount;
clickSequence.push(
{
slot: input1Slot,
item: item1,
mode: 0,
desc: `pickup ${item1.itemCount} from slot ${input1Slot}`,
},
{
slot: 0,
item: null,
mode: 0,
desc: `place ${tradeInfo.input1.itemCount} in trade slot 0`,
},
);
if (hasRemainder) {
clickSequence.push({
slot: input1Slot,
item: null,
mode: 0,
desc: `return extra to slot ${input1Slot}`,
});
}
clickSequence._updateSlot = input1Slot;
clickSequence._updateItem = item1;
clickSequence._usedCount = tradeInfo.input1.itemCount;
}
let targetSlot = null;
for (let slot = 30; slot <= 38; slot++) {
if (!inventory[slot] || inventory[slot].blockId === -1) {
targetSlot = slot;
break;
}
}
if (!targetSlot) {
for (let slot = 3; slot <= 29; slot++) {
if (!inventory[slot] || inventory[slot].blockId === -1) {
targetSlot = slot;
break;
}
}
}
if (targetSlot) {
clickSequence.push(
{
slot: 2,
item: tradeInfo.output,
mode: 0,
desc: `pick up trade result from slot 2`,
},
{
slot: targetSlot,
item: null,
mode: 0,
desc: `place trade result in slot ${targetSlot}`,
},
);
}
let delay = 0;
clickSequence.forEach((click) => {
setTimeout(() => {
const action = client.windowClickAction++;
try {
let itemParam;
if (click.item) {
itemParam = {
blockId: click.item.blockId,
itemCount: click.item.itemCount,
itemDamage: click.item.itemDamage || 0,
};
} else {
itemParam = { blockId: -1 };
}
const clickMode = click.mode !== undefined ? click.mode : 0;
client.javaClient.write("window_click", {
windowId: windowId,
slot: click.slot,
mouseButton: 0,
action: action,
mode: clickMode,
item: itemParam,
});
} catch (err) {
/* do nothing */
}
}, delay);
delay += 75;
});
if (clickSequence._updateSlot) {
const remainingCount =
clickSequence._updateItem.itemCount - clickSequence._usedCount;
if (remainingCount > 0) {
inventory[clickSequence._updateSlot] = {
blockId: clickSequence._updateItem.blockId,
itemCount: remainingCount,
itemDamage: clickSequence._updateItem.itemDamage,
};
} else {
delete inventory[clickSequence._updateSlot];
}
}
const totalDelay = delay;
setTimeout(() => {
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("window_click", {
windowId: windowId,
slot: -999,
mouseButton: 0,
action: client.windowClickAction++,
mode: 0,
item: { blockId: -1 },
});
} catch (err) {
/* do nothing */
}
}
setTimeout(() => {
client._tradeInProgress = false;
}, 200);
}, totalDelay + 100);
}
handleCraftItem(client, data) {
const debugLog = false;
if (client._craftingInProgress) {
if (debugLog) console.log("craft already in progress ignoring");
return;
}
const reader = new PacketReader(data.slice(1));
const uid = reader.readShort();
const recipeId = reader.readInt();
//console.log(recipeId);
const grid = recipeData[recipeId] || [
"none",
"none",
"none",
"none",
"none",
"none",
"none",
"none",
"none",
];
if (!client.inventory) {
if (debugLog) console.log("no inventory");
client._craftingInProgress = false;
return;
}
if (debugLog) console.log("id", recipeId);
client._craftingInProgress = true;
this.tryPlaceItemsInCraftingGrid(client, grid, recipeId);
}
handleJavaCrafting(client, data) {
/* do nothing */
}
getCraftingResult(grid) {
const craftingResults = {
0: { resultItemId: 5, resultCount: 4 },
1: { resultItemId: 5, resultCount: 4 },
2: { resultItemId: 5, resultCount: 4 },
3: { resultItemId: 5, resultCount: 4 },
4: { resultItemId: 280, resultCount: 4 },
44: { resultItemId: 58, resultCount: 1 },
46: { resultItemId: 54, resultCount: 1 },
};
const recipeStr = grid.join(",");
for (const [recipeId, recipe] of Object.entries(recipeData)) {
if (recipe.join(",") === recipeStr) {
return craftingResults[recipeId];
}
}
return null;
}
craftItemDirectly(client, grid, recipeId) {
if (!client.javaClient || !client.javaClient.write) {
return;
}
const craftingResults = this.getCraftingResult(grid);
if (!craftingResults) {
return;
}
const { resultItemId, resultCount } = craftingResults;
const requiredItems = this.findRequiredItems(client, grid);
if (!requiredItems) {
return;
}
requiredItems.forEach((req) => {
if (!req) return;
const slot = req.sourceSlot;
const item = client.inventory[slot];
if (item && item.itemCount > 1) {
item.itemCount--;
client.inventory[slot] = item;
} else {
client.inventory[slot] = { blockId: -1, itemCount: 0, itemDamage: 0 };
}
});
let addedToExisting = false;
for (let slot = 9; slot < 45; slot++) {
const item = client.inventory[slot];
if (item && item.blockId === resultItemId && item.itemCount < 64) {
item.itemCount = Math.min(64, (item.itemCount || 0) + resultCount);
client.inventory[slot] = item;
addedToExisting = true;
break;
}
}
if (!addedToExisting) {
for (let slot = 9; slot < 45; slot++) {
const item = client.inventory[slot];
if (!item || item.blockId === -1) {
client.inventory[slot] = {
blockId: resultItemId,
itemCount: resultCount,
itemDamage: 0,
};
break;
}
}
}
for (let i = 0; i < 45; i++) {
const item = client.inventory[i] || {
blockId: -1,
itemCount: 0,
itemDamage: 0,
};
try {
client.javaClient.write("set_slot", {
windowId: 0,
slot: i,
item: item,
});
} catch (err) {
/* do nothing */
}
}
}
tryPlaceItemsInCraftingGrid(client, grid, recipeId) {
const debugLog = false;
if (debugLog) console.log("placing items");
const itemNameToDamage = {
wool: 0,
orange_wool: 1,
magenta_wool: 2,
light_blue_wool: 3,
yellow_wool: 4,
lime_wool: 5,
pink_wool: 6,
gray_wool: 7,
light_gray_wool: 8,
cyan_wool: 9,
purple_wool: 10,
blue_wool: 11,
brown_wool: 12,
green_wool: 13,
red_wool: 14,
black_wool: 15,
};
const itemNameToJavaId = {
oak_log: 17,
birch_log: 17,
spruce_log: 17,
jungle_log: 17,
acacia_log: 162,
dark_oak_log: 162,
any_log: [17, 162],
any_plank: 5,
oak_plank: 5,
birch_planks: 5,
spruce_planks: 5,
jungle_planks: 5,
stick: 280,
cobblestone: 4,
iron_ingot: 265,
diamond: 264,
gold_ingot: 266,
any_wool: 35,
wool: 35,
sand: 12,
sandstone: 24,
sandstone_slab: 44,
quartz_slab: 44,
quartz_block: 155,
stone: 1,
nether_brick: 112,
snowball: 332,
clay: 337,
brick: 336,
nether_quartz: 406,
book: 340,
obsidian: 49,
iron_block: 42,
blaze_rod: 369,
chest: 54,
tripwire_hook: 131,
ender_eye: 381,
mossy_cobblestone: 48,
stonebrick: 98,
brick_block: 45,
charcoal: 263,
coal: 263,
glowstone: 348,
gunpowder: 289,
blaze_powder: 377,
pumpkin: 86,
torch: 50,
redstone: 331,
ender_pearl: 368,
slime_ball: 341,
paper: 339,
compass: 345,
apple: 260,
gold_block: 41,
gold_nugget: 371,
melon: 360,
carrot: 391,
brown_mushroom: 39,
red_mushroom: 40,
bowl: 281,
wheat: 296,
cocoa_beans: 351,
flint: 318,
string: 287,
fishing_rod: 346,
feather: 288,
glass: 20,
any_slab: 44,
inc_sac: 351,
red_dye: 351,
green_dye: 351,
lapis: 351,
purple_dye: 351,
cyan_dye: 351,
light_gray_dye: 351,
gray_dye: 351,
pink_dye: 351,
lime_dye: 351,
yellow_dye: 351,
light_blue_dye: 351,
magenta_dye: 351,
orange_dye: 351,
bone_meal: 351,
dandelion: 37,
rose: 38,
orange_wool: 35,
magenta_wool: 35,
light_blue_wool: 35,
yellow_wool: 35,
lime_wool: 35,
pink_wool: 35,
gray_wool: 35,
light_gray_wool: 35,
cyan_wool: 35,
purple_wool: 35,
blue_wool: 35,
brown_wool: 35,
green_wool: 35,
red_wool: 35,
black_wool: 35,
};
const requiredItems = [];
let allItemsFound = true;
const slotUsageCount = new Map();
for (let i = 0; i < 9; i++) {
const requiredItem = grid[i];
if (requiredItem === "none") {
requiredItems[i] = null;
continue;
}
const javaItemId = itemNameToJavaId[requiredItem];
if (!javaItemId) {
if (debugLog) console.log("could not find item for", requiredItem);
allItemsFound = false;
continue;
}
const acceptedIds = Array.isArray(javaItemId) ? javaItemId : [javaItemId];
if (debugLog)
console.log("looking for", requiredItem, "with id(s)", acceptedIds);
let foundSlot = null;
const windowId = client.openWindowId || 0;
const isCraftingTable =
client.openWindowType === "minecraft:crafting_table";
const inventoryToSearch =
isCraftingTable &&
client._windowInventories &&
client._windowInventories[windowId]
? client._windowInventories[windowId]
: client.inventory;
const startSlot = isCraftingTable ? 10 : 9;
const endSlot = isCraftingTable ? 46 : 45;
for (let slot = startSlot; slot < endSlot; slot++) {
const item = inventoryToSearch[slot];
if (item && acceptedIds.includes(item.blockId)) {
const requiredDamage = itemNameToDamage[requiredItem];
if (
requiredDamage !== undefined &&
item.itemDamage !== requiredDamage
) {
if (debugLog)
console.log(
"found item at slot",
slot,
"but damage does not match. need",
requiredDamage,
"got",
item.itemDamage,
);
continue;
}
if (debugLog) console.log("found", requiredItem, "at slot", slot);
const itemCount = item.itemCount || 1;
const usedCount = slotUsageCount.get(slot) || 0;
if (usedCount < itemCount) {
foundSlot = slot;
let gridSlot;
if (isCraftingTable) {
gridSlot = i + 1;
} else {
const slot2x2Map = { 0: 1, 1: 2, 3: 3, 4: 4 };
gridSlot = slot2x2Map[i] || i + 1;
}
requiredItems[i] = {
sourceSlot: slot,
itemId: item.blockId,
gridSlot: gridSlot,
};
slotUsageCount.set(slot, usedCount + 1);
break;
}
}
}
if (!foundSlot) {
if (debugLog)
console.log("could not find", requiredItem, "in inventory");
allItemsFound = false;
}
}
if (!allItemsFound) {
if (debugLog) console.log("not all items found");
client._craftingInProgress = false;
return;
}
if (debugLog) console.log("all items found, placing");
this.placeItemsInGrid(client, requiredItems);
}
placeItemsInGrid(client, requiredItems) {
const debugLog = false;
if (!client.javaClient || !client.javaClient.write) {
client._craftingInProgress = false;
return;
}
if (debugLog) console.log("placing", requiredItems.length, "items");
if (!client.windowClickAction) {
client.windowClickAction = 1;
}
const clicks = [];
const slotGroups = new Map();
for (let i = 0; i < requiredItems.length; i++) {
if (!requiredItems[i]) continue;
const { sourceSlot, gridSlot, itemId } = requiredItems[i];
if (!slotGroups.has(sourceSlot)) {
slotGroups.set(sourceSlot, []);
}
slotGroups.get(sourceSlot).push({ gridSlot, itemId });
}
let lastSourceSlot = null;
let lastSourceItem = null;
for (const [sourceSlot, gridSlots] of slotGroups) {
const windowId = client.openWindowId || 0;
const isCraftingTable =
client.openWindowType === "minecraft:crafting_table";
const inventoryToUse =
isCraftingTable &&
client._windowInventories &&
client._windowInventories[windowId]
? client._windowInventories[windowId]
: client.inventory;
const sourceItem = inventoryToUse[sourceSlot];
if (!sourceItem) continue;
lastSourceSlot = sourceSlot;
lastSourceItem = sourceItem;
clicks.push({
windowId: windowId,
slot: sourceSlot,
mouseButton: 0,
action: client.windowClickAction++,
mode: 0,
item: sourceItem,
});
for (const { gridSlot, itemId } of gridSlots) {
clicks.push({
windowId: windowId,
slot: gridSlot,
mouseButton: 1,
action: client.windowClickAction++,
mode: 0,
item: { blockId: -1, itemCount: 0, itemDamage: 0 },
});
}
if (gridSlots.length < sourceItem.itemCount) {
clicks.push({
windowId: windowId,
slot: sourceSlot,
mouseButton: 0,
action: client.windowClickAction++,
mode: 0,
item: { blockId: -1, itemCount: 0, itemDamage: 0 },
});
}
}
let cursorState = { blockId: -1, itemCount: 0 };
const tempListener = (packet) => {
if (packet.windowId === 0) {
const slotName =
packet.slot === 0
? "RESULT"
: packet.slot >= 1 && packet.slot <= 9
? `GRID ${packet.slot}`
: `INV ${packet.slot}`;
}
if (packet.windowId === -1 && packet.slot === -1) {
const oldCursor = `${cursorState.blockId}:${cursorState.itemCount}`;
cursorState = packet.item || { blockId: -1, itemCount: 0 };
const newCursor = `${cursorState.blockId}:${cursorState.itemCount}`;
}
};
client.javaClient.on("set_slot", tempListener);
setTimeout(() => {
client.javaClient.removeListener("set_slot", tempListener);
}, 100);
let delay = 0;
clicks.forEach((click, index) => {
setTimeout(() => {
try {
client.javaClient.write("window_click", click);
} catch (err) {
/* do nothing */
}
}, delay);
delay += 1;
});
const usedGridSlots = requiredItems.filter((r) => r).map((r) => r.gridSlot);
setTimeout(() => {
this.checkCraftingResult(client, usedGridSlots);
}, delay + 50);
}
checkCraftingResult(client, usedGridSlots) {
const debugLog = false;
if (debugLog) console.log("checking crafting result");
const windowId = client.openWindowId || 0;
const checkForResult = () => {
const inventoryToCheck =
windowId !== 0 &&
client._windowInventories &&
client._windowInventories[windowId]
? client._windowInventories[windowId]
: client.inventory;
if (debugLog) console.log("slot 0 content", inventoryToCheck[0]);
if (!inventoryToCheck || !inventoryToCheck[0]) {
if (debugLog) console.log("no result slot found");
return null;
}
const resultItem = inventoryToCheck[0];
const itemId = resultItem.blockId;
if (itemId === -1 || itemId === null || itemId === undefined) {
if (debugLog) console.log("no valid result item");
return null;
}
if (debugLog) console.log("found result item:", itemId);
return resultItem;
};
const resultItem = checkForResult();
if (resultItem) {
this.takeCraftedItemAndClear(client, usedGridSlots);
return;
}
let handledResult = false;
const resultListener = (packet) => {
if (packet.windowId === windowId && packet.slot === 0) {
if (packet.item && packet.item.blockId !== -1) {
if (client.javaClient) {
client.javaClient.removeListener("set_slot", resultListener);
}
if (!handledResult) {
handledResult = true;
if (debugLog)
console.log("received set_slot for result, taking item");
setTimeout(() => {
this.takeCraftedItemAndClear(client, usedGridSlots);
}, 50);
}
}
}
};
if (client.javaClient) {
client.javaClient.on("set_slot", resultListener);
setTimeout(() => {
if (client.javaClient) {
client.javaClient.removeListener("set_slot", resultListener);
}
if (!handledResult) {
const finalCheck = checkForResult();
if (finalCheck) {
handledResult = true;
this.takeCraftedItemAndClear(client, usedGridSlots);
} else {
client._craftingInProgress = false;
}
}
}, 150);
}
}
takeCraftedItemAndClear(client, usedGridSlots) {
const debugLog = false;
if (debugLog) console.log("taking crafted item and clearing grid");
if (!client.javaClient || !client.javaClient.write) {
if (debugLog) console.log("no java client available");
client._craftingInProgress = false;
return;
}
if (!client.windowClickAction) {
client.windowClickAction = 1;
}
const windowId = client.openWindowId || 0;
const isCraftingTable = windowId !== 0;
const inventoryToCheck =
isCraftingTable &&
client._windowInventories &&
client._windowInventories[windowId]
? client._windowInventories[windowId]
: client.inventory;
const resultItem = inventoryToCheck[0];
if (!resultItem || resultItem.blockId === -1) {
return;
}
const resultItemId = resultItem.blockId;
const resultCount = resultItem.itemCount || 1;
const slotsWithSameItem = [];
const slotRange = isCraftingTable ? [10, 46] : [9, 45];
for (let slot = slotRange[0]; slot < slotRange[1]; slot++) {
const item = inventoryToCheck[slot];
if (item && item.blockId === resultItemId) {
slotsWithSameItem.push({
slot: slot,
count: item.itemCount || 1,
hasSpace: (item.itemCount || 1) < 64,
});
}
}
const targetSlots = [];
let remainingCount = resultCount;
for (const slotInfo of slotsWithSameItem) {
if (slotInfo.hasSpace && remainingCount > 0) {
const spaceAvailable = 64 - slotInfo.count;
const amountToPlace = Math.min(spaceAvailable, remainingCount);
targetSlots.push({ slot: slotInfo.slot, amount: amountToPlace });
remainingCount -= amountToPlace;
}
}
if (remainingCount > 0) {
const hotbarSlots = isCraftingTable
? [37, 38, 39, 40, 41, 42, 43, 44, 45]
: [36, 37, 38, 39, 40, 41, 42, 43, 44];
for (const slot of hotbarSlots) {
if (remainingCount <= 0) break;
const item = inventoryToCheck[slot];
if (!item || item.blockId === -1) {
const amountToPlace = Math.min(64, remainingCount);
targetSlots.push({ slot: slot, amount: amountToPlace });
remainingCount -= amountToPlace;
}
}
}
if (remainingCount > 0) {
const mainInvSlots = isCraftingTable
? [
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
]
: [
9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
];
for (const slot of mainInvSlots) {
if (remainingCount <= 0) break;
const item = inventoryToCheck[slot];
if (!item || item.blockId === -1) {
const amountToPlace = Math.min(64, remainingCount);
targetSlots.push({ slot: slot, amount: amountToPlace });
remainingCount -= amountToPlace;
}
}
}
setTimeout(() => {
try {
client.javaClient.write("window_click", {
windowId: windowId,
slot: -999,
mouseButton: 0,
action: client.windowClickAction++,
mode: 0,
item: { blockId: -1, itemCount: 0, itemDamage: 0 },
});
if (debugLog) console.log("clicked slot -999");
} catch (err) {}
setTimeout(() => {
try {
client.javaClient.write("window_click", {
windowId: windowId,
slot: 0,
mouseButton: 0,
action: client.windowClickAction++,
mode: 1,
item: resultItem,
});
if (debugLog) console.log("shift clicked result");
} catch (err) {
/* do nothing */
}
}, 1);
}, 1);
setTimeout(() => {
setTimeout(() => {
let foundPlanks = false;
for (let i = 9; i < 45; i++) {
const item = client.inventory[i];
if (item && item.blockId === 5) {
foundPlanks = true;
}
}
const inventoryToCheck =
windowId !== 0 &&
client._windowInventories &&
client._windowInventories[windowId]
? client._windowInventories[windowId]
: client.inventory;
if (debugLog) console.log("clearing grid slots", usedGridSlots);
usedGridSlots.forEach((gridSlot, index) => {
setTimeout(() => {
const gridItem = inventoryToCheck[gridSlot];
if (gridItem && gridItem.blockId !== -1) {
try {
client.javaClient.write("window_click", {
windowId: windowId,
slot: gridSlot,
mouseButton: 0,
action: client.windowClickAction++,
mode: 1,
item: gridItem,
});
if (debugLog) console.log("shift clicked grid", gridSlot);
} catch (err) {
/* do nothing */
}
} else {
if (debugLog) console.log("grid slot", gridSlot, "already empty");
}
}, index * 1);
});
}, 1);
}, 1);
setTimeout(() => {
if (debugLog) console.log("craft complete clearing flag");
client._craftingInProgress = false;
}, 200);
}
placeCraftingItems(
client,
windowId,
itemsToPlace,
inventoryWindow,
startAction,
) {
/* do nothing - but keep this here so nothing breaks */
}
sendInventoryUpdate(client) {
if (!client.inventory || !client.javaClient) {
return;
}
for (let i = 0; i < 45; i++) {
const item = client.inventory[i];
const PacketWriter = require("./packetwriter");
const slotWriter = new PacketWriter();
slotWriter.writeByte(0);
slotWriter.writeShort(i);
this.writeItem(slotWriter, item);
this.sendPacket(client, 0x67, slotWriter.toBuffer());
}
}
//LCE automatically accepts a trade, whilst java requires items in specific slots
//we should write a server plugin that handles this because lazy
handleTradeItem(client, data) {
/* do nothing */
}
handleContainerAck(client, data) {
const reader = new PacketReader(data.slice(1));
const windowId = reader.readByte();
const action = reader.readShort();
const accepted = reader.readBoolean();
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("transaction", {
windowId: windowId,
action: action,
accepted: accepted,
});
} catch (err) {
/* do nothing */
}
}
}
handleContainerClick(client, data) {
const reader = new PacketReader(data.slice(1));
const containerId = reader.readByte();
const slotNum = reader.readShort();
const buttonNum = reader.readByte();
const uid = reader.readShort();
const clickType = reader.readByte();
const item = this.readItemWithNBT(reader);
if (client._bookGUI && containerId === client.openWindowId) {
if (slotNum === 48 && client._bookGUI.currentPage > 0) {
client._bookGUI.currentPage--;
this.updateBookGUIContents(client, containerId);
return;
}
if (
slotNum === 50 &&
client._bookGUI.currentPage < client._bookGUI.pages.length - 1
) {
client._bookGUI.currentPage++;
this.updateBookGUIContents(client, containerId);
return;
}
if (
client._bookGUI.clickActions &&
client._bookGUI.clickActions[slotNum]
) {
const action = client._bookGUI.clickActions[slotNum];
if (action.type === "run_command" && action.value) {
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("chat", { message: action.value });
} catch (err) {
/* do nothing */
}
}
const PacketWriter = require("./packetwriter");
const writer = new PacketWriter();
writer.writeByte(containerId & 0xff);
this.sendPacket(client, 0x65, writer.toBuffer());
delete client._bookGUI;
}
}
const PacketWriter = require("./packetwriter");
const rejectWriter = new PacketWriter();
rejectWriter.writeByte(containerId & 0xff);
rejectWriter.writeShort(uid);
rejectWriter.writeBoolean(false);
this.sendPacket(client, 0x6a, rejectWriter.toBuffer());
return;
}
//0=Normal click
//1=Shift click
//2=Number key (hotbar swap)
//3=Middle click (creative)
//4=Drop
//5=Drag
//6=Double click
let javaItem = null;
if (item) {
const javaItemId = this.mapLCEItemToJava(item.blockId);
if (javaItemId !== null) {
javaItem = {
blockId: javaItemId,
itemCount: item.itemCount,
itemDamage: item.itemDamage,
};
if (item.nbt) {
javaItem.nbtData = this.convertLCENBTToJava(item.nbt);
}
}
}
if (client.javaClient && client.javaClient.write) {
try {
//luckily both LCE and java use slot -999 for dropping items outside inventory window
let finalMode = clickType;
let finalButton = buttonNum;
const clickPacket = {
windowId: containerId,
slot: slotNum,
mouseButton: finalButton,
action: uid,
mode: finalMode,
item: javaItem || { blockId: -1 },
};
client.javaClient.write("window_click", clickPacket);
} catch (err) {
/* do nothing */
}
}
}
handleSignUpdate(client, data) {
const reader = new PacketReader(data.slice(1));
const x = reader.readInt();
const y = reader.readShort();
const z = reader.readInt();
const verified = reader.readBoolean();
const censored = reader.readBoolean();
const text1 = reader.readUtf();
const text2 = reader.readUtf();
const text3 = reader.readUtf();
const text4 = reader.readUtf();
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("update_sign", {
location: { x: x, y: y, z: z },
text1: JSON.stringify({ text: text1 }),
text2: JSON.stringify({ text: text2 }),
text3: JSON.stringify({ text: text3 }),
text4: JSON.stringify({ text: text4 }),
});
} catch (err) {
/* do nothing */
}
}
}
handleSetCreativeModeSlot(client, data) {
const reader = new PacketReader(data.slice(1));
const slotNum = reader.readShort();
const item = this.readItemWithNBT(reader);
let javaItem = null;
if (item) {
const javaItemId = this.mapLCEItemToJava(item.blockId);
if (javaItemId !== null) {
javaItem = {
blockId: javaItemId,
itemCount: item.itemCount,
itemDamage: item.itemDamage || 0,
};
if (item.nbt) {
javaItem.nbtData = this.convertLCENBTToJava(item.nbt);
}
}
}
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("set_creative_slot", {
slot: slotNum,
item: javaItem || { blockId: -1 },
});
} catch (err) {
/* do nothing */
}
}
}
handleClientCommand(client, data) {
const reader = new PacketReader(data.slice(1));
const action = reader.readByte();
if (action === 1) {
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("client_command", {
payload: 0,
});
} catch (err) {
/* do nothing */
}
}
}
}
handleDisconnect(client, data) {
const reader = new PacketReader(data.slice(1));
const reason = reader.readInt();
if (this.clientChests.has(client)) {
const openChestKeys = this.clientChests.get(client);
for (const posKey of openChestKeys) {
const chestData = this.openChests.get(posKey);
if (chestData) {
chestData.clients.delete(client);
if (chestData.clients.size === 0) {
this.openChests.delete(posKey);
}
}
}
this.clientChests.delete(client);
}
this.removeClient(client);
if (client.socket && !client.socket.destroyed) {
client.socket.destroy();
}
}
handlePlayerMovement(client, packetId, data) {
const reader = new PacketReader(data.slice(1));
const dataSize = data.length - 1;
let x, y, yView, z, yRot, xRot, onGround;
switch (packetId) {
case 0x0b: //MovePlayerPacket (pos)
if (dataSize < 33) {
return;
}
x = reader.readDouble();
y = reader.readDouble();
yView = reader.readDouble();
z = reader.readDouble();
onGround = reader.readByte();
client.playerX = x;
client.playerY = y;
client.playerZ = z;
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("position_look", {
x: x,
y: y,
z: z,
yaw: client.lastYaw || 0,
pitch: client.lastPitch || 0,
onGround: onGround !== 0,
});
} catch (err) {
/* do nothing */
}
}
break;
case 0x0c: //MovePlayerPacket (rot)
if (dataSize < 9) {
return;
}
yRot = reader.readFloat();
xRot = reader.readFloat();
onGround = reader.readByte();
const normalizedYaw = normalizeYaw(yRot);
client.playerYaw = normalizedYaw;
client.playerPitch = xRot;
client.lastYaw = normalizedYaw;
client.lastPitch = xRot;
if (client.javaClient && client.javaClient.write) {
try {
client.javaClient.write("look", {
yaw: normalizedYaw,
pitch: xRot,
onGround: onGround !== 0,
});
} catch (err) {
/* do nothing */
}
}
break;
}
}
readItem(reader) {
return readItem(reader);
}
readItemWithNBT(reader) {
const itemId = reader.readShort();
if (itemId === -1) {
return null;
}
const count = reader.readByte();
const damage = reader.readShort();
if (reader.offset + 2 > reader.buffer.length) {
return {
blockId: itemId,
itemCount: count,
itemDamage: damage,
nbt: null,
};
}
const nbtLength = reader.readShort();
let nbt = null;
if (nbtLength > 0 && reader.offset + nbtLength <= reader.buffer.length) {
const nbtBytes = reader.readBytes(nbtLength);
nbt = nbtBytes;
}
return {
blockId: itemId,
itemCount: count,
itemDamage: damage,
nbt: nbt,
};
}
convertLCENBTToJava(nbtBytes) {
try {
const zlib = require("zlib");
const decompressed = zlib.gunzipSync(nbtBytes);
const nbtReader = new PacketReader(decompressed);
const rootTag = this.readNBTTag(nbtReader);
return rootTag;
} catch (err) {
return null;
}
}
readNBTTag(reader) {
const type = reader.readByte();
if (type === 0) return null;
const name = reader.readUTF();
return this.readNBTPayload(reader, type, name);
}
readNBTPayload(reader, type, name) {
switch (type) {
case 1:
return { type: "byte", name, value: reader.readByte() };
case 2:
return { type: "short", name, value: reader.readShort() };
case 3:
return { type: "int", name, value: reader.readInt() };
case 8:
return { type: "string", name, value: reader.readUTF() };
case 9:
const listType = reader.readByte();
const listSize = reader.readInt();
const listItems = [];
for (let i = 0; i < listSize; i++) {
listItems.push(this.readNBTPayload(reader, listType, ""));
}
return {
type: "list",
name,
value: { type: listType, value: listItems },
};
case 10:
const compound = {};
while (true) {
const tagType = reader.readByte();
if (tagType === 0) break;
const tagName = reader.readUTF();
compound[tagName] = this.readNBTPayload(reader, tagType, tagName);
}
return { type: "compound", name, value: compound };
default:
return null;
}
}
writeItemNBT(writer, item) {
return writeItemNBT(writer, item);
}
writeItem(writer, item) {
return writeItem(writer, item, this.mapJavaItemToLCE.bind(this));
}
sendPacket(client, packetId, payload) {
if (!this.validLCEPackets.has(packetId)) {
return;
}
if (!payload || !Buffer.isBuffer(payload)) {
return;
}
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]);
if (fullPacket.length > 1000000) {
return;
}
if (!client._packetQueue) {
client._packetQueue = [];
client._sending = false;
}
client._packetQueue.push(fullPacket);
if (!client._sending) {
client._sending = true;
setImmediate(() => {
while (client._packetQueue.length > 0 && !client.socket.destroyed) {
const pkt = client._packetQueue.shift();
client.socket.write(pkt);
}
client._sending = false;
});
}
} catch (err) {
/* do nothing */
}
}
disconnectClient(client, reason = 0) {
const reasonMap = {
0: "Generic disconnect",
2: "Connection lost",
10: "Kicked/Banned",
20: "Timeout",
23: "Server full",
};
const reasonText = reasonMap[reason] || `Unknown (${reason})`;
if (client.socket && !client.socket.destroyed) {
try {
const writer = new PacketWriter();
writer.writeInt(reason);
this.sendPacket(client, 0xff, writer.toBuffer());
setTimeout(() => {
this.removeClient(client);
}, 100);
return;
} catch (err) {
/* do nothing */
}
}
this.removeClient(client);
}
removeClient(client) {
if (client._removing) {
return;
}
client._removing = true;
if (this.clientChests.has(client)) {
const openChestKeys = this.clientChests.get(client);
for (const posKey of openChestKeys) {
const chestData = this.openChests.get(posKey);
if (chestData) {
chestData.clients.delete(client);
if (chestData.clients.size === 0) {
this.openChests.delete(posKey);
}
}
}
this.clientChests.delete(client);
}
const index = this.clients.indexOf(client);
if (index > -1) {
this.clients.splice(index, 1);
}
if (client.smallId !== undefined) {
this.usedSmallIds.delete(client.smallId);
}
if (client.javaClient) {
const oldJavaClient = client.javaClient;
client.javaClient = null;
oldJavaClient.removeAllListeners();
oldJavaClient.on("error", () => {
/* do nothing */
});
try {
oldJavaClient.end();
} catch (err) {
/* do nothing */
}
}
if (client.socket && !client.socket.destroyed) {
try {
client.socket.destroy();
} catch (err) {
/* do nothing */
}
}
}
async connectToJavaServer(client) {
try {
return await connectToJavaServerFunc(this, client);
} catch (err) {
/* do nothing */
}
}
stop() {
if (this.broadcastInterval) {
clearInterval(this.broadcastInterval);
}
if (this.udpSocket) {
this.udpSocket.close();
}
if (this.tcpServers && this.tcpServers.length > 0) {
this.tcpServers.forEach((server) => server.close());
}
this.clients.forEach((client) => {
this.disconnectClient(client, 2);
});
}
}
module.exports = LCEProxy;
/* --------------------------------------------------------------- */