Files
GDL-Continuation/src/app/desktop/utils/index.js
T
2025-07-31 01:32:28 +02:00

1100 lines
30 KiB
JavaScript

import { promises as fs } from 'fs';
import originalFs from 'original-fs';
import fse from 'fs-extra';
import { extractFull } from 'node-7z';
import jimp from 'jimp/es';
import makeDir from 'make-dir';
import { promisify } from 'util';
import { ipcRenderer } from 'electron';
import path from 'path';
import crypto from 'crypto';
import { exec, spawn } from 'child_process';
import {
MC_LIBRARIES_URL,
FABRIC,
FORGE,
LATEST_JAVA_VERSION
} from '../../../common/utils/constants';
import {
addQuotes,
removeDuplicates,
sortByForgeVersionDesc
} from '../../../common/utils';
import { getAddon, getAddonFile, mcGetPlayerSkin } from '../../../common/api';
import { downloadFile } from './downloader';
import browserDownload from '../../../common/utils/browserDownload';
import { REQUIRED_JAVA_ARGS } from './constants';
export const isDirectory = source =>
fs.lstat(source).then(r => r.isDirectory());
export const getDirectories = async source => {
const dirs = await fs.readdir(source);
return Promise.all(
dirs
.map(name => path.join(source, name))
.filter(isDirectory)
.map(dir => path.basename(dir))
);
};
export const mavenToArray = (s, nativeString, forceExt) => {
const pathSplit = s.split(':');
const fileName = pathSplit[3]
? `${pathSplit[2]}-${pathSplit[3]}`
: pathSplit[2];
const finalFileName = fileName.includes('@')
? fileName.replace('@', '.')
: `${fileName}${nativeString || ''}${forceExt || '.jar'}`;
const initPath = pathSplit[0]
.split('.')
.concat(pathSplit[1])
.concat(pathSplit[2].split('@')[0])
.concat(`${pathSplit[1]}-${finalFileName}`);
return initPath;
};
export const convertOSToMCFormat = ElectronFormat => {
switch (ElectronFormat) {
case 'win32':
return 'windows';
case 'darwin':
return 'osx';
case 'linux':
return 'linux';
default:
return false;
}
};
export const convertOSToJavaFormat = ElectronFormat => {
switch (ElectronFormat) {
case 'win32':
return 'windows';
case 'darwin':
return 'mac';
case 'linux':
return 'linux';
default:
return false;
}
};
export const convertArchToJavaFormat = ElectronFormat => {
switch (ElectronFormat) {
case 'x64':
return 'x64';
case 'ia32':
return 'x32';
case 'arm':
return 'arm';
case 'arm64':
return 'aarch64';
default:
return false;
}
};
export const skipLibrary = lib => {
let skip = false;
if (lib.rules) {
skip = true;
lib.rules.forEach(({ action, os, features }) => {
if (features) return true;
if (
action === 'allow' &&
((os && os.name === convertOSToMCFormat(process.platform)) || !os)
) {
skip = false;
}
if (
action === 'disallow' &&
((os && os.name === convertOSToMCFormat(process.platform)) || !os)
) {
skip = true;
}
});
}
return skip;
};
export const librariesMapper = (libraries, librariesPath) => {
return removeDuplicates(
libraries
.filter(v => !skipLibrary(v))
.reduce((acc, lib) => {
const tempArr = [];
// Forge libs
if (lib.downloads && lib.downloads.artifact) {
let { url } = lib.downloads.artifact;
// Handle special case for forge universal where the url is "".
if (lib.downloads.artifact.url === '') {
url = `https://files.minecraftforge.net/${mavenToArray(
lib.name
).join('/')}`;
}
tempArr.push({
url,
path: path.join(librariesPath, lib.downloads.artifact.path),
sha1: lib.downloads.artifact.sha1,
name: lib.name
});
}
const native = (
(lib?.natives &&
lib?.natives[convertOSToMCFormat(process.platform)]) ||
''
).replace(
'${arch}', // eslint-disable-line no-template-curly-in-string
'64'
);
// Vanilla native libs
if (native && lib?.downloads?.classifiers[native]) {
tempArr.push({
url: lib.downloads.classifiers[native].url,
path: path.join(
librariesPath,
lib.downloads.classifiers[native].path
),
sha1: lib.downloads.classifiers[native].sha1,
natives: true,
name: lib.name
});
}
if (tempArr.length === 0) {
tempArr.push({
url: `${lib.url || `${MC_LIBRARIES_URL}/`}${mavenToArray(
lib.name,
native && `-${native}`
).join('/')}`,
path: path.join(librariesPath, ...mavenToArray(lib.name, native)),
...(native && { natives: true }),
name: lib.name
});
}
// Patch log4j versions https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228
for (const k in tempArr) {
if (tempArr[k]?.url?.includes('log4j')) {
// Get rid of all log4j aside from official
if (!tempArr[k].url.includes('libraries.minecraft.net')) {
tempArr[k] = null;
continue;
}
if (tempArr[k].url.includes('2.0-beta9')) {
tempArr[k] = {
url: tempArr[k].url
.replace(
'libraries.minecraft.net',
'cdn.gdlauncher.com/maven'
)
.replace(/2.0-beta9/g, '2.0-beta9-fixed'),
path: tempArr[k].path.replace(/2.0-beta9/g, '2.0-beta9-fixed'),
sha1: tempArr[k].url.includes('log4j-api')
? 'b61eaf2e64d8b0277e188262a8b771bbfa1502b3'
: '677991ea2d7426f76309a73739cecf609679492c',
name: tempArr[k].name
};
} else {
const splitName = tempArr[k].name.split(':');
splitName[splitName.length - 1] = '2.15.0';
const patchedName = splitName.join(':');
// Assuming we can use 2.15
tempArr[k] = {
url: `https://cdn.gdlauncher.com/maven/${mavenToArray(
patchedName,
native
).join(path.sep)}`,
path: path.join(
librariesPath,
...mavenToArray(patchedName, native)
),
sha1: tempArr[k].url.includes('log4j-api')
? '42319af9991a86b4475ab3316633a3d03e2d29e1'
: '9bd89149d5083a2a3ab64dcc88b0227da14152ec'
};
}
}
}
return acc.concat(tempArr.filter(_ => _));
}, []),
'url'
);
};
export const getFilteredVersions = (
vanillaManifest,
forgeManifest,
fabricManifest
) => {
const versions = [
{
value: 'vanilla',
label: 'Vanilla',
children: [
{
value: 'release',
label: 'Releases',
children: vanillaManifest.versions
.filter(v => v.type === 'release')
.map(v => ({
value: v.id,
label: v.id
}))
},
{
value: 'snapshot',
label: 'Snapshots',
children: vanillaManifest.versions
.filter(v => v.type === 'snapshot')
.map(v => ({
value: v.id,
label: v.id
}))
},
{
value: 'old_beta',
label: 'Old Beta',
children: vanillaManifest.versions
.filter(v => v.type === 'old_beta')
.map(v => ({
value: v.id,
label: v.id
}))
},
{
value: 'old_alpha',
label: 'Old Alpha',
children: vanillaManifest.versions
.filter(v => v.type === 'old_alpha')
.map(v => ({
value: v.id,
label: v.id
}))
}
]
},
{
value: 'forge',
label: 'Forge',
children: Object.entries(forgeManifest).map(([k, v]) => ({
value: k,
label: k,
children: v.sort(sortByForgeVersionDesc).map(child => ({
value: child,
label: child.split('-')[1]
}))
}))
},
{
value: 'fabric',
label: 'Fabric',
children: [
{
value: 'release',
label: 'Releases',
children: fabricManifest.game
.filter(v => v.stable)
.map(v => ({
value: v.version,
label: v.version,
children: fabricManifest.loader.map(c => ({
value: c.version,
label: c.version
}))
}))
},
{
value: 'snapshot',
label: 'Snapshots',
children: fabricManifest.game
.filter(v => !v.stable)
.map(v => ({
value: v.version,
label: v.version,
children: fabricManifest.loader.map(c => ({
value: c.version,
label: c.version
}))
}))
}
]
},
{
value: 'quilt',
label: 'Quilt',
children: [
{
value: 'release',
label: 'Releases',
children: fabricManifest.game
.filter(v => v.stable)
.map(v => ({
value: v.version,
label: v.version,
children: fabricManifest.loader.map(c => ({
value: c.version,
label: c.version
}))
}))
},
{
value: 'snapshot',
label: 'Snapshots',
children: fabricManifest.game
.filter(v => !v.stable)
.map(v => ({
value: v.version,
label: v.version,
children: fabricManifest.loader.map(c => ({
value: c.version,
label: c.version
}))
}))
}
]
},
{
value: 'neoforge',
label: 'NeoForge',
children: Object.entries(forgeManifest).map(([k, v]) => ({
value: k,
label: k,
children: v.sort(sortByForgeVersionDesc).map(child => ({
value: child,
label: child.split('-')[1]
}))
}))
}
];
return versions;
};
export const isLatestJavaDownloaded = async (
meta,
userData,
retry,
version = 8
) => {
const javaOs = convertOSToJavaFormat(process.platform);
const javaArch = convertArchToJavaFormat(process.arch);
let log = null;
const isJavaLatest = version === LATEST_JAVA_VERSION;
const manifest = isJavaLatest ? meta.javaLatest : meta.java;
const javaMeta = manifest.find(
v =>
v.os === javaOs &&
v.architecture === javaArch &&
(v.binary_type === 'jre' || v.binary_type === 'jdk')
);
const javaFolder = path.join(
userData,
'java',
javaMeta.version_data.openjdk_version
);
// Check if it's downloaded, if it's latest version and if it's a valid download
let isValid = true;
const javaExecutable = path.join(
javaFolder,
'bin',
`java${javaOs === 'windows' ? '.exe' : ''}`
);
try {
await fs.access(javaFolder);
log = await promisify(exec)(`"${javaExecutable}" -version`);
} catch (err) {
if (retry) {
if (process.platform !== 'win32') {
try {
await promisify(exec)(`chmod +x "${javaExecutable}"`);
await promisify(exec)(`chmod 755 "${javaExecutable}"`);
} catch {
// swallow error
}
}
return isLatestJavaDownloaded(meta, userData, null, version);
}
isValid = false;
}
// Return stderr because that garbage of a language which is java
// outputs the result of the version command to the error stream
// https://stackoverflow.com/questions/13483443/why-does-java-version-go-to-stderr
return { isValid, log: log?.stderr };
};
export const get7zPath = async () => {
// Get userData from ipc because we can't always get this from redux
let baseDir = await ipcRenderer.invoke('getExecutablePath');
const isDev = process.env.NODE_ENV === 'development';
if (isDev) {
baseDir = path.resolve(baseDir, '../../');
if (process.platform === 'win32') {
baseDir = path.join(baseDir, '7zip-bin/win/x64');
} else if (process.platform === 'linux') {
baseDir = path.join(baseDir, '7zip-bin/linux/x64');
} else if (process.platform === 'darwin') {
baseDir = path.resolve(baseDir, '../../../', '7zip-bin/mac/x64');
}
}
if (process.platform === 'linux') {
return path.join(baseDir, '7za');
}
if (process.platform === 'darwin') {
return path.resolve(baseDir, isDev ? '' : '../', '7za');
}
return path.join(baseDir, '7za.exe');
};
get7zPath();
export const extractAll = async (
source,
destination,
args = {},
funcs = {}
) => {
const sevenZipPath = await get7zPath();
const extraction = extractFull(source, destination, {
...args,
yes: true,
$bin: sevenZipPath,
$spawnOptions: { shell: true }
});
let extractedParentDir = null;
await new Promise((resolve, reject) => {
if (funcs.progress) {
extraction.on('progress', ({ percent }) => {
funcs.progress(percent);
});
}
extraction.on('data', data => {
if (!extractedParentDir) {
[extractedParentDir] = data.file.split('/');
}
});
extraction.on('end', () => {
funcs.end?.();
resolve(extractedParentDir);
});
extraction.on('error', err => {
funcs.error?.();
reject(err);
});
});
return { extraction, extractedParentDir };
};
export const extractNatives = async (libraries, instancePath) => {
const extractLocation = path.join(instancePath, 'natives');
await Promise.all(
libraries
.filter(l => l.natives)
.map(async l => {
await extractAll(l.path, extractLocation, {
$raw: ['-xr!META-INF']
});
})
);
};
export const copyAssetsToResources = async assets => {
await Promise.all(
assets.map(async asset => {
try {
await fs.access(asset.resourcesPath);
} catch {
await makeDir(path.dirname(asset.resourcesPath));
await fs.copyFile(asset.path, asset.resourcesPath);
}
})
);
};
export const copyAssetsToLegacy = async assets => {
await Promise.all(
assets.map(async asset => {
try {
await fs.access(asset.legacyPath);
} catch {
await makeDir(path.dirname(asset.legacyPath));
await fs.copyFile(asset.path, asset.legacyPath);
}
})
);
};
const hiddenToken = '__HIDDEN_TOKEN__';
export const getJVMArguments112 = (
libraries,
mcjar,
instancePath,
assetsPath,
mcJson,
account,
memory,
resolution,
hideAccessToken,
jvmOptions = []
) => {
const needsQuote = process.platform !== 'win32';
const args = [];
args.push('-cp');
args.push(
[...libraries, mcjar]
.filter(l => !l.natives)
.map(l => `${addQuotes(needsQuote, l.path)}`)
.join(process.platform === 'win32' ? ';' : ':')
);
const javaArgs = jvmOptions.filter(Boolean);
// if (process.platform === "darwin") {
// args.push("-Xdock:name=instancename");
// args.push("-Xdock:icon=instanceicon");
// }
args.push(`-Xmx${memory}m`);
args.push(`-Xms${memory}m`);
args.push(...REQUIRED_JAVA_ARGS.split(' '));
if (javaArgs.length > 0) args.push(...javaArgs);
args.push(
`-Djava.library.path=${addQuotes(
needsQuote,
path.join(instancePath, 'natives')
)}`
);
args.push(
`-Dminecraft.applet.TargetDirectory=${addQuotes(needsQuote, instancePath)}`
);
if (mcJson.logging) {
args.push(mcJson?.logging?.client?.argument || '');
}
args.push(mcJson.mainClass);
const mcArgs = mcJson.minecraftArguments.split(' ');
const argDiscovery = /\${*(.*)}/;
for (let i = 0; i < mcArgs.length; i += 1) {
if (argDiscovery.test(mcArgs[i])) {
const identifier = mcArgs[i].match(argDiscovery)[1];
let val = null;
switch (identifier) {
case 'auth_player_name':
val = account.selectedProfile.name.trim();
break;
case 'version_name':
val = mcJson.id;
break;
case 'game_directory':
val = `${addQuotes(needsQuote, instancePath)}`;
break;
case 'assets_root':
val = `${addQuotes(needsQuote, assetsPath)}`;
break;
case 'game_assets':
val = `${path.join(assetsPath, 'virtual', 'legacy')}`;
break;
case 'assets_index_name':
val = mcJson.assets;
break;
case 'auth_uuid':
val = account.selectedProfile.id.trim();
break;
case 'auth_access_token':
val = hideAccessToken ? hiddenToken : account.accessToken;
break;
case 'auth_session':
val = hideAccessToken ? hiddenToken : account.accessToken;
break;
case 'user_type':
val = 'mojang';
break;
case 'version_type':
val = mcJson.type;
break;
case 'user_properties':
val = '{}';
break;
default:
break;
}
if (val != null) {
mcArgs[i] = val;
}
if (typeof args[i] === 'string' && !needsQuote) {
args[i] = args[i].replaceAll('"', '');
}
}
}
args.push(...mcArgs);
if (resolution) {
args.push(`--width=${resolution.width}`);
args.push(`--height=${resolution.height}`);
}
return args;
};
export const getJVMArguments113 = (
libraries,
mcjar,
instancePath,
assetsPath,
mcJson,
account,
memory,
resolution,
hideAccessToken,
jvmOptions = []
) => {
const argDiscovery = /\${*(.*)}/;
let args = mcJson.arguments.jvm.filter(v => !skipLibrary(v));
const needsQuote = process.platform !== 'win32';
const javaArgs = jvmOptions.filter(Boolean);
// if (process.platform === "darwin") {
// args.push("-Xdock:name=instancename");
// args.push("-Xdock:icon=instanceicon");
// }
args.push(`-Xmx${memory}m`);
args.push(`-Xms${memory}m`);
args.push(
`-Dminecraft.applet.TargetDirectory=${addQuotes(needsQuote, instancePath)}`
);
if (mcJson.logging) {
args.push(mcJson?.logging?.client?.argument || '');
}
args.push(...REQUIRED_JAVA_ARGS.split(' '));
if (javaArgs.length > 0) args.push(...javaArgs);
// Eventually inject additional arguments (from 1.17 (?))
if (mcJson?.forge?.arguments?.jvm) {
args.push(...mcJson.forge.arguments.jvm);
}
args.push(mcJson.mainClass);
args.push(...mcJson.arguments.game.filter(v => !skipLibrary(v)));
for (let i = 0; i < args.length; i += 1) {
if (typeof args[i] === 'object' && args[i].rules) {
if (typeof args[i].value === 'string') {
args[i] = `${addQuotes(needsQuote, args[i].value)}`;
} else if (typeof args[i].value === 'object') {
args.splice(i, 1, ...args[i].value.map(v => `${v}`));
}
i -= 1;
} else if (typeof args[i] === 'string') {
if (argDiscovery.test(args[i])) {
const identifier = args[i].match(argDiscovery)[1];
let val = null;
switch (identifier) {
case 'auth_player_name':
val = account.selectedProfile.name.trim();
break;
case 'version_name':
val = mcJson.id;
break;
case 'game_directory':
val = `${addQuotes(needsQuote, instancePath)}`;
break;
case 'assets_root':
val = `${addQuotes(needsQuote, assetsPath)}`;
break;
case 'assets_index_name':
val = mcJson.assets;
break;
case 'auth_uuid':
val = account.selectedProfile.id.trim();
break;
case 'auth_access_token':
val = hideAccessToken ? hiddenToken : account.accessToken;
break;
case 'user_type':
val = 'mojang';
break;
case 'version_type':
val = mcJson.type;
break;
case 'resolution_width':
val = 800;
break;
case 'resolution_height':
val = 600;
break;
case 'natives_directory':
val = args[i].replace(
argDiscovery,
`${addQuotes(needsQuote, path.join(instancePath, 'natives'))}`
);
break;
case 'launcher_name':
val = args[i].replace(argDiscovery, 'GDLauncher');
break;
case 'launcher_version':
val = args[i].replace(argDiscovery, '1.0');
break;
case 'classpath':
val = [...libraries, mcjar]
.filter(l => !l.natives)
.map(l => `${addQuotes(needsQuote, l.path)}`)
.join(process.platform === 'win32' ? ';' : ':');
break;
default:
break;
}
if (val !== null) {
args[i] = val;
}
}
if (!needsQuote) args[i] = args[i].replaceAll('"', '');
}
}
if (resolution) {
args.push(`--width=${resolution.width}`);
args.push(`--height=${resolution.height}`);
}
args = args.filter(arg => {
return arg != null;
});
return args;
};
export const readJarManifest = async (jarPath, property) => {
const { extraction: list } = await extractAll(jarPath, '.', {
toStdout: true,
$cherryPick: 'META-INF/MANIFEST.MF'
});
if (list.info.has(property)) return list.info.get(property);
return null;
};
export const patchForge113 = async (
forgeJson,
mainJar,
librariesPath,
installerPath,
mcJsonPath,
universalPath,
javaPath,
updatePercentage
) => {
const { processors } = forgeJson;
const replaceIfPossible = arg => {
const finalArg = arg.replace('{', '').replace('}', '');
if (forgeJson.data[finalArg]) {
// Handle special case
if (finalArg === 'BINPATCH') {
return `"${path
.join(librariesPath, ...mavenToArray(forgeJson.path || universalPath))
.replace('.jar', '-clientdata.lzma')}"`;
}
// Return replaced string
return forgeJson.data[finalArg].client;
}
// Fix forge madness
return arg
.replace('{SIDE}', `client`)
.replace('{ROOT}', `"${path.dirname(installerPath)}"`)
.replace('{MINECRAFT_JAR}', `"${mainJar}"`)
.replace('{MINECRAFT_VERSION}', `"${mcJsonPath}"`)
.replace('{INSTALLER}', `"${installerPath}"`)
.replace('{LIBRARY_DIR}', `"${librariesPath}"`);
};
const computePathIfPossible = arg => {
if (arg[0] === '[') {
return `"${path.join(
librariesPath,
...mavenToArray(arg.replace('[', '').replace(']', ''))
)}"`;
}
return arg;
};
let counter = 1;
/* eslint-disable no-await-in-loop, no-restricted-syntax */
for (const key in processors) {
if (Object.prototype.hasOwnProperty.call(processors, key)) {
const p = processors[key];
if (p?.sides && !(p?.sides || []).includes('client')) {
continue;
}
const filePath = path.join(librariesPath, ...mavenToArray(p.jar));
const args = p.args
.map(arg => replaceIfPossible(arg))
.map(arg => computePathIfPossible(arg));
const classPaths = p.classpath.map(
cp => `"${path.join(librariesPath, ...mavenToArray(cp))}"`
);
const mainClass = await readJarManifest(filePath, 'Main-Class');
await new Promise(resolve => {
const ps = spawn(
`"${javaPath}"`,
[
'-classpath',
[`"${filePath}"`, ...classPaths].join(path.delimiter),
mainClass,
...args
],
{ shell: true }
);
ps.stdout.on('data', data => {
console.log(data.toString());
});
ps.stderr.on('data', data => {
console.error(`ps stderr: ${data}`);
});
ps.on('close', code => {
if (code !== 0) {
console.log(`process exited with code ${code}`);
resolve();
}
resolve();
});
});
updatePercentage(counter, processors.length);
counter += 1;
}
}
/* eslint-enable no-await-in-loop, no-restricted-syntax */
};
export const importAddonZip = async (
zipPath,
instancePath,
instanceTempPath,
tempPath
) => {
const tempZipFile = path.join(instanceTempPath, 'addon.zip');
await makeDir(instanceTempPath);
if (zipPath.includes(tempPath)) {
await fse.move(zipPath, tempZipFile);
} else {
await fse.copyFile(zipPath, tempZipFile);
}
const instanceManifest = path.join(instancePath, 'manifest.json');
// Wait 500ms to avoid `The process cannot access the file because it is being used by another process.`
await new Promise(resolve => {
setTimeout(() => resolve(), 500);
});
await extractAll(tempZipFile, instancePath, {
yes: true,
$cherryPick: 'manifest.json'
});
const manifest = await fse.readJson(instanceManifest);
return manifest;
};
export const downloadAddonZip = async (id, fileID, instancePath, tempPath) => {
const data = await getAddonFile(id, fileID);
const instanceManifest = path.join(instancePath, 'manifest.json');
const zipFile = path.join(tempPath, 'addon.zip');
if (data.downloadUrl) {
await downloadFile(zipFile, data.downloadUrl);
} else {
const addon = await getAddon(id);
const addonZipUrl = `https://www.curseforge.com/minecraft/modpacks/${addon.slug}/download/${fileID}`;
await browserDownload(addonZipUrl, zipFile);
}
// Wait 500ms to avoid `The process cannot access the file because it is being used by another process.`
await new Promise(resolve => {
setTimeout(() => resolve(), 500);
});
await extractAll(zipFile, instancePath, {
yes: true,
$cherryPick: 'manifest.json'
});
const manifest = await fse.readJson(instanceManifest);
return manifest;
};
export const getPlayerSkin = async uuid => {
const playerSkin = await mcGetPlayerSkin(uuid);
const { data } = playerSkin;
const base64 = data.properties[0].value;
const decoded = JSON.parse(Buffer.from(base64, 'base64').toString());
return decoded?.textures?.SKIN?.url;
};
export const extractFace = async buffer => {
const face = await jimp.read(buffer);
const hat = await jimp.read(buffer);
face.crop(8, 8, 8, 8);
hat.crop(40, 8, 8, 8);
face.scale(10, jimp.RESIZE_NEAREST_NEIGHBOR);
hat.scale(10, jimp.RESIZE_NEAREST_NEIGHBOR);
face.composite(hat, 0, 0);
const ImageBuffer = await face.getBufferAsync(jimp.MIME_PNG);
return ImageBuffer.toString('base64');
};
export const normalizeModData = (data, projectID, modName) => {
const temp = data;
temp.name = modName;
if (data.fileFingerprint) {
temp.packageFingerprint = data.fileFingerprint;
}
if (data.projectID && data.fileID) return temp;
if (data.id) {
temp.projectID = projectID;
temp.fileID = data?.file?.id || data.id;
}
return temp;
};
export const reflect = p =>
p.then(
v => ({ v, status: true }),
e => ({ e, status: false })
);
export const convertCompletePathToInstance = (f, instancesPath) => {
const escapeRegExp = stringToGoIntoTheRegex => {
return stringToGoIntoTheRegex.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
};
return f.replace(new RegExp(escapeRegExp(instancesPath), 'gi'), '');
};
export const isMod = (fileName, instancesPath) =>
/^(\\|\/)([\w\d\-.{}()[\]@#$%^&!\s])+((\\|\/)mods((\\|\/)(([^\\/])*))(\.jar|\.disabled))$/.test(
convertCompletePathToInstance(fileName, instancesPath)
);
export const isInstanceFolderPath = (f, instancesPath) =>
/^(\\|\/)([\w\d-.{}()[\]@#$%^&!\s])+$/.test(
convertCompletePathToInstance(f, instancesPath)
);
export const isFileModFabric = file => {
return (
(file.gameVersions.includes('Fabric') ||
file.modules.find(v => v.foldername === 'fabric.mod.json')) &&
!file.gameVersions.includes('Forge')
);
};
export const filterFabricFilesByVersion = (files, version) => {
return files.filter(v => {
if (Array.isArray(v.gameVersions)) {
return v.gameVersions.includes(version) && isFileModFabric(v);
}
return v.gameVersions === version;
});
};
export const filterForgeFilesByVersion = (files, version) => {
return files.filter(v => {
if (Array.isArray(v.gameVersions)) {
return v.gameVersions.includes(version) && !isFileModFabric(v);
}
return v.gameVersions === version;
});
};
export const getFirstPreferredCandidate = (files, release) => {
let counter = release || 1;
let latestFile = null;
while (counter <= 3 && !latestFile) {
const c = counter;
const latest = files.find(v => v.releaseType <= c);
if (latest) {
latestFile = latest;
}
counter += 1;
}
return latestFile;
};
export const getFileHash = async (filePath, algorithm = 'sha1') => {
// Calculate sha1 on original file
const shasum = crypto.createHash(algorithm);
const s = originalFs.ReadStream(filePath);
s.on('data', data => {
shasum.update(data);
});
const hash = await new Promise(resolve => {
s.on('end', () => {
resolve(shasum.digest('hex'));
});
});
return hash;
};
export const getFilesRecursive = async dir => {
const subdirs = await originalFs.promises.readdir(dir);
const files = await Promise.all(
subdirs.map(async subdir => {
const res = path.resolve(dir, subdir);
return (await originalFs.promises.stat(res)).isDirectory()
? getFilesRecursive(res)
: res;
})
);
return files.reduce((a, f) => a.concat(f), []);
};
export const extractFabricVersionFromManifest = manifest => {
// Backwards compatability for manifest entries that use the `yarn`
// property to set the fabric loader version. Newer manifests use the
// format `fabric-<version>` in the id.
let loaderVersion = manifest?.minecraft?.modLoaders[0]?.yarn;
if (!loaderVersion) {
loaderVersion = manifest?.minecraft?.modLoaders[0]?.id?.split('-', 2)[1];
}
return loaderVersion;
};
export const convertcurseForgeToCanonical = (
curseForge,
mcVersion,
forgeManifest
) => {
const patchedCurseForge = curseForge.replace('forge-', '');
const forgeEquivalent = forgeManifest[mcVersion].find(v => {
return v.split('-')[1] === patchedCurseForge;
});
return forgeEquivalent;
};
export const getPatchedInstanceType = instance => {
const isForge = instance.loader?.loaderType === FORGE;
const hasJumpLoader = (instance.mods || []).find(v => v.projectID === 361988);
if (isForge && !hasJumpLoader) {
return FORGE;
}
return FABRIC;
};