added initial files

This commit is contained in:
2025-07-31 00:48:48 +02:00
parent 411cc26582
commit 2a3dc51ce9
193 changed files with 110293 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
// This class is used to create a queue of operations when working with instances configs, to avoid data corruption.
// Each instance will have its own queue. This can also be used in more generic situations where a queue structure is needed
class PromiseQueue {
constructor() {
this.queue = [];
this.isPending = false;
this.listeners = {};
}
add(promise) {
return new Promise((resolve, reject) => {
this.queue.push({
promise,
resolve,
reject
});
this.execute();
});
}
on(eventName, handler) {
switch (eventName) {
case 'executed':
this.listeners.executed = () => handler(this.queue.length + 1);
break;
case 'start':
this.listeners.start = () => handler(this.queue.length + 1);
break;
case 'end':
this.listeners.end = () => handler();
break;
default:
return null;
}
return null;
}
async execute() {
const startHandler = this.listeners.start;
if (this.isPending) return false;
if (startHandler) {
setTimeout(startHandler, 0);
}
while (this.queue[0]) {
const item = this.queue.shift();
this.isPending = true;
try {
// eslint-disable-next-line
const value = await item.promise();
const executedHandler = this.listeners.executed;
if (executedHandler) {
setTimeout(executedHandler, 0);
}
item.resolve(value);
} catch (e) {
item.reject(e);
}
this.isPending = false;
}
const endHandler = this.listeners.end;
if (endHandler) {
setTimeout(endHandler, 0);
}
return null;
}
}
export default PromiseQueue;
+13
View File
@@ -0,0 +1,13 @@
import fs from 'fs';
import crypto from 'crypto';
const computeFileHash = (filePath, algorithm = 'sha1') =>
new Promise((resolve, reject) => {
const hash = crypto.createHash(algorithm);
fs.createReadStream(filePath)
.on('data', data => hash.update(data))
.on('end', () => resolve(hash.digest('hex')))
.on('error', reject);
});
export default computeFileHash;
+29
View File
@@ -0,0 +1,29 @@
import path from 'path';
import { platform, homedir } from 'os';
export const WINDOWS = 'win32';
export const LINUX = 'linux';
export const DARWIN = 'darwin';
export const DESKTOP_PATH = path.join(homedir(), 'Desktop');
export const CLASSPATH_DIVIDER_CHAR = platform() === WINDOWS ? ';' : ':';
export const DEFAULT_JAVA_ARGS = `${
platform() === WINDOWS
? '-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump'
: ''
} -Xms256m`;
export const REQUIRED_JAVA_ARGS =
'-Dfml.ignorePatchDiscrepancies=true -Dfml.ignoreInvalidMinecraftCertificates=true';
export const DEFAULT_MEMORY = 4096;
export const resolutionPresets = [
'854x480',
'800x600',
'1024x768',
'1280x1024',
'1366x768',
'1600x900',
'1920x1080',
'2560x1440',
'3440x1440',
'3440x1500'
];
+161
View File
@@ -0,0 +1,161 @@
import makeDir from 'make-dir';
import fss from 'fs';
import axios from 'axios';
import pMap from 'p-map';
import path from 'path';
import adapter from 'axios/lib/adapters/http';
import computeFileHash from './computeFileHash';
const fs = fss.promises;
function getUri(url) {
return new URL(url).href;
}
export const downloadInstanceFiles = async (
arr,
updatePercentage,
threads = 4
) => {
let downloaded = 0;
await pMap(
arr,
async item => {
let counter = 0;
let res = false;
if (!item.path || !item.url) {
console.warn('Skipping', item);
return;
}
do {
counter += 1;
if (counter !== 1) {
await new Promise(resolve => setTimeout(resolve, 5000));
}
try {
res = await downloadFileInstance(
item.path,
item.url,
item.sha1,
item.legacyPath
);
} catch {
// Do nothing
}
} while (!res && counter < 3);
downloaded += 1;
if (
updatePercentage &&
(downloaded % 5 === 0 || downloaded === arr.length)
) {
updatePercentage(downloaded);
}
},
{ concurrency: threads }
);
};
const downloadFileInstance = async (fileName, url, sha1, legacyPath) => {
let encodedUrl;
try {
const filePath = path.dirname(fileName);
try {
await fs.access(fileName);
if (legacyPath) await fs.access(legacyPath);
const checksum = await computeFileHash(fileName);
const legacyChecksum = legacyPath && (await computeFileHash(legacyPath));
if (checksum === sha1 && (!legacyPath || legacyChecksum === sha1)) {
return true;
}
} catch {
await makeDir(filePath);
if (legacyPath) await makeDir(path.dirname(legacyPath));
}
encodedUrl = getUri(url);
const { data } = await axios.get(encodedUrl, {
responseType: 'stream',
responseEncoding: null,
adapter,
timeout: 60000 * 20
});
const wStream = fss.createWriteStream(fileName, {
encoding: null
});
data.pipe(wStream);
let wStreamLegacy;
if (legacyPath) {
wStreamLegacy = fss.createWriteStream(legacyPath, {
encoding: null
});
data.pipe(wStreamLegacy);
}
await new Promise((resolve, reject) => {
data.on('error', err => {
console.error(err);
reject(err);
});
data.on('end', () => {
wStream.end();
wStream.close();
if (legacyPath) {
wStreamLegacy.end();
wStreamLegacy.close();
}
resolve();
});
});
return true;
} catch (e) {
console.error(
`Error while downloading <${url} | ${encodedUrl}> to <${fileName}> --> ${e.message}`
);
return false;
}
};
export const downloadFile = async (fileName, url, onProgress) => {
await makeDir(path.dirname(fileName));
const encodedUrl = getUri(url);
const { data, headers } = await axios.get(encodedUrl, {
responseType: 'stream',
responseEncoding: null,
adapter,
timeout: 60000 * 20
});
const out = fss.createWriteStream(fileName, { encoding: null });
data.pipe(out);
// Save variable to know progress
let receivedBytes = 0;
const totalBytes = parseInt(headers['content-length'], 10);
data.on('data', chunk => {
// Update the received bytes
receivedBytes += chunk.length;
if (onProgress) {
onProgress(parseInt(((receivedBytes * 100) / totalBytes).toFixed(1), 10));
}
});
return new Promise((resolve, reject) => {
data.on('end', () => {
out.end();
out.close();
resolve();
});
data.on('error', () => {
reject();
});
});
};
+70
View File
@@ -0,0 +1,70 @@
const fmlLibsMapping = {};
// 1.3.*
const libs13 = [
['argo-2.25.jar', 'bb672829fde76cb163004752b86b0484bd0a7f4b', false],
['guava-12.0.1.jar', 'b8e78b9af7bf45900e14c6f958486b6ca682195f', false],
['asm-all-4.0.jar', '98308890597acb64047f7e896638e0d98753ae82', false]
];
fmlLibsMapping['1.3.2'] = libs13;
// 1.4.*
const libs14 = [
['argo-2.25.jar', 'bb672829fde76cb163004752b86b0484bd0a7f4b', false],
['guava-12.0.1.jar', 'b8e78b9af7bf45900e14c6f958486b6ca682195f', false],
['asm-all-4.0.jar', '98308890597acb64047f7e896638e0d98753ae82', false],
['bcprov-jdk15on-147.jar', 'b6f5d9926b0afbde9f4dbe3db88c5247be7794bb', false]
];
fmlLibsMapping['1.4'] = libs14;
fmlLibsMapping['1.4.1'] = libs14;
fmlLibsMapping['1.4.2'] = libs14;
fmlLibsMapping['1.4.3'] = libs14;
fmlLibsMapping['1.4.4'] = libs14;
fmlLibsMapping['1.4.5'] = libs14;
fmlLibsMapping['1.4.6'] = libs14;
fmlLibsMapping['1.4.7'] = libs14;
// 1.5
fmlLibsMapping['1.5'] = [
['argo-small-3.2.jar', '58912ea2858d168c50781f956fa5b59f0f7c6b51', false],
['guava-14.0-rc3.jar', '931ae21fa8014c3ce686aaa621eae565fefb1a6a', false],
['asm-all-4.1.jar', '054986e962b88d8660ae4566475658469595ef58', false],
['bcprov-jdk15on-148.jar', '960dea7c9181ba0b17e8bab0c06a43f0a5f04e65', true],
[
'deobfuscation_data_1.5.zip',
'5f7c142d53776f16304c0bbe10542014abad6af8',
false
],
['scala-library.jar', '458d046151ad179c85429ed7420ffb1eaf6ddf85', true]
];
// 1.5.1
fmlLibsMapping['1.5.1'] = [
['argo-small-3.2.jar', '58912ea2858d168c50781f956fa5b59f0f7c6b51', false],
['guava-14.0-rc3.jar', '931ae21fa8014c3ce686aaa621eae565fefb1a6a', false],
['asm-all-4.1.jar', '054986e962b88d8660ae4566475658469595ef58', false],
['bcprov-jdk15on-148.jar', '960dea7c9181ba0b17e8bab0c06a43f0a5f04e65', true],
[
'deobfuscation_data_1.5.1.zip',
'22e221a0d89516c1f721d6cab056a7e37471d0a6',
false
],
['scala-library.jar', '458d046151ad179c85429ed7420ffb1eaf6ddf85', true]
];
// 1.5.2
fmlLibsMapping['1.5.2'] = [
['argo-small-3.2.jar', '58912ea2858d168c50781f956fa5b59f0f7c6b51', false],
['guava-14.0-rc3.jar', '931ae21fa8014c3ce686aaa621eae565fefb1a6a', false],
['asm-all-4.1.jar', '054986e962b88d8660ae4566475658469595ef58', false],
['bcprov-jdk15on-148.jar', '960dea7c9181ba0b17e8bab0c06a43f0a5f04e65', true],
[
'deobfuscation_data_1.5.2.zip',
'446e55cd986582c70fcf12cb27bc00114c5adfd9',
false
],
['scala-library.jar', '458d046151ad179c85429ed7420ffb1eaf6ddf85', true]
];
export default fmlLibsMapping;
+124
View File
@@ -0,0 +1,124 @@
import path from 'path';
import fse from 'fs-extra';
import { promises as fs } from 'fs';
import pMap from 'p-map';
import { getDirectories } from '.';
import { CURSEFORGE } from '../../../common/utils/constants';
const getInstances = async instancesPath => {
const mapFolderToInstance = async instance => {
try {
const configPath = path.join(
path.join(instancesPath, instance, 'config.json')
);
const rawConfig = await fs.readFile(configPath);
// Remove temp config if present
try {
const tempConfigPath = path.join(
path.join(instancesPath, instance, 'config_new_temp.json')
);
await fs.unlink(tempConfigPath);
} catch {
// Nothing
}
// Restore config in case of crash
if (rawConfig.every(v => v === 0)) {
const backupConfigPath = path.join(
path.join(instancesPath, instance, 'config.bak.json')
);
const backupConfig = await fs.readFile(backupConfigPath);
JSON.parse(backupConfig);
await fs.rename(backupConfigPath, configPath);
}
const newRawConfig = await fs.readFile(configPath);
const config = JSON.parse(newRawConfig);
// if the launcher has the modloader as an array, convert it to object
if (Array.isArray(config.modloader)) {
// source is the source where the modpack comes from example: curseforge
// loaderType is the modloader example: forge
const [
loaderType,
mcVersion,
loaderVersion,
projectID,
fileID,
source
] = config.modloader;
const patchedConfig = {
...config,
loader: {
loaderType,
mcVersion,
...(loaderVersion && { loaderVersion }),
...(fileID && { fileID }),
...(projectID && { projectID }),
...(!source && fileID && projectID && { source: CURSEFORGE })
}
};
delete patchedConfig.modloader;
await fse.writeFile(configPath, JSON.stringify(patchedConfig));
return { ...patchedConfig, name: instance };
}
if (
config.loader?.fileId ||
config.loader?.addonId ||
config.loader?.addonID
) {
const { fileId, addonId, addonID } = config.loader;
const patchedConfig = {
...config,
loader: {
...config.loader,
...(fileId && { fileID: fileId }),
...(addonId && { projectID: addonId }),
...(addonID && { projectID: addonID })
}
};
delete patchedConfig.loader.fileId;
delete patchedConfig.loader.addonId;
await fse.writeFile(configPath, JSON.stringify(patchedConfig));
return { ...patchedConfig, name: instance };
}
return {
...config,
name: instance
};
} catch (err) {
console.error(err);
}
return null;
};
const folders = await getDirectories(instancesPath);
const instances = await pMap(
folders.filter(folder => folder !== '.DS_Store'),
mapFolderToInstance,
{
concurrency: 5
}
);
const hashMap = {};
// eslint-disable-next-line
for (const instance of instances) {
// eslint-disable-next-line
if (!instance) continue;
hashMap[instance.name] = instance;
}
return hashMap;
};
export default getInstances;
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
import { push } from 'connected-react-router';
const middleware = store => next => action => {
const currState = store.getState();
const result = next(action);
const nextState = store.getState();
const { dispatch } = store;
const currentAccountIdChanged =
currState.app.currentAccountId !== nextState.app.currentAccountId;
if (currentAccountIdChanged && !nextState.app.currentAccountId) {
dispatch(push('/'));
}
if (currState.settings.potatoPcMode !== nextState.settings.potatoPcMode) {
if (nextState.settings.potatoPcMode) {
document.getElementById('root').classList.add('disable-animations');
} else {
document.getElementById('root').classList.remove('disable-animations');
}
}
return result;
};
export default middleware;
@@ -0,0 +1,62 @@
// import watch from "node-watch";
import makeDir from 'make-dir';
import { ipcRenderer } from 'electron';
import { notification } from 'antd';
import * as ActionTypes from '../../../common/reducers/actionTypes';
import getInstances from './getInstances';
import modsFingerprintsScan from './modsFingerprintsScan';
import { startListener } from '../../../common/reducers/actions';
import { _getInstancesPath } from '../../../common/utils/selectors';
const middleware = store => next => action => {
const currState = store.getState();
const result = next(action);
const nextState = store.getState();
const { dispatch } = store;
if (!nextState.userData) return result;
const instancesPath = _getInstancesPath(nextState);
const userDataChanged = currState.userData !== nextState.userData;
// If not initialized yet, start listener and do a first-time read
if (!nextState.instances.started || userDataChanged) {
const startInstancesListener = async () => {
await ipcRenderer.invoke('stop-listener');
await makeDir(instancesPath);
const instances = await getInstances(instancesPath);
dispatch({
type: ActionTypes.UPDATE_INSTANCES,
instances
});
const instances1 = await modsFingerprintsScan(instancesPath);
dispatch({
type: ActionTypes.UPDATE_INSTANCES,
instances: instances1
});
try {
await makeDir(instancesPath);
await dispatch(startListener());
} catch (err) {
console.error(err);
// eslint-disable-next-line
notification.open({
key: 'nsfwNotWorking',
message: 'NSFW Error',
description: 'Node Sentinel File Watcher could not be initialized',
top: 47,
duration: 10
});
}
};
dispatch({
type: ActionTypes.UPDATE_INSTANCES_STARTED,
started: true
});
startInstancesListener();
}
return result;
};
export default middleware;
@@ -0,0 +1,163 @@
import path from 'path';
import { promises as fs } from 'fs';
import fse from 'fs-extra';
import pMap from 'p-map';
import { getDirectories, normalizeModData, isMod } from '.';
import { getFileMurmurHash2 } from '../../../common/utils';
import { getAddonsByFingerprint, getAddon } from '../../../common/api';
const modsFingerprintsScan = async instancesPath => {
const mapFolderToInstance = async instance => {
try {
const configPath = path.join(
path.join(instancesPath, instance, 'config.json')
);
const config = await fse.readJSON(configPath);
if (!config.loader) {
throw new Error(`Config for ${instance} could not be parsed`);
}
const modsFolder = path.join(instancesPath, instance, 'mods');
const modsFolderExists = await fse.pathExists(modsFolder);
if (!modsFolderExists) return { ...config, name: instance };
// Check if config.mods has a different number of mods than the actual number of mods
// Count the actual mods inside the folder
const files = await fs.readdir(modsFolder);
const fileNamesToRemove = [];
const missingMods = {};
/* eslint-disable */
// Check for new mods in local storage that are not present in config
for (const file of files) {
try {
const completeFilePath = path.join(modsFolder, file);
const stat = await fs.lstat(completeFilePath);
if (stat.isFile() && isMod(completeFilePath, instancesPath)) {
// Check if file is in config
if (!(config?.mods || []).find(mod => mod.fileName === file)) {
const murmurHash = await getFileMurmurHash2(completeFilePath);
console.log(
'[MODS SCANNER] Local mod not found in config',
file,
murmurHash
);
missingMods[file] = murmurHash;
}
}
} catch {}
}
// Check for old mods in config that are not present on local storage
for (const configMod of config?.mods || []) {
if (!files.includes(configMod.fileName)) {
fileNamesToRemove.push(configMod.fileName);
console.log(
`[MODS SCANNER] Removing ${configMod.fileName} from config`
);
}
}
/* eslint-enable */
let newMods = config?.mods || [];
if (Object.values(missingMods).length !== 0) {
let success = false;
let tries = 10;
while (!success || tries > 10) {
try {
const data = await getAddonsByFingerprint(
Object.values(missingMods)
);
const matches = await Promise.all(
Object.entries(missingMods).map(async ([fileName, hash]) => {
const exactMatch = (data.exactMatches || []).find(
v => v.file.fileFingerprint === hash
);
if (exactMatch?.file) {
let addonData = null;
try {
addonData = await getAddon(exactMatch.file.modId);
return {
...normalizeModData(
exactMatch.file,
exactMatch.file.modId,
addonData.name
),
fileName
};
} catch {
return {
fileName,
displayName: fileName,
packageFingerprint: hash
};
}
}
return {
fileName,
displayName: fileName,
packageFingerprint: hash
};
})
);
newMods = [...newMods, ...matches];
success = true;
} catch (err) {
console.error(err);
tries += 1;
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 5000);
});
}
}
}
const filterMods = newMods
.filter(_ => _)
.filter(v => !fileNamesToRemove.includes(v.fileName));
const newConfig = {
...config,
mods: filterMods
};
if (JSON.stringify(config) !== JSON.stringify(newConfig)) {
await fse.outputJson(configPath, newConfig);
return { ...newConfig, name: instance };
}
return { ...config, name: instance };
} catch (err) {
console.error(err);
}
return null;
};
const folders = await getDirectories(instancesPath);
const instances = await pMap(
folders.filter(folder => folder !== '.DS_Store'),
mapFolderToInstance,
{
concurrency: 5
}
);
const hashMap = {};
// eslint-disable-next-line
for (const instance of instances) {
// eslint-disable-next-line
if (!instance) continue;
hashMap[instance.name] = instance;
}
return hashMap;
};
export default modsFingerprintsScan;
+25
View File
@@ -0,0 +1,25 @@
import { lazy } from 'react';
import AsyncComponent from '../../../common/components/AsyncComponent';
import withScroll from './withScroll';
const Login = lazy(() => import('../views/Login'));
const Home = lazy(() => import('../views/Home'));
const Onboarding = lazy(() => import('../views/Onboarding'));
const routes = [
{
path: '/',
exact: true,
component: AsyncComponent(Login)
},
{
path: '/home',
component: withScroll(AsyncComponent(Home))
},
{
path: '/onboarding',
component: AsyncComponent(Onboarding)
}
];
export default routes;
+24
View File
@@ -0,0 +1,24 @@
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import analytics from '../../../common/utils/analytics';
const INTERVAL_DURATION = 5 * 60 * 1000;
const useTrackIdle = pathname => {
const clientToken = useSelector(state => state.app.clientToken);
useEffect(() => {
let interval;
if (clientToken && process.env.NODE_ENV !== 'development') {
interval = setInterval(() => {
analytics.idle(pathname);
}, INTERVAL_DURATION);
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [pathname, clientToken]);
};
export default useTrackIdle;
+34
View File
@@ -0,0 +1,34 @@
import React from 'react';
const withScroll = Component => {
return props => {
return (
<div
css={`
flex-grow: 1;
overflow-y: auto;
padding: 10px 0;
`}
>
<div
css={`
flex-grow: 1;
display: flex;
justify-content: center;
`}
>
<div
css={`
width: 1000px;
`}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Component {...props} />
</div>
</div>
</div>
);
};
};
export default withScroll;