472 lines
15 KiB
JavaScript
Executable File
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 } |