Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Modrinth mod/modpack support #1391

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"include": ["src/**/*.js"]
}
21 changes: 18 additions & 3 deletions src/app/desktop/components/Instances/Instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,14 +389,29 @@ const Instance = ({ instanceName }) => {
disabled={Boolean(isInQueue) || Boolean(isPlaying)}
onClick={async () => {
let manifest = null;
try {
const isCursePack = await fs
.stat(path.join(instancesPath, instanceName, 'manifest.json'))
.then(() => true)
.catch(() => false);

if (isCursePack) {
// CurseForge
manifest = JSON.parse(
await fs.readFile(
path.join(instancesPath, instanceName, 'manifest.json')
)
);
} catch {
// NO-OP
} else {
// Modrinth
manifest = JSON.parse(
await fs.readFile(
path.join(
instancesPath,
instanceName,
'modrinth.index.json'
)
)
);
}

dispatch(
Expand Down
1 change: 0 additions & 1 deletion src/app/desktop/components/Instances/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const Container = styled.div`
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: center;
margin-bottom: 2rem;
`;

Expand Down
105 changes: 105 additions & 0 deletions src/app/desktop/utils/downloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,111 @@ const downloadFileInstance = async (fileName, url, sha1, legacyPath) => {
}
};

/**
* @param {{ path: string, hashes: { sha1: string, sha512: string }, downloads: string[] }[]} files
* @param {string} instancePath
* @param {number} updatePercentage
* @param {number} threads
*/
export const downloadInstanceFilesWithFallbacks = async (
files,
instancePath,
updatePercentage,
threads = 4
) => {
let downloaded = 0;
await pMap(
files,
async file => {
let counter = 0;
let res = false;
do {
counter += 1;
if (counter !== 1) {
await new Promise(resolve => setTimeout(resolve, 5000));
}

try {
res = await downloadFileInstanceWithFallbacks(file, instancePath);
} catch {
// Do nothing
}
} while (!res && counter < 3);
downloaded += 1;
if (
updatePercentage &&
(downloaded % 5 === 0 || downloaded === files.length)
) {
updatePercentage(downloaded);
}
},
{ concurrency: threads }
);
};

/**
* @param {{ path: string, hashes: { [algo: string]: string }, downloads: string[] }[]} file
* @param {string} instancePath
*/
const downloadFileInstanceWithFallbacks = async (file, instancePath) => {
const filePath = path.join(instancePath, file.path);
const dirPath = path.dirname(filePath);
try {
await fs.access(filePath);

let allChecksumsMatch = false;
for (const algo of Object.keys(file.hashes)) {
const checksum = await computeFileHash(filePath, algo);
if (file.hashes[algo] === checksum) {
allChecksumsMatch = true;
}
}
if (allChecksumsMatch) {
// the file already exists on disk, skip it
return true;
}
} catch {
await makeDir(dirPath);
}

// this loop exits as soon as a download has been successful
for (const url of file.downloads) {
const encodedUrl = getUri(url);
try {
const { data } = await axios.get(encodedUrl, {
responseType: 'stream',
responseEncoding: null,
adapter,
timeout: 60000 * 20
});

const wStream = fss.createWriteStream(filePath, {
encoding: null
});

data.pipe(wStream);

await new Promise((resolve, reject) => {
data.on('error', err => {
console.error(err);
reject(err);
});

data.on('end', () => {
wStream.end();
resolve();
});
});

return true;
} catch (e) {
console.error(
`Error while downloading <${url} | ${encodedUrl}> to <${file.path}> --> ${e.message}`
);
}
}
};

export const downloadFile = async (fileName, url, onProgress) => {
await makeDir(path.dirname(fileName));

Expand Down
79 changes: 48 additions & 31 deletions src/app/desktop/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { promises as fs } from 'fs';
import originalFs from 'original-fs';
import fse from 'fs-extra';
import { extractFull } from 'node-7z';
import * as Seven from 'node-7z';
import jimp from 'jimp/es';
import makeDir from 'make-dir';
import { promisify } from 'util';
Expand All @@ -17,10 +17,10 @@ import {
} from '../../../common/utils/constants';

import {
addQuotes,
removeDuplicates,
sortByForgeVersionDesc
} from '../../../common/utils';
// eslint-disable-next-line import/no-cycle
import { getAddon, getAddonFile, mcGetPlayerSkin } from '../../../common/api';
import { downloadFile } from './downloader';
import browserDownload from '../../../common/utils/browserDownload';
Expand Down Expand Up @@ -395,14 +395,46 @@ export const get7zPath = async () => {

get7zPath();

export const extract = async (source, destination, args = {}, funcs = {}) => {
const sevenZipPath = await get7zPath();
const extraction = Seven.extract(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 extractAll = async (
source,
destination,
args = {},
funcs = {}
) => {
const sevenZipPath = await get7zPath();
const extraction = extractFull(source, destination, {
const extraction = Seven.extractFull(source, destination, {
...args,
yes: true,
$bin: sevenZipPath,
Expand Down Expand Up @@ -484,14 +516,13 @@ export const getJVMArguments112 = (
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)}`)
.map(l => `"${l.path}"`)
.join(process.platform === 'win32' ? ';' : ':')
);

Expand All @@ -503,15 +534,8 @@ export const getJVMArguments112 = (
args.push(`-Xmx${memory}m`);
args.push(`-Xms${memory}m`);
args.push(...jvmOptions);
args.push(
`-Djava.library.path=${addQuotes(
needsQuote,
path.join(instancePath, 'natives')
)}`
);
args.push(
`-Dminecraft.applet.TargetDirectory=${addQuotes(needsQuote, instancePath)}`
);
args.push(`-Djava.library.path="${path.join(instancePath, 'natives')}"`);
args.push(`-Dminecraft.applet.TargetDirectory="${instancePath}"`);
if (mcJson.logging) {
args.push(mcJson?.logging?.client?.argument || '');
}
Expand All @@ -533,13 +557,13 @@ export const getJVMArguments112 = (
val = mcJson.id;
break;
case 'game_directory':
val = `${addQuotes(needsQuote, instancePath)}`;
val = `"${instancePath}"`;
break;
case 'assets_root':
val = `${addQuotes(needsQuote, assetsPath)}`;
val = `"${assetsPath}"`;
break;
case 'game_assets':
val = `${path.join(assetsPath, 'virtual', 'legacy')}`;
val = `"${path.join(assetsPath, 'virtual', 'legacy')}"`;
break;
case 'assets_index_name':
val = mcJson.assets;
Expand Down Expand Up @@ -568,9 +592,6 @@ export const getJVMArguments112 = (
if (val != null) {
mcArgs[i] = val;
}
if (typeof args[i] === 'string' && !needsQuote) {
args[i] = args[i].replaceAll('"', '');
}
}
}

Expand Down Expand Up @@ -598,7 +619,6 @@ export const getJVMArguments113 = (
) => {
const argDiscovery = /\${*(.*)}/;
let args = mcJson.arguments.jvm.filter(v => !skipLibrary(v));
const needsQuote = process.platform !== 'win32';

// if (process.platform === "darwin") {
// args.push("-Xdock:name=instancename");
Expand All @@ -607,9 +627,7 @@ export const getJVMArguments113 = (

args.push(`-Xmx${memory}m`);
args.push(`-Xms${memory}m`);
args.push(
`-Dminecraft.applet.TargetDirectory=${addQuotes(needsQuote, instancePath)}`
);
args.push(`-Dminecraft.applet.TargetDirectory="${instancePath}"`);
if (mcJson.logging) {
args.push(mcJson?.logging?.client?.argument || '');
}
Expand All @@ -627,9 +645,9 @@ export const getJVMArguments113 = (
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)}`;
args[i] = `"${args[i].value}"`;
} else if (typeof args[i].value === 'object') {
args.splice(i, 1, ...args[i].value.map(v => `${v}`));
args.splice(i, 1, ...args[i].value.map(v => `"${v}"`));
}
i -= 1;
} else if (typeof args[i] === 'string') {
Expand All @@ -644,10 +662,10 @@ export const getJVMArguments113 = (
val = mcJson.id;
break;
case 'game_directory':
val = `${addQuotes(needsQuote, instancePath)}`;
val = `"${instancePath}"`;
break;
case 'assets_root':
val = `${addQuotes(needsQuote, assetsPath)}`;
val = `"${assetsPath}"`;
break;
case 'assets_index_name':
val = mcJson.assets;
Expand All @@ -673,7 +691,7 @@ export const getJVMArguments113 = (
case 'natives_directory':
val = args[i].replace(
argDiscovery,
`${addQuotes(needsQuote, path.join(instancePath, 'natives'))}`
`"${path.join(instancePath, 'natives')}"`
);
break;
case 'launcher_name':
Expand All @@ -685,7 +703,7 @@ export const getJVMArguments113 = (
case 'classpath':
val = [...libraries, mcjar]
.filter(l => !l.natives)
.map(l => `${addQuotes(needsQuote, l.path)}`)
.map(l => `"${l.path}"`)
.join(process.platform === 'win32' ? ';' : ':');
break;
default:
Expand All @@ -695,7 +713,6 @@ export const getJVMArguments113 = (
args[i] = val;
}
}
if (!needsQuote) args[i] = args[i].replaceAll('"', '');
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/app/desktop/utils/withScroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ const withScroll = Component => {
justify-content: center;
`}
>
<div>
<div
css={`
width: 1000px;
`}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Component {...props} />
</div>
Expand Down