gmll
Version:
A generic launcher core for building custom launchers
615 lines (614 loc) • 25.7 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.getResourcePacks = exports.getWorlds = exports.getMods = exports.getMetaPaths = void 0;
const crypto_1 = require("crypto");
const path_1 = require("path");
const nbt_js_1 = require("../../nbt.js");
const gfsl_1 = require("gfsl");
const config_js_1 = require("../../config.js");
/**
* @returns Some low level meta paths used to obtain some key files of this instance.
*/
async function getMetaPaths() {
const version = await this.getVersion();
const p = this.getDir();
return {
mods: p.getDir("mods"),
jarMods: p.getDir("jarmods"),
saves: p.getDir("saves"),
resourcePacks: p.getDir(Date.parse(version.json.releaseTime) >=
Date.parse("2013-06-13T15:32:23+00:00")
? "resourcepacks"
: "texturepacks"),
coreMods: p.getDir("coremods"),
configs: p.getDir("config"),
};
}
exports.getMetaPaths = getMetaPaths;
async function getIcon(file, d, jarPath) {
if (!jarPath || jarPath.length < 1)
return null;
file.unzip(d, { include: [jarPath] });
const jarFile = jarPath.split("/");
const logoFile = d.getFile(...jarFile);
if (logoFile.exists())
return `data:image/png;base64,${logoFile.toBase64()}`;
return null;
}
/**
* Gets information about mods in this instance. This includes the loader version plus some general
* information about the mod author and mod itself. This will also provide you the icon for a set mod if it can be obtained.\
*
* Works with Legacy forge, forge, fabric, riftLoader and liteLoader
*/
async function getMods() {
(0, config_js_1.emit)("parser.start", "mod", this);
const meta = await this.getMetaPaths();
const mods = [];
const tmp = gfsl_1.Dir.tmpdir()
.getDir("gmll", "mods", this.getDir().getName(), (0, crypto_1.createHash)("sha1").update(this.getDir().sysPath()).digest("hex"))
.rm()
.mkdir();
async function readMod(file, type, prefix) {
let name = file.getName();
try {
name = name.slice(0, name.lastIndexOf("."));
const fname = prefix ? (0, path_1.join)(prefix, name) : name;
const d = tmp.getDir(fname).mkdir();
await file.unzip(d, {
include: [
"pack.mcmeta",
"mcmod.info",
"fabric.mod.json",
"litemod.json",
"riftmod.json",
"quilt.mod.json",
"META-INF/mods.toml",
"META-INF/MANIFEST.MF",
],
});
let metaFile;
//Legacy forge
if ((metaFile = d.getFile("mcmod.info")).exists()) {
const mcInfoJson = metaFile.toJSON();
for (const mcInfoVal of mcInfoJson) {
const icon = await getIcon(file, d, mcInfoVal.logoFile);
mods.push({
id: mcInfoVal.modid,
authors: mcInfoVal.authorsList
? mcInfoVal.authorsList
: mcInfoVal.authors,
loader: "forge",
name: mcInfoVal.name
? prefix
? (0, path_1.join)(prefix, mcInfoVal.name)
: mcInfoVal.name
: name,
version: mcInfoVal.version,
path: file,
depends: mcInfoVal.dependencies,
screenshots: mcInfoVal.screenshots,
parent: mcInfoVal.parent,
updateUrl: mcInfoVal.updateUrl,
url: mcInfoVal.url,
description: mcInfoVal.description,
credits: mcInfoVal.credits,
mcversion: mcInfoVal.mcversion,
icon,
type,
});
}
return;
}
//Quilt mods
if ((metaFile = d.getFile("quilt.mod.json")).exists()) {
const metaInfo = metaFile.toJSON();
let icon;
if (metaInfo.quilt_loader.metadata.icon)
icon = await getIcon(file, d, metaInfo.quilt_loader.metadata.icon);
const authors = [];
if (metaInfo.quilt_loader.metadata?.contributors?.authors) {
authors.push(...Object.keys(metaInfo.quilt_loader.metadata.contributors.authors));
}
const depends = [];
if (metaInfo.quilt_loader.depends) {
metaInfo.quilt_loader.depends.forEach((e) => {
if (typeof e == "string")
depends.push(e);
else {
depends.push({
modId: e.id,
mandatory: !e.optional,
versionRange: e.versions,
side: "both",
});
}
});
}
mods.push({
id: metaInfo.quilt_loader.id,
authors: authors,
loader: "quilt",
name: metaInfo.quilt_loader.metadata?.name
? prefix
? (0, path_1.join)(prefix, metaInfo.quilt_loader.metadata?.name)
: metaInfo.quilt_loader.metadata?.name
: name,
version: metaInfo.quilt_loader.version,
path: file,
depends: depends,
description: metaInfo.quilt_loader.metadata?.description,
url: metaInfo.quilt_loader.metadata?.contact?.homepage,
source: metaInfo.quilt_loader.metadata?.contact?.sources,
licence: metaInfo.quilt_loader.metadata?.license,
icon,
type,
});
return;
}
//Fabric and rift
if ((metaFile = d.getFile("fabric.mod.json")).exists() ||
(metaFile = d.getFile("riftmod.json")).exists()) {
let metaInfo;
try {
metaInfo = metaFile.toJSON();
}
catch {
try {
metaInfo = JSON.parse(metaFile.read().replaceAll("\n", ""));
}
catch {
try {
metaInfo = JSON.parse(metaFile.read().replace(/("description":[^"]"[^"]*",)/g, ""));
}
catch (err) {
(0, config_js_1.emit)("parser.fail", "mod", err, file);
mods.push({
id: "unknown",
authors: [],
version: "unknown",
loader: metaFile.getName().endsWith("fabric.mod.json")
? "fabric"
: "riftMod",
name: name,
path: file,
type,
error: true,
});
return;
}
}
}
const icon = await getIcon(file, d, metaInfo.icon);
mods.push({
id: metaInfo.id,
authors: metaInfo.authors,
loader: metaFile.getName().endsWith("fabric.mod.json")
? "fabric"
: "riftMod",
name: metaInfo.name
? prefix
? (0, path_1.join)(prefix, metaInfo.name)
: metaInfo.name
: name,
version: metaInfo.version,
path: file,
depends: metaInfo.depends,
description: metaInfo.description,
url: metaInfo.contact?.homepage,
source: metaInfo.contact?.sources,
licence: metaInfo.license,
icon,
type,
});
return;
}
//LiteLoader
if ((metaFile = d.getFile("litemod.json")).exists()) {
const lintInfJson = metaFile.toJSON();
mods.push({
id: lintInfJson.name,
name: lintInfJson.name
? prefix
? (0, path_1.join)(prefix, lintInfJson.name)
: lintInfJson.name
: name,
authors: [lintInfJson.author],
version: lintInfJson.version || lintInfJson.revision || "unknown",
loader: "liteLoader",
description: lintInfJson.description,
type,
path: file,
});
return;
}
//Modern forge
if ((metaFile = d.getFile("META-INF", "mods.toml")).exists()) {
const modInfoJson = {
type,
id: "unknown",
authors: [],
version: "unknown",
loader: "forge",
name: name,
path: file,
depends: [],
};
let dep = {
modId: "",
mandatory: false,
versionRange: "",
ordering: "",
side: "",
};
const lines = metaFile.read().split("\n");
let inModHeader = false;
let independency = false;
const state1Map = new Map();
state1Map.set("license", (val) => (modInfoJson.licence = val));
state1Map.set("credits", (val) => (modInfoJson.credits = val));
state1Map.set("logoFile", async (val) => (modInfoJson.icon = await getIcon(file, d, val)));
const state2Map = new Map();
state2Map.set("modId", (val) => (modInfoJson.id = val));
state2Map.set("version", (val) => (modInfoJson.version = val));
state2Map.set("displayURL", (val) => (modInfoJson.url = val));
state2Map.set("updateJSONURL", (val) => (modInfoJson.updateUrl = val));
state2Map.set("credits", (val) => (modInfoJson.credits = val));
state2Map.set("authors", (val) => {
try {
modInfoJson.authors = val.startsWith("[") ? JSON.parse(val) : [val];
}
catch {
modInfoJson.authors = [val];
}
});
state2Map.set("description", (val) => (modInfoJson.description = val));
const state3Map = new Map();
state3Map.set("modId", (val) => (dep.modId = val));
state3Map.set("mandatory", (val) => (dep.mandatory = Boolean(val)));
state3Map.set("versionRange", (val) => (dep.versionRange = val));
state3Map.set("ordering", (val) => (dep.ordering = val));
state3Map.set("side", (val) => (dep.side = val));
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
line = line.includes("#") ? line.slice(0, line.indexOf("#")) : line;
if (line.length < 1)
continue;
if (line.includes("=")) {
const raw = line.split("=");
if (raw[1].includes("'''")) {
raw[1] = raw[1].slice(3);
while (!lines[++i].includes("'''") && i < lines.length)
raw[1] += lines[i] + "\n";
raw[i] += lines[i].slice(0, lines[i].indexOf("'''"));
}
raw[1] = raw[1].trim();
if (raw[1].startsWith('"'))
raw[1] = raw[1].slice(1);
if (raw[1].endsWith('"'))
raw[1] = raw[1].slice(0, raw[1].length - 1);
raw[0] = raw[0].trim();
if (state1Map.has(raw[0])) {
await state1Map.get(raw[0])(raw[1]);
continue;
}
if (independency && state3Map.has(raw[0])) {
await state3Map.get(raw[0])(raw[1]);
continue;
}
if (inModHeader && state2Map.has(raw[0])) {
await state2Map.get(raw[0])(raw[1]);
continue;
}
}
else if (line.startsWith("[[mods")) {
inModHeader = true;
}
else if (line.startsWith("[[dependencies")) {
inModHeader = false;
if (independency) {
if (!(modInfoJson.depends instanceof Array))
modInfoJson.depends = [];
modInfoJson.depends.push(dep);
dep = {
modId: "",
mandatory: false,
versionRange: "",
ordering: "",
side: "",
};
}
else {
modInfoJson.depends = [];
}
independency = true;
}
}
if (modInfoJson.depends instanceof Array) {
modInfoJson.depends.push(dep);
}
mods.push(modInfoJson);
return;
}
//Unknown modloader
if ((metaFile = d.getFile("META-INF", "MANIFEST.MF")).exists()) {
const metaInfFinal = {
type,
id: "unknown",
authors: [],
version: "unknown",
loader: "unknown",
name: name,
path: file,
depends: [],
};
const lines = metaFile.read().split("\n");
const state1Map = new Map();
state1Map.set("Manifest-Version", (val) => (metaInfFinal.version = val));
state1Map.set("Specification-Title", (val) => (metaInfFinal.name = val));
state1Map.set("Specification-Vendor", (val) => {
if (!metaInfFinal.authors.includes(val))
metaInfFinal.authors.push(val);
});
state1Map.set("Specification-Version", (val) => (metaInfFinal.version = val));
state1Map.set("Implementation-Title", (val) => (metaInfFinal.name = val));
state1Map.set("Implementation-Version", (val) => (metaInfFinal.version = val));
state1Map.set("Implementation-Vendor", (val) => {
if (!metaInfFinal.authors.includes(val))
metaInfFinal.authors.push(val);
});
state1Map.set("Automatic-Module-Name", (val) => (metaInfFinal.id = val));
state1Map.set("Fabric-Minecraft-Version", (val) => (metaInfFinal.mcversion = val));
state1Map.set("Fabric-Loom-Version", () => (metaInfFinal.loader = "fabric"));
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.length < 1)
continue;
if (!line.includes(":"))
continue;
const raw = line.split(":");
raw[1] = raw[1].trim();
if (raw[1].startsWith('"'))
raw[1] = raw[1].slice(1);
if (raw[1].endsWith('"'))
raw[1] = raw[1].slice(0, raw[1].length - 1);
raw[0] = raw[0].trim();
if (state1Map.has(raw[0])) {
await state1Map.get(raw[0])(raw[1]);
continue;
}
}
mods.push(metaInfFinal);
return;
}
if ((metaFile = d.getFile("pack.mcmeta")).exists()) {
let description = "";
let format = 0;
let name = d
.getName()
.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>{}[\]\\/]/gi, " ");
let icon = undefined;
try {
const p = metaFile.toJSON().pack;
if (p.name)
name = p.name;
if (p.description)
description = p.description;
if (p.pack_format)
format = p.pack_format;
}
catch {
const r = metaFile.read().split("\n");
description = r[(0, crypto_1.randomInt)(0, Math.min(r.length, 200))];
}
try {
icon = getIcon(file, d, "pack.png");
}
catch { }
mods.push({
id: "unknown",
authors: [],
version: `pack_format (${format}`,
loader: "unknown",
name: name,
icon,
path: file,
description,
type,
error: false,
});
return;
}
(0, config_js_1.emit)("parser.fail", "mod", "Possibly missing mod data!", file);
}
catch (err) {
(0, config_js_1.emit)("parser.fail", "mod", err, file);
}
mods.push({
id: "unknown",
authors: [],
version: "unknown",
loader: "unknown",
name: name,
path: file,
type,
error: true,
});
}
let c = 0;
let n = 0;
async function loop(d, type) {
const l = d.ls();
c += l.length;
for (const e of l) {
(0, config_js_1.emit)("parser.progress", e.sysPath(), ++n, c, c - n, this);
if (!(e instanceof gfsl_1.File)) {
for (const e2 of e.ls())
if (e2 instanceof gfsl_1.File)
await readMod(e2, type);
}
else {
await readMod(e, type);
}
}
}
await loop(meta.mods, "mod");
await loop(meta.coreMods, "coremod");
await loop(meta.jarMods, "jarmod");
(0, config_js_1.emit)("parser.done", "mods", this);
return mods;
}
exports.getMods = getMods;
/**
* Gets some general information about all the world files in this instance.
* It also decodes the level.DAT file for you and returns the decoded file as a JSON file.
*
* It also decodes the player data stored in the "playerdata" and "stats" subfolder in newer versions of the game.
*/
async function getWorlds() {
(0, config_js_1.emit)("parser.start", "save file", this);
const meta = await this.getMetaPaths();
const saves = [];
if (!meta.saves.exists())
return saves;
const l = meta.saves.ls();
const c = l.length;
let n = 0;
for (const e of l) {
try {
(0, config_js_1.emit)("parser.progress", e.sysPath(), ++n, c, c - n, this);
if (e instanceof gfsl_1.File)
return;
const DAT = e.getFile("level.dat");
const IMG = e.getFile("icon.png");
const PLAYERDATA = e.getDir("playerdata");
const PLAYERSTATS = e.getDir("stats");
let icon = undefined;
if (!DAT.exists())
return;
if (IMG.exists())
icon = IMG.sysPath();
const level = await (0, nbt_js_1.readDat)(DAT);
const players = {};
if (PLAYERDATA.exists()) {
for (const plr of PLAYERDATA.ls()) {
if (plr instanceof gfsl_1.File && plr.name.endsWith(".dat")) {
try {
const nm = plr.name.substring(0, plr.name.length - 4);
const PD = await (0, nbt_js_1.readDat)(plr);
let stats = undefined;
const statefile = PLAYERSTATS.getFile(`${nm}.json`);
if (statefile.exists())
stats = statefile.toJSON();
players[nm] = { data: PD, stats };
}
catch (e) {
(0, config_js_1.emit)("debug.warn", "Failed to parse player data!");
}
}
}
}
else {
players["Player"] = { data: level.Data.Player };
}
saves.push({
players,
name: level.Data?.LevelName || e.getName(),
level,
path: e,
icon,
});
}
catch (err) {
(0, config_js_1.emit)("parser.fail", "save file", err, gfsl_1.File);
}
}
(0, config_js_1.emit)("parser.done", "save file", this);
return saves;
}
exports.getWorlds = getWorlds;
/**
* Gets information about the installed resource and texture packs of this instance.
* This includes information like the pack icon, name, description, legal documents and credits.
*/
async function getResourcePacks() {
(0, config_js_1.emit)("parser.start", "resource/texture pack", this);
const meta = await this.getMetaPaths();
const packs = [];
if (!meta.resourcePacks.exists())
return packs;
const l = meta.resourcePacks.ls();
const tmp = gfsl_1.Dir.tmpdir()
.getDir("gmll", "resources", this.getDir().getName(), (0, crypto_1.createHash)("sha1").update(this.path).digest("hex"))
.rm()
.mkdir();
function readPackData(d, source) {
let icon = null;
const i = d.getFile("pack.png");
if (i.exists()) {
icon = `data:image/png;base64,${i.toBase64()}`;
}
let pack = d.getFile("pack.mcmeta");
let name = d
.getName()
.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>{}[\]\\/]/gi, " ");
let description = "A Minecraft resourcePack/texturePack";
let format = null;
if (!pack.exists())
pack = d.getFile("pack.txt");
if (pack.exists()) {
try {
const p = pack.toJSON().pack;
if (p.name)
name = p.name;
if (p.description)
description = p.description;
if (p.pack_format)
format = p.pack_format;
}
catch {
const r = pack.read().split("\n");
description = r[(0, crypto_1.randomInt)(0, Math.min(r.length, 200))];
}
}
let licenseFile = d.getFile("licence.txt");
if (!licenseFile.exists())
licenseFile = d.getFile("license.txt");
if (!licenseFile.exists())
licenseFile = d.getFile("terms of use.txt");
const license = licenseFile.exists() ? licenseFile.read() : null;
const creditsFile = d.getFile("credits.txt");
const credits = creditsFile.exists() ? creditsFile.read() : null;
return {
credits,
license,
name,
description,
format,
icon,
path: source,
};
}
const c = l.length;
let n = 0;
for (const e of l) {
(0, config_js_1.emit)("parser.progress", e.sysPath(), ++n, c, c - n, this);
try {
if (e instanceof gfsl_1.File) {
const name = e.getName();
const d = tmp.getDir(name.slice(0, name.lastIndexOf(".")));
await e.unzip(d, { include: ["*/*", "*/"] });
packs.push(readPackData(d, e));
}
else {
packs.push(readPackData(e, e));
}
}
catch (err) {
(0, config_js_1.emit)("parser.fail", "resource/texture pack", err, gfsl_1.File);
}
}
(0, config_js_1.emit)("parser.done", "resource/texture pack", this);
return packs;
}
exports.getResourcePacks = getResourcePacks;
;