Files
Plasma-Launcher/core/LauncherCore.js
2025-07-13 18:09:48 +02:00

472 lines
15 KiB
JavaScript
Executable File

const { exec, spawn } = require("child_process");
const path = require("path");
const fs = require("fs");
const { validateLibraryRules, getOS } = require("./Utils");
const http = require("http");
const https = require("https");
const nbt = require("prismarine-nbt");
const crypto = require("crypto");
const EventEmitter = require("events");
const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME)
async function getJSON(url) {
const response = await fetch(url);
return await response.json();
}
class ServersDatServer {
data = {
hidden: {
type: "byte",
value: 0
},
ip: {
type: "string",
value: "localhost"
},
name: {
type: "string",
value: "Minecraft Server"
}
};
constructor(name = "Minecraft Server", address = "localhost") {
if (typeof(name) === "object") {
this.data = name;
}else if (typeof(name) === "string") {
this.setName(name);
this.setAddress(address);
}
}
setName(name) {
return this.data.name.value = name;
}
setAddress(address) {
return this.data.ip.value = address;
}
getName() {
return this.data.name.value;
}
getAddress() {
return this.data.ip.value;
}
}
class ServersDat {
path = `servers.dat`;
type = "big";
data = {
type: "compound",
name: "",
value: {
servers: {
type: "list",
value: {
type: "compound",
value: []
}
}
}
}
servers = [];
constructor(_path) {
this.path = _path;
}
async load() {
if (!fs.existsSync(this.path)) {
return this;
}
const buffer = await fs.promises.readFile(this.path);
const { parsed, type } = await nbt.parse(buffer);
// this.data = parsed;
this.type = type;
if (parsed && parsed.value && parsed.value.servers && parsed.value.servers.value && parsed.value.servers.value.value && parsed.value.servers.value.value.length > 0) {
parsed.value.servers.value.value.forEach(server => {
this.addServer(new ServersDatServer(server));
});
}
//console.log("JSON serialized:", JSON.stringify(parsed.value.servers, null, 2));
return this;
}
async save() {
this.generateData();
const ws = fs.createWriteStream(this.path);
ws.write(nbt.writeUncompressed(this.data, "big"));
ws.close();
}
generateData() {
const dataServers = this.data.value.servers.value.value = [];
this.servers.forEach(server => {
dataServers.push(server.data);
});
return this.data;
}
updateOrAddToTop(server) {
const oldServer = this.getServerByIp(server.getAddress());
this.removeAllServersByAddress(server.getAddress());
if (oldServer) {
const serverData = oldServer.data;
Object.assign(serverData, server.data);
server.data = serverData;
}
this.addServerToTop(server);
}
addServerToTop(server) {
this.servers.unshift(server);
}
addServer(server) {
this.servers.push(server);
}
getServerByIp(address) {
for (let i in this.servers) {
const server = this.servers[i];
if (server.getAddress() === address) {
return server;
}
}
return null;
}
removeAllServersByAddress(address) {
const indexes = [];
for (let i in this.servers) {
const server = this.servers[i];
if (server.getAddress() === address) {
indexes.unshift(i);
}
}
indexes.forEach(i => {
this.servers.splice(i, 1);
});
return indexes.length;
}
}
class Instance {
events = new EventEmitter();
versionManifestUrl = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
resourcesUrl = "https://resources.download.minecraft.net";
serverListUrl = null;
workDir = path.join(__dirname, "..", "minecraft");
authData = {
playerName: "Cyanoure",
uuid: "faceface-face-face-face-facefaceface",
accessToken: "null",
clientId: "0",
xuid: "0",
type: "mojang",
isDemo: false
}
getAssetsDir() {
return `${this.workDir}/assets`;
}
setWorkDirAtDefaultLocation(dirname) {
this.workDir = `${sysRoot}/${dirname}`;
}
generateOfflineUUID() {
const hash = crypto.createHash("md5").update(`OfflinePlayer:${this.authData.playerName}`).digest("hex");
this.authData.uuid = `${hash.substring(0, 8)}-${hash.substring(8, 12)}-4${hash.substring(13, 16)}-a${hash.substring(17, 20)}-${hash.substring(20)}`;
}
async loadVersionManifest() {
this.versionManifest = await getJSON(this.versionManifestUrl);
return this.versionManifest;
}
async downloadFile(url, path) {
return new Promise(async resolve => {
if (!url || typeof(url) !== "string") {
console.log(`Invalid URL for '${path}'`);
resolve();
return;
}
if (fs.existsSync(path)) {
console.log(`Found '${path}'`);
resolve();
return;
}
const pathParts = path.split("/");
const dir = pathParts.slice(0, pathParts.length - 1).join("/");
if (!fs.existsSync(dir)) {
await fs.promises.mkdir(dir, { recursive: true });
}
console.log(`Downloading '${path}'...`);
const _http = url.startsWith("http://") ? http : https;
const file = fs.createWriteStream(path);
_http.get(url, response => {
response.pipe(file);
file.on("finish", () => {
file.close();
console.log(`Downloaded '${path}'.`);
resolve();
});
});
});
}
async getVersion(versionId) {
for(let i in this.versionManifest.versions) {
const versionData = this.versionManifest.versions[i];
if (versionData.id === versionId) {
if (!versionData.data) {
versionData.data = await getJSON(this.versionManifest.versions[i].url);
}
return versionData;
}
}
}
async getLatestRelease() {
return await this.getVersion(this.versionManifest.latest.release);
}
async getLatestSnapshot() {
return await this.getVersion(this.versionManifest.latest.snapshot);
}
async getAssets(version) {
const versionData = await this.getVersion(version);
if (versionData && versionData.data && versionData.data.assetIndex && versionData.data.assetIndex.url) {
return await getJSON(versionData.data.assetIndex.url);
}
return {};
}
async downloadVersion(version) {
this.events.emit("download-started", version);
this.events.emit("download-process", { percent: 0, event: "preparing", file: `Version: ${version}` });
const versionData = await this.getVersion(version);
const data = versionData.data;
const libraries = data.libraries;
const assets = await this.getAssets(version);
const assetObjects = assets.objects;
const assetObjectPaths = Object.keys(assetObjects);
const fullDownloadLength = libraries.length + assetObjectPaths.length + 1;
let downloadCounter = 0;
console.log("Downloading libraries...");
for (const i in libraries) {
downloadCounter++;
const libraryData = libraries[i];
if (!libraryData.rules || validateLibraryRules(libraryData.rules)) {
const url = libraryData.downloads.artifact.url;
const filePath = `libraries/${libraryData.downloads.artifact.path}`;
this.events.emit("download-process", { percent: downloadCounter/fullDownloadLength, event: "downloading", file: filePath });
await this.downloadFile(url, `${this.workDir}/${filePath}`);
}
}
console.log("Libraries downloaded.");
if (!fs.existsSync(`${this.getAssetsDir()}/indexes`)) {
fs.mkdirSync(`${this.getAssetsDir()}/indexes`, {recursive: true});
}
fs.writeFileSync(`${this.getAssetsDir()}/indexes/${data.assets}.json`, JSON.stringify(assets));
if (!fs.existsSync(`${this.getAssetsDir()}/objects`)) {
fs.mkdirSync(`${this.getAssetsDir()}/objects`, {recursive: true});
}
console.log("Downloading assets...");
for (const i in assetObjectPaths) {
downloadCounter++;
const assetPath = assetObjectPaths[i];
const assetObject = assetObjects[assetPath];
const assetHash = assetObject.hash;
const url = `${this.resourcesUrl}/${assetHash.slice(0, 2)}/${assetHash}`;
const filePath = `objects/${assetHash.slice(0, 2)}/${assetHash}`;
this.events.emit("download-process", { percent: downloadCounter/fullDownloadLength, event: "downloading", file: filePath });
await this.downloadFile(url, `${this.getAssetsDir()}/${filePath}`);
}
console.log("Assets downloaded.");
const versionPath = `versions/${version}/${version}.jar`;
this.events.emit("download-process", { percent: (fullDownloadLength - 1)/fullDownloadLength, event: "downloading", file: versionPath });
await this.downloadFile(data.downloads.client.url, `${this.workDir}/${versionPath}`);
fs.writeFileSync(`${this.workDir}/versions/${version}/${version}.json`, JSON.stringify(data));
this.events.emit("download-process", { percent: 1, event: "done", file: `Version: ${version}` });
}
async downloadLatestRelease() {
const version = this.versionManifest.latest.release;
return await this.downloadVersion(version);
}
async startVersion(version) {
if (version === "latest-release") {
version = this.versionManifest.latest.release;
} else if (version === "latest-snapshot") {
version = this.versionManifest.latest.snapshot;
}
await this.downloadVersion(version);
this.events.emit("starting");
const versionPath = `versions/${version}/${version}.jar`;
const versionData = await this.getVersion(version);
const data = versionData.data;
const libraries = data.libraries;
const librariesList = [versionPath];
for (const i in libraries) {
const libraryData = libraries[i];
if (!libraryData.rules || validateLibraryRules(libraryData.rules)) {
const filePath = `libraries/${libraryData.downloads.artifact.path}`;
librariesList.push(filePath);
}
}
const launchData = {
auth_player_name: this.authData.playerName,
version_name: version,
game_directory: this.workDir,
assets_root: this.getAssetsDir(),
assets_index_name: data.assets,
auth_uuid: this.authData.uuid,
auth_access_token: this.authData.accessToken,
clientid: this.authData.clientId,
auth_xuid: this.authData.xuid,
user_type: this.authData.type,
version_type: versionData.type
}
const launchDataKeys = Object.keys(launchData);
const gameArguments = [];
data.arguments.game.forEach(argument => {
if (typeof(argument) === "string") {
let arg = argument;
for (let i in launchDataKeys) {
const launchDataKey = launchDataKeys[i];
const launchDataValue = launchData[launchDataKey];
arg = arg.replaceAll("${"+launchDataKey+"}", launchDataValue);
}
gameArguments.push(arg);
} else {
if (argument.rules) {
let add = true;
let checkedRules = 0;
for (let i in argument.rules) {
const rule = argument.rules[i];
if (rule.features) {
if (rule.features.is_demo_user === true) {
checkedRules++;
add = add & this.authData.isDemo;
} else if (rule.features.is_demo_user === false) {
checkedRules++;
add = add & !this.authData.isDemo;
}
}
}
if (checkedRules === argument.rules.length && add && argument.value) {
gameArguments.push(argument.value);
}
}
}
});
const jvmArguments = [
"-Xms2G",
"-Xmx4G",
"-cp",
`${librariesList.join(";")}`,
data.mainClass,
...gameArguments
];
console.log(jvmArguments);
if (this.serverListUrl && this.serverListUrl !== "") {
const serverList = await getJSON(this.serverListUrl);
if (serverList && serverList.length > 0) {
const serversDat = new ServersDat(`${this.workDir}/servers.dat`);
await serversDat.load();
serverList.forEach(server => {
serversDat.updateOrAddToTop(new ServersDatServer(server.name, server.address));
});
await serversDat.save();
}
}
let connectingTo = "";
const mc = spawn("java", jvmArguments, {
cwd: this.workDir
});
this.events.emit("started", mc);
mc.stdout.on('data', function (data) {
const logLine = data.toString().trim();
if (connectingTo !== "" && logLine.indexOf("[System] [CHAT]") > -1) {
console.log("Connected to", connectingTo);
}
const connectingIndex = logLine.indexOf("Connecting to ");
if (connectingIndex > -1) {
const d = logLine.substring(14 + connectingIndex).split(",");
const address = d[0].trim();
const port = d[1].trim();
connectingTo = `${address}:${port}`;
} else {
connectingTo = "";
}
console.log('Minecraft(stdout): ' + logLine);
});
mc.stderr.on('data', function (data) {
console.log('Minecraft(stderr): ' + data.toString().trim());
});
mc.on('exit', function (code) {
console.log('Minecraft exited with code ' + code.toString());
});
}
async startLatestRelease() {
const version = this.versionManifest.latest.release;
return await this.startVersion(version);
}
}
module.exports = { Instance, ServersDat, ServersDatServer }