In game connection manager

This commit is contained in:
unknown
2026-04-01 20:17:24 -04:00
parent 3cdd1a50e8
commit a94f5f8c58
6 changed files with 467 additions and 224 deletions

View File

@@ -10,19 +10,38 @@
**Client modifications, or server plugins ARE NOT required!**
- Install [NodeJS](https://nodejs.org/en/download), you may need to restart your computer after
- Start a localhost Java Edition server running on port 25564, and in offline mode (you can edit these in `server.properties`)
- Run the file `install_dependencies.bat`, - you only need to run this file one time.
- Run `start.bat`, the Java Edition server will appear in the join tab on Legacy Console Edition! You can configure the server IP / PORT in `constants.js`.
- Run `start.bat`, the proxy will appear in the join tab on Legacy Console Edition!
# Known issues, will be fixed
---
- Crafting is buggy
- You're unable to accept villager trades
# How to Use
Use the `/lce` commands to manage your connection between Legacy Edition and Java Edition:
- `/lce connect <ip>`
Connect to a Java Edition server.
- `/lce disconnect`
Disconnect from the Java Edition server.
- `/lce link`
Link your Java Edition account.
- `/lce unlink`
Unlink your Java Edition account.
- `/lce uselink <true/false>`
Enable or disable the use of your linked Java Edition account when connecting.
---
# TODO
- Crafting is buggy / not fully implemented
- You're unable to accept villager trades
- Lighting is a little bugged in other dimensions (afaik)
## Chunks
- [x] Java → LCE chunks

View File

@@ -1,14 +1,10 @@
/* --------------------------------------------------------------- */
/* constants.js */
/* --------------------------------------------------------------- */
const USE_LEGACY_USERNAME = false;
const CUSTOM_USERNAME = "LCE_Player";
const LOBBY_NAME = "Legacy Cross Play";
/* EVERYTHING IS NOW HANDLED IN GAME, JOIN THE PROXY DIRECTLY INSTEAD OF CONFIGURING THIS FILE! */
const SERVERS = [
{ server: "us-1.creepernation.net:25568", cracked: true },
{ server: "hypixel.net", cracked: false },
{ server: "localhost:25564", cracked: true },
];
/* DONT TOUCH THESE BELOW THINGS UNLESS YOU KNOW WHAT YOURE DOING! */
@@ -20,8 +16,6 @@ module.exports = {
GAME_PORT,
WIN64_LAN_DISCOVERY_PORT,
MINECRAFT_NET_VERSION,
USE_LEGACY_USERNAME,
CUSTOM_USERNAME,
SERVERS,
LOBBY_NAME,
};
/* --------------------------------------------------------------- */

View File

@@ -6,6 +6,7 @@ const fs = require("fs");
const path = require("path");
const AUTH_CACHE_DIR = path.join(__dirname, "..", ".auth_cache");
const USER_IDENTIFIER = "lce-proxy-user";
class MicrosoftAuth {
constructor() {
@@ -22,72 +23,65 @@ class MicrosoftAuth {
}
}
async authenticate() {
async authenticate(onDeviceCode) {
this.ensureCacheDir();
if (!this.authflow) {
const userIdentifier = "lce-proxy-user";
this.authflow = new Authflow(userIdentifier, AUTH_CACHE_DIR);
this.authflow = new Authflow(USER_IDENTIFIER, AUTH_CACHE_DIR);
if (onDeviceCode) {
this.authflow.codeCallback = onDeviceCode;
}
if (!this.authflow.onMsaCode) {
this.authflow.onMsaCode = (data) => {
console.log("\nYou have not yet logged into your Java account!");
const url = `https://microsoft.com/link?otc=${data.userCode}`;
console.log(`enter the code ${data.userCode} at ${url}`);
console.log(
"If you enabled this by accident and would like to only join offline servers, set USE_JAVA_ACCOUNT to false in constants.js\n",
);
const result = await this.authflow.getMinecraftJavaToken({
fetchProfile: true,
});
try {
const { exec } = require("child_process");
const platform = process.platform;
const command =
platform === "win32"
? `start ${url}`
: platform === "darwin"
? `open ${url}`
: `xdg-open ${url}`;
exec(command);
} catch (err) {
/* do nothing */
}
};
if (!result.profile) {
throw new Error(
"Couldn't log into Java Edition account! Do you own Minecraft Java Edition?",
);
}
try {
const result = await this.authflow.getMinecraftJavaToken({
fetchProfile: true,
});
this.username = result.profile.name;
this.uuid = result.profile.id;
this.accessToken = result.token;
this.authenticated = true;
if (!result.profile) {
throw new Error(
"Error logging into Java Edition account! Do you own Minecraft Java Edition?\n",
);
}
this.username = result.profile.name;
this.uuid = result.profile.id;
this.accessToken = result.token;
this.authenticated = true;
return {
username: this.username,
uuid: this.uuid,
accessToken: this.accessToken,
};
} catch (err) {
throw err;
}
return {
username: this.username,
uuid: this.uuid,
accessToken: this.accessToken,
};
}
async getAuthData() {
return await this.authenticate();
unlink() {
try {
const files = fs.readdirSync(AUTH_CACHE_DIR);
for (const file of files) {
fs.unlinkSync(path.join(AUTH_CACHE_DIR, file));
}
} catch (e) {
/* do nothing */
}
this.authflow = null;
this.authenticated = false;
this.username = null;
this.uuid = null;
this.accessToken = null;
}
isAuthenticated() {
return this.authenticated;
}
hasCachedAccount() {
try {
const files = fs.readdirSync(AUTH_CACHE_DIR);
return files.length > 0;
} catch (e) {
return false;
}
}
}
const authInstance = new MicrosoftAuth();

View File

@@ -5,7 +5,7 @@
//beware that there are a lot of magic numbers, need to do a refractor soon.
const mc = require("minecraft-protocol");
const { USE_LEGACY_USERNAME, CUSTOM_USERNAME } = require("../constants");
const msAuth = require("./auth");
const PacketWriter = require("./packetwriter");
const { mapJavaItemToLCE, mapJavaBlockToLCE } = require("./mappings");
@@ -26,9 +26,7 @@ async function connectToJavaServer(proxy, client) {
let authConfig = {
host: serverConfig.host,
port: serverConfig.port,
username: !USE_LEGACY_USERNAME
? CUSTOM_USERNAME
: client.username || "undefined",
username: client.username || "Player",
auth: "offline",
version: "1.8.9",
};
@@ -50,9 +48,6 @@ async function connectToJavaServer(proxy, client) {
version: "1.8.9",
};
} else {
console.log(
`Server ${serverConfig.name} requires authentication. Authenticating...`,
);
await msAuth.authenticate();
authConfig = {
host: serverConfig.host,
@@ -82,72 +77,61 @@ async function connectToJavaServer(proxy, client) {
client.javaClient = javaClient;
client._lastJavaPackets = [];
const returnToLobby = (message) => {
if (client.socket && client.socket.destroyed) return;
const old = client.javaClient;
client.javaClient = null;
client.inLobby = true;
if (old) {
old.removeAllListeners();
old.on("error", () => {
/* do nothing */
});
try {
old.end();
} catch (e) {
/* do nothing */
}
}
proxy.enterLobby(client, message);
};
javaClient.on("error", (err) => {
console.log(`DISCONNECTED: ${err}`);
if (!client._removing) {
client._removing = true;
const index = proxy.clients.indexOf(client);
if (index > -1) {
proxy.clients.splice(index, 1);
}
if (client.smallId) {
proxy.usedSmallIds.delete(client.smallId);
}
client.javaClient = null;
if (client.socket && !client.socket.destroyed) {
try {
const writer = new PacketWriter();
writer.writeInt(20);
proxy.sendPacket(client, 0xff, writer.toBuffer());
client.socket.destroy();
} catch (err) {
/* do nothing */
}
}
}
returnToLobby(`Connection error: ${err.message || err}`);
});
javaClient.on("disconnect", (packet) => {
console.log(`DISCONNECTED: ${packet.reason}`);
if (!client._removing) {
proxy.disconnectClient(client, 2);
const handleDisconnectReason = (raw, prefix) => {
let text = "";
try {
text = typeof raw === "string" ? raw : JSON.parse(raw)?.text || raw;
} catch (e) {
text = String(raw);
}
});
javaClient.on("kick_disconnect", (packet) => {
console.log(`DISCONNECTED: ${packet.reason}`);
if (client.state === "play") {
let reason = 10;
if (packet.reason) {
const reasonText =
typeof packet.reason === "string"
? packet.reason
: JSON.stringify(packet.reason);
const lowerReason = reasonText.toLowerCase();
if (lowerReason.includes("ban")) {
reason = 10;
} else if (lowerReason.includes("kick")) {
reason = 10;
} else if (
lowerReason.includes("timeout") ||
lowerReason.includes("timed out")
) {
reason = 20;
} else if (lowerReason.includes("full")) {
reason = 23;
} else {
reason = 10;
}
if (text.toLowerCase().includes("failed to verify username")) {
if (msAuth.hasCachedAccount() && client.useLinkedAccount === false) {
returnToLobby(
'This server requires a Java Edition account, execute "/lce uselink true"',
);
} else {
returnToLobby(
"This server requires a Java Edition account, use /lce link",
);
}
proxy.disconnectClient(client, reason);
} else {
proxy.disconnectClient(client, 2);
returnToLobby(`${prefix}: ${text}`);
}
});
};
javaClient.on("disconnect", (packet) =>
handleDisconnectReason(packet.reason, "Disconnected"),
);
javaClient.on("kick_disconnect", (packet) =>
handleDisconnectReason(packet.reason, "Kicked"),
);
javaClient.on("end", () => {
if (proxy.clients.indexOf(client) > -1 && !client._removing) {
proxy.disconnectClient(client, 2);
if (client.javaClient) {
returnToLobby("Connection to server closed");
}
});

View File

@@ -16,13 +16,17 @@ class LANBroadcast {
this.buffer.writeUInt16LE(char, 8 + i * 2);
}
this.buffer.writeUInt8(1, 72); //player count including host
this.buffer.writeUInt8(1, 72); //player count (updated dynamically)
this.buffer.writeUInt8(8, 73); //max players
this.buffer.writeUInt32LE(0, 74); //host settings
this.buffer.writeUInt32LE(0, 78); //texture pack ID
this.buffer.writeUInt8(0, 82); //sub texture pack ID
this.buffer.writeUInt8(1, 83); //is joinable
}
setPlayerCount(count) {
this.buffer.writeUInt8(Math.max(1, count), 72);
}
}
module.exports = LANBroadcast;

View File

@@ -11,9 +11,7 @@ const {
GAME_PORT,
WIN64_LAN_DISCOVERY_PORT,
MINECRAFT_NET_VERSION,
USE_LEGACY_USERNAME,
CUSTOM_USERNAME,
SERVERS,
LOBBY_NAME,
} = require("../constants");
const PacketWriter = require("./packetwriter");
@@ -185,13 +183,6 @@ class LCEProxy {
this.startTCPServer();
console.log("\nProxy is now running!");
console.log(
`Broadcasting ${SERVERS.length} server(s) as LAN games on Minecraft Legacy Edition.`,
);
SERVERS.forEach((server, index) => {
const serverStr = typeof server === "string" ? server : server.server;
console.log(` - ${serverStr} on port ${GAME_PORT + index}`);
});
console.log("Press Ctrl+C to stop.\n");
}
@@ -201,96 +192,80 @@ class LCEProxy {
this.udpSocket.setBroadcast(true);
});
this.broadcasts = SERVERS.map((server, index) => {
const gamePort = GAME_PORT + index;
const serverStr = typeof server === "string" ? server : server.server;
const parts = serverStr.split(":");
this.serverConfigs[index] = {
host: parts[0],
port: parts[1] ? parseInt(parts[1]) : 25565,
name: serverStr,
cracked: typeof server === "object" ? server.cracked : false,
gamePort: gamePort,
};
return new LANBroadcast(serverStr, gamePort);
});
this.lobbyBroadcast = new LANBroadcast(LOBBY_NAME, GAME_PORT);
this.broadcastInterval = setInterval(() => {
this.broadcasts.forEach((broadcast) => {
this.udpSocket.send(
broadcast.buffer,
0,
broadcast.buffer.length,
WIN64_LAN_DISCOVERY_PORT,
"255.255.255.255",
);
});
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() {
this.serverConfigs.forEach((serverConfig, index) => {
const tcpServer = net.createServer((socket) => {
socket.setNoDelay(true);
const tcpServer = net.createServer((socket) => {
socket.setNoDelay(true);
let smallId = 0;
while (this.usedSmallIds.has(smallId) && smallId <= 8) {
smallId++;
}
let smallId = 0;
while (this.usedSmallIds.has(smallId) && smallId <= 8) {
smallId++;
}
if (smallId > 8) {
socket.destroy();
return;
}
if (smallId > 8) {
socket.destroy();
return;
}
this.usedSmallIds.add(smallId);
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: serverConfig,
};
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);
this.clients.push(client);
const smallIdBuffer = Buffer.from([smallId]);
socket.write(smallIdBuffer);
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);
}
});
socket.on("data", (data) => {
this.handleClientData(client, data);
});
tcpServer.listen(serverConfig.gamePort, () => {
console.log(
`Server ${serverConfig.name} should now be visible on LCE.`,
);
socket.on("end", () => {
this.removeClient(client);
});
this.tcpServers.push(tcpServer);
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) {
@@ -500,9 +475,6 @@ class LCEProxy {
this.handlePreLogin(client, packetData);
break;
case 0xab: //AuthResponsePacket (171)
console.log(
`[AUTH] Received AuthResponsePacket, state=${client.state}, authState=${client.authState}`,
);
if (client.state === "login") {
this.handleAuthResponse(client, packetData);
}
@@ -810,12 +782,11 @@ class LCEProxy {
const username = reader.readString();
client.state = "login";
client.username = !USE_LEGACY_USERNAME ? CUSTOM_USERNAME : username;
client.username = username;
if (client.javaClient) {
const oldJavaClient = client.javaClient;
client.javaClient = null;
oldJavaClient.removeAllListeners();
oldJavaClient.on("error", () => {
/* do nothing */
@@ -827,7 +798,109 @@ class LCEProxy {
}
}
this.connectToJavaServer(client);
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) {
@@ -2132,6 +2205,22 @@ class LCEProxy {
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 &&
@@ -2139,9 +2228,7 @@ class LCEProxy {
message.length > 0
) {
try {
client.javaClient.write("chat", {
message: message,
});
client.javaClient.write("chat", { message });
} catch (err) {
/* do nothing */
}
@@ -2149,6 +2236,168 @@ class LCEProxy {
}
}
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();
@@ -3850,7 +4099,6 @@ class LCEProxy {
23: "Server full",
};
const reasonText = reasonMap[reason] || `Unknown (${reason})`;
console.log(`DISCONNECTED: ${reasonText}`);
if (client.socket && !client.socket.destroyed) {
try {
@@ -3926,7 +4174,7 @@ class LCEProxy {
try {
return await connectToJavaServerFunc(this, client);
} catch (err) {
console.log(`DISCONNECTED: ${err}`);
/* do nothing */
}
}