4 Commits

Author SHA1 Message Date
unknown 109157b19f Update README.md 2026-04-01 20:22:54 -04:00
unknown a94f5f8c58 In game connection manager 2026-04-01 20:17:24 -04:00
unknown 3cdd1a50e8 Fixed authentication + Java --> LCE lighting 2026-04-01 18:23:30 -04:00
unknown 60737f5669 Lazily fix authentication - no more 3rd party client required! 2026-04-01 17:38:20 -04:00
6 changed files with 817 additions and 363 deletions
+185 -127
View File
@@ -1,188 +1,246 @@
# Legacy Cross Play
## Any questions, need help? [Join the server!](https://discord.gg/cb9rs9yfDm)
## CURRENTLY, THE ORIGINAL LEGACY CLIENT IS BROKEN, USE [THIS ONE](https://github.com/DeveloperExotic/LegacyCrossPlay/releases)
**This project allows Legacy Console Edition clients to join a vanilla 1.8 Java Edition server!**
![](https://raw.githubusercontent.com/DeveloperExotic/LegacyCrossPlay/refs/heads/main/.assets/lce1.png)
![](https://raw.githubusercontent.com/DeveloperExotic/LegacyCrossPlay/refs/heads/main/.assets/lce2.png)
# How to Install
**Code 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`.
# Known issues, will be fixed
- Chunk lighting is always based on sky light (no lighting data)
- You're unable to accept villager trades
## Any questions or need help?
Join the server: https://discord.gg/cb9rs9yfDm
---
# TODO
**This project allows Legacy Console Edition clients to join a vanilla 1.8 Java Edition server.**
![](https://raw.githubusercontent.com/DeveloperExotic/LegacyCrossPlay/refs/heads/main/.assets/lce1.png)
![](https://raw.githubusercontent.com/DeveloperExotic/LegacyCrossPlay/refs/heads/main/.assets/lce2.png)
---
## How to Install
**Client modifications or server plugins are not required.**
- Install Node.js
https://nodejs.org/en/download
(you may need to restart your computer)
- Run `install_dependencies.bat`
(only required once)
- Run `start.bat`
The proxy will appear in the join tab on Legacy Console Edition.
---
## How to Use
Use `/lce` commands to manage your connection.
- `/lce connect <ip>`
Connect to a Java Edition server.
- `/lce disconnect`
Disconnect from the server.
- `/lce link`
Link your Java Edition account.
- `/lce unlink`
Unlink your account.
- `/lce uselink <true/false>`
Enable or disable the linked account when connecting.
---
## TODO
### Known Issues
- Crafting is buggy / not fully implemented
- Villager trades cannot be accepted
- Lighting has minor dimension related issues
---
## Chunks
- [x] Java LCE chunks
- [x] Blocks
- [x] Metadata
- [x] Lighting
- [x] Biomes
- [x] Java -> LCE chunks
- [x] Blocks
- [x] Metadata
- [x] Lighting
- [x] Biomes
## Inventory Syncing
---
- [x] Held item
- [x] Item dropping
- [x] Creative items
- [x] Creative clear
- [x] Moved items
## Inventory Sync
## Sync Gamemode / XP / Health / Hunger
- [x] Held item
- [x] Item dropping
- [x] Creative items
- [x] Inventory movement
- [x] Damage knockback / sound
---
## Game State
- [x] Gamemode / XP / Health / Hunger
- [x] Damage knockback and sounds
---
## Respawn
- [x] Death animation
- [x] Respawn handling
- [x] Death animation
- [x] Respawn handling
---
## Environment
### Time
- [x] Sync Time
- [x] DoDaylightCycle fix
- [x] Time synchronization
- [x] Daylight cycle fixes
### Weather
- [x] Sync Weather
- [x] Weather synchronization
---
## Sound
- [x] Sound effects
- [x] Sound effects
---
# Entities
## Entities
## Base Entities
### Base Entities
- [x] Java edition players
- [x] Items
- [x] Mobs
- [x] Java edition players
- [x] Items
- [x] Mobs
---
### Mob Features
- [x] Ageable entities
- [x] Entity metadata (e.g. size, type)
- [x] Nametags
- [x] Mob inventory
- [x] Animations (e.g. bats)
- [x] Ageable entities
- [x] Metadata (size, type)
- [x] Nametags
- [x] Mob inventory
- [x] Animations
## Entity Attachments
---
- [x] Attach to entities (e.g. riding pig, minecart, boat)
### Entity Attachments
- [x] Riding (boats, minecarts, etc.)
---
### Damage Translation
#### LCE Damage → Java Entity
- [x] LCE -> Java
- [x] Entities
- [x] Players
- [x] Entity
- [x] Player
#### Java Damage → LCE Entity
- [x] Mobs
- [x] Players
- [x] Hurt sound
- [x] Entity velocity knockback
### Critical Hits
- [x] Normal critical
- [x] Magical critical
### Other Entity Systems
- [x] Fix minecart metadata (e.g. tnt minecart)
- [x] Arrows
- [x] Fix arrow bug when colliding with player / low bow pull
- [x] Falling block entities (e.g. sand, gravel, anvil)
- [x] Fireball
- [x] Custom Java Edition skins
- [x] Java -> LCE
- [x] Mobs
- [x] Players
- [x] Knockback
- [x] Hurt sounds
---
# Server
### Combat
## Chat
- [x] Receive server chat
- [x] Finter server colors
- [x] Send server chat
- [x] Fix chat commands (e.g. /say, /tellraw)
- [x] Java edition english chat translations
## Disconnections
- [x] Our LCE client disconnects
- [x] Our java player disconnects
- [x] Critical hits (normal and magical)
---
# Player Animations
### Misc Entities
## LCE → Java
- [x] Crouch
- [x] Sprint
- [x] Attack (swing)
- [x] Riding
- [x] Sword blocking
## Java → LCE
- [x] Crouch
- [x] Sprint
- [x] Attack
- [x] Entity
- [x] Player
- [x] Eating
- [x] Sleeping
- [x] Minecart fixes
- [x] Arrows
- [x] Collision and behavior fixes
- [x] Falling blocks (sand, gravel, anvils)
- [x] Fireballs
- [x] Custom Java skins
---
# Block Updates
## Server
## Java → LCE
### Chat
- [x] Creative
- [x] Survival
- [x] Explosions
## LCE → Java
- [x] Creative
- [x] Survival
- [x] Receive and send chat
- [x] Color handling
- [x] Command support (/say, /tellraw)
- [x] Translation support
---
# World
### Disconnections
- [x] Particles
- [x] Nether and End dimensions
- [x] LCE client disconnect handling
- [x] Java player disconnect handling
---
# UI
## Player Animations
- [x] Crafting
- [x] Brewing stand
- [x] Furnace
- [x] Villager trading
- [ ] Accept trade _(Hard to implement, coming soon)_
- [x] Anvils
- [x] Chests
### LCE -> Java
- [x] Crouch
- [x] Sprint
- [x] Attack
- [x] Riding
- [x] Blocking
---
### Java -> LCE
- [x] Crouch
- [x] Sprint
- [x] Attack
- [x] Player
- [x] Entity
- [x] Eating
- [x] Sleeping
---
## Block Updates
### Java -> LCE
- [x] Creative
- [x] Survival
- [x] Explosions
---
### LCE -> Java
- [x] Creative
- [x] Survival
---
## World
- [x] Particles
- [x] Nether and End support
---
## UI
- [x] Crafting
- [x] Brewing stand
- [x] Furnace
- [x] Villager trading
- [ ] Accepting trades
- [x] Anvils
- [x] Chests
+4 -10
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,
};
/* --------------------------------------------------------------- */
+45 -51
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();
+112 -81
View File
@@ -5,10 +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");
@@ -29,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",
};
@@ -53,7 +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,
@@ -83,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");
}
});
@@ -1426,18 +1409,6 @@ async function connectToJavaServer(proxy, client) {
let tileY = packet.y / 32;
let tileZ = packet.z / 32;
// if (lceEntityType === 71 && itemFrameDirection !== null) {
// if (itemFrameDirection === 0) {
// tileZ--;
// } else if (itemFrameDirection === 1) {
// tileX++;
// } else if (itemFrameDirection === 2) {
// tileZ++;
// } else if (itemFrameDirection === 3) {
// tileX -= 1;
// }
// }
const entityInfo = {
entityId: lceEntityId,
javaEntityId: packet.entityId,
@@ -1691,7 +1662,7 @@ async function connectToJavaServer(proxy, client) {
const sleepWriter = new PacketWriter();
sleepWriter.writeInt(player.entityId);
sleepWriter.writeByte(0); //0=start sleep
sleepWriter.writeByte(0);
sleepWriter.writeInt(packet.location.x);
sleepWriter.writeByte(packet.location.y & 0xff);
sleepWriter.writeInt(packet.location.z);
@@ -2614,9 +2585,68 @@ function convertAndSendJavaChunk(proxy, client, javaChunk) {
}
}
const hasSky = client.javaDimension === 0;
const javaBlitBase = sectionCount * 8192;
const javaSlitBase = hasSky ? javaBlitBase + sectionCount * 2048 : -1;
const BLOCK_LIGHT_OFF = blockCount + halfBlockCount;
const SKY_LIGHT_OFF = blockCount + halfBlockCount + halfBlockCount;
rawData.fill(0xff, SKY_LIGHT_OFF, SKY_LIGHT_OFF + halfBlockCount);
let litSectionIdx = 0;
for (let sectionY = 0; sectionY < 16; sectionY++) {
if ((bitMap & (1 << sectionY)) === 0) continue;
const baseY = sectionY * 16;
const jBlit = javaBlitBase + litSectionIdx * 2048;
const jSlit =
javaSlitBase >= 0 ? javaSlitBase + litSectionIdx * 2048 : -1;
for (let sx = 0; sx < 16; sx++) {
const jLow = (sx & 1) === 0;
for (let sz = 0; sz < 16; sz++) {
for (let yp = 0; yp < 8; yp++) {
const y0 = yp * 2;
const totalY = baseY + y0;
if (totalY >= chunkHeight) continue;
const jb0 = (y0 * 256 + sz * 16 + sx) >> 1;
const jb1 = jb0 + 128;
let cIdx;
if (totalY < 128) {
cIdx = (sx * 16 + sz) * 64 + (totalY >> 1);
} else {
cIdx = 16384 + (sx * 16 + sz) * 64 + ((totalY - 128) >> 1);
}
//block light
if (jBlit + jb1 < javaData.length) {
const b0 = javaData[jBlit + jb0];
const b1 = javaData[jBlit + jb1];
const n0 = jLow ? b0 & 0x0f : (b0 >> 4) & 0x0f;
const n1 = jLow ? b1 & 0x0f : (b1 >> 4) & 0x0f;
rawData[BLOCK_LIGHT_OFF + cIdx] = n0 | (n1 << 4);
}
//sky light
if (jSlit >= 0 && jSlit + jb1 < javaData.length) {
const s0 = javaData[jSlit + jb0];
const s1 = javaData[jSlit + jb1];
const n0 = jLow ? s0 & 0x0f : (s0 >> 4) & 0x0f;
const n1 = jLow ? s1 & 0x0f : (s1 >> 4) & 0x0f;
rawData[SKY_LIGHT_OFF + cIdx] = n0 | (n1 << 4);
}
}
}
}
litSectionIdx++;
}
//biome fix!
const biomeOffset = blockCount + 3 * halfBlockCount;
const hasSky = client.javaDimension === 0;
const javaBiomeOffset = sectionCount * (hasSky ? 12288 : 10240);
if (javaChunk.x === 0 && javaChunk.z === 0) {
@@ -2645,8 +2675,9 @@ function convertAndSendJavaChunk(proxy, client, javaChunk) {
}
}
const skyLightOffset = blockCount + halfBlockCount + halfBlockCount;
if (skyLightOffset >= 0 && skyLightOffset + halfBlockCount <= rawSize) {
const hasSkyFallback = client.javaDimension === 0;
if (!hasSkyFallback) {
const skyLightOffset = blockCount + halfBlockCount + halfBlockCount;
for (let i = 0; i < halfBlockCount; i++) {
rawData[skyLightOffset + i] = 0xff;
}
+5 -1
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;
+466 -93
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");
@@ -70,7 +68,7 @@ class LCEProxy {
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,
0xcf, 0xd0, 0xd1, 0xfa, 0xfc, 0xfd, 0xfe, 0xff, 0xaa, 0xac,
]);
}
@@ -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 = 1;
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) {
@@ -499,6 +474,11 @@ class LCEProxy {
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;
@@ -625,6 +605,7 @@ class LCEProxy {
const loginKey = reader.readString();
client.state = "login";
client._netcodeVersion = netcodeVersion;
const writer = new PacketWriter();
writer.writeShort(MINECRAFT_NET_VERSION);
@@ -677,20 +658,135 @@ class LCEProxy {
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 = !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 */
@@ -702,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) {
@@ -727,7 +925,7 @@ class LCEProxy {
writer.writeInt(0); //ugcPlayersVersion
writer.writeByte(1); //difficulty (hardcoded to Easy)
writer.writeInt(0); //multiplayerInstanceId
writer.writeByte(0); //playerIndex
writer.writeByte(client.smallId); //playerIndex
writer.writeInt(0); //playerSkinId
writer.writeInt(0); //playerCapeId
writer.writeBoolean(false); //isGuest
@@ -2007,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 &&
@@ -2014,9 +2228,7 @@ class LCEProxy {
message.length > 0
) {
try {
client.javaClient.write("chat", {
message: message,
});
client.javaClient.write("chat", { message });
} catch (err) {
/* do nothing */
}
@@ -2024,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();
@@ -3725,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 {
@@ -3769,7 +4142,7 @@ class LCEProxy {
this.clients.splice(index, 1);
}
if (client.smallId) {
if (client.smallId !== undefined) {
this.usedSmallIds.delete(client.smallId);
}
@@ -3801,7 +4174,7 @@ class LCEProxy {
try {
return await connectToJavaServerFunc(this, client);
} catch (err) {
console.log(`DISCONNECTED: ${err}`);
/* do nothing */
}
}