4202 lines
112 KiB
JavaScript
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;
|
|
/* --------------------------------------------------------------- */
|