Extract bundle overrides before downloading mods

This commit is contained in:
Wyvest 2026-02-23 06:10:46 -05:00
parent e745c75cc8
commit fe0b1eed06
3 changed files with 68 additions and 80 deletions

View file

@ -303,11 +303,8 @@ export type VersionType =
*/
"old_beta"
const ARGS_MAP = { 'core':'{"removeUser":["uuid"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"changeCape":["access_token","cape_uuid"],"getScreenshots":["id"],"setClusterStage":["id","stage"],"getRunningProcesses":[],"convertUsernameUUID":["username_uuid"],"getClusterById":["id"],"getRunningProcessesByClusterId":["cluster_id"],"getLoadersForVersion":["mc_version"],"getPackageBody":["provider","body"],"open":["input"],"getMultiplePackages":["provider","slugs"],"isClusterRunning":["cluster_id"],"removeCape":["access_token"],"getGlobalProfile":[],"setDiscordRPCMessage":["message"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"fetchLoggedInProfile":["access_token"],"readSettings":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"killProcess":["pid"],"refreshAccounts":[],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getLogs":["id"],"updateClusterProfile":["name","profile"],"searchPackages":["provider","query"],"updateClusterById":["id","request"],"getWorlds":["id"],"getProfileOrDefault":["name"],"refreshAccount":["uuid"],"getUsers":[],"getLogByName":["id","name"],"removeCluster":["id"],"launchCluster":["id","uuid","search_for_java"],"openMsaLogin":[],"getPackage":["provider","slug"],"writeSettings":["setting"],"getClusters":[],"getUsersFromAuthor":["provider","author"],"getGameVersions":[],"getUser":["uuid"],"getLinkedPackages":["cluster_id"],"removePackage":["cluster_id","package_hash"],"createCluster":["options"],"getDefaultUser":["fallback"],"setDefaultUser":["uuid"],"installModpack":["modpack","cluster_id"],"fetchMinecraftProfile":["uuid"],"createSettingsProfile":["name"],"changeSkin":["access_token","skin_url","skin_variant"]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'debug':'{"getLocale":[],"getPlatform":[],"isInDev":[],"getGitCommitHash":[],"getOsVersion":[],"getFamily":[],"getBuildTimestamp":[],"getPackageVersion":[],"openDevTools":[],"getType":[],"getArch":[]}', 'oneclient':'{"getVersions":[],"installUpdate":[],"getClustersGroupedByMajor":[],"extractBundleOverrides":["bundle_path","cluster_id"],"checkForUpdate":[],"getBundlesFor":["cluster_id"]}' }
export type Router = { 'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
const ARGS_MAP = { 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'debug':'{"getLocale":[],"getPlatform":[],"isInDev":[],"getGitCommitHash":[],"getOsVersion":[],"getFamily":[],"getBuildTimestamp":[],"getPackageVersion":[],"openDevTools":[],"getType":[],"getArch":[]}', 'core':'{"removeUser":["uuid"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"changeCape":["access_token","cape_uuid"],"getScreenshots":["id"],"setClusterStage":["id","stage"],"getRunningProcesses":[],"convertUsernameUUID":["username_uuid"],"getClusterById":["id"],"getRunningProcessesByClusterId":["cluster_id"],"getLoadersForVersion":["mc_version"],"getPackageBody":["provider","body"],"open":["input"],"getMultiplePackages":["provider","slugs"],"isClusterRunning":["cluster_id"],"removeCape":["access_token"],"getGlobalProfile":[],"setDiscordRPCMessage":["message"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"fetchLoggedInProfile":["access_token"],"readSettings":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"killProcess":["pid"],"refreshAccounts":[],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getLogs":["id"],"updateClusterProfile":["name","profile"],"searchPackages":["provider","query"],"updateClusterById":["id","request"],"getWorlds":["id"],"getProfileOrDefault":["name"],"refreshAccount":["uuid"],"getUsers":[],"getLogByName":["id","name"],"removeCluster":["id"],"launchCluster":["id","uuid","search_for_java"],"openMsaLogin":[],"getPackage":["provider","slug"],"writeSettings":["setting"],"getClusters":[],"getUsersFromAuthor":["provider","author"],"getGameVersions":[],"getUser":["uuid"],"getLinkedPackages":["cluster_id"],"removePackage":["cluster_id","package_hash"],"createCluster":["options"],"getDefaultUser":["fallback"],"setDefaultUser":["uuid"],"installModpack":["modpack","cluster_id"],"fetchMinecraftProfile":["uuid"],"createSettingsProfile":["name"],"changeSkin":["access_token","skin_url","skin_variant"]}', 'oneclient':'{"getVersions":[],"installUpdate":[],"getClustersGroupedByMajor":[],"extractBundleOverrides":["bundle_path","cluster_id"],"checkForUpdate":[],"getBundlesFor":["cluster_id"]}' }
export type Router = { 'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> },
'core': { getClusters: () => Promise<ClusterModel[]>,
getClusterById: (id: number) => Promise<ClusterModel | null>,
@ -360,12 +357,6 @@ removeCape: (accessToken: string) => Promise<MojangFullPlayerProfile>,
convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
setDiscordRPCMessage: (message: string) => Promise<null>,
open: (input: string) => Promise<null> },
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
getVersions: () => Promise<OnlineClusterManifest>,
extractBundleOverrides: (bundlePath: string, clusterId: number) => Promise<null>,
checkForUpdate: () => Promise<Update | null>,
installUpdate: () => Promise<null> },
'debug': { openDevTools: () => Promise<void>,
isInDev: () => Promise<boolean>,
getArch: () => Promise<string>,
@ -376,7 +367,16 @@ getPlatform: () => Promise<string>,
getOsVersion: () => Promise<string>,
getGitCommitHash: () => Promise<string>,
getBuildTimestamp: () => Promise<string>,
getPackageVersion: () => Promise<string> } };
getPackageVersion: () => Promise<string> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
getVersions: () => Promise<OnlineClusterManifest>,
extractBundleOverrides: (bundlePath: string, clusterId: number) => Promise<null>,
checkForUpdate: () => Promise<Update | null>,
installUpdate: () => Promise<null> } };
export type { InferCommandOutput }

View file

@ -1,11 +1,11 @@
import type { ExternalPackage, ManagedVersionDependency, ModpackArchive, ModpackFile, Provider } from '@/bindings.gen';
import type { ExternalPackage, ManagedVersionDependency, ModpackFile, Provider } from '@/bindings.gen';
import { getModMetaDataName, Overlay } from '@/components';
import { useSettings } from '@/hooks/useSettings';
import { bindings } from '@/main';
import { useCommandMut } from '@onelauncher/common';
import { Button } from '@onelauncher/common/components';
import { useNavigate } from '@tanstack/react-router';
import { useEffect, useImperativeHandle, useRef, useState } from 'react';
import { useEffect, useImperativeHandle, useState } from 'react';
import { DialogTrigger } from 'react-aria-components';
export interface DownloadModsRef {
@ -36,7 +36,7 @@ export function isManagedMod(mod: ModData): mod is ManagedModData {
return mod.managed === true;
}
export function DownloadMods({ modsPerCluster, bundlesPerCluster, ref }: { modsPerCluster: Record<string, Array<ModpackFile>>; bundlesPerCluster?: Record<string, Array<ModpackArchive>>; ref: React.Ref<DownloadModsRef> }) {
export function DownloadMods({ modsPerCluster, ref }: { modsPerCluster: Record<string, Array<ModpackFile>>; ref: React.Ref<DownloadModsRef> }) {
const navigate = useNavigate();
const [isOpen, setOpen] = useState<boolean>(false);
const [mods, setMods] = useState<ModDataArray>([]);
@ -89,7 +89,7 @@ export function DownloadMods({ modsPerCluster, bundlesPerCluster, ref }: { modsP
<Button className="mb-4" isDisabled={mods.length === 0} onPress={() => setOpen(prev => !prev)}>Download Mods</Button>
<Overlay isDismissable={false} isOpen={isOpen}>
<DownloadingMods bundlesPerCluster={bundlesPerCluster} mods={mods} nextPath={nextPath} setOpen={setOpen} />
<DownloadingMods mods={mods} nextPath={nextPath} setOpen={setOpen} />
</Overlay>
</DialogTrigger>
);
@ -106,12 +106,10 @@ function downloadModsParallel(items: ModDataArray, limit: number, fn: (mod: ModD
return Promise.all(workers);
}
function DownloadingMods({ mods, setOpen, nextPath, bundlesPerCluster }: { mods: ModDataArray; setOpen: React.Dispatch<React.SetStateAction<boolean>>; nextPath: string; bundlesPerCluster?: Record<string, Array<ModpackArchive>> }) {
function DownloadingMods({ mods, setOpen, nextPath }: { mods: ModDataArray; setOpen: React.Dispatch<React.SetStateAction<boolean>>; nextPath: string }) {
const navigate = useNavigate();
const [downloadedMods, setDownloadedMods] = useState(0);
const [modName, setModName] = useState<string | null>(null);
const [allDone, setAllDone] = useState(false);
const hasStarted = useRef(false);
const download = useCommandMut(async (mod: ModData) => {
if (isManagedMod(mod)) {
if (mod.dependencies.length > 0)
@ -135,72 +133,37 @@ function DownloadingMods({ mods, setOpen, nextPath, bundlesPerCluster }: { mods:
let useParallelModDownloading = setting('parallel_mod_downloading');
useEffect(() => {
if (hasStarted.current)
return;
hasStarted.current = true;
const downloadAll = async () => {
try {
if (useParallelModDownloading)
await downloadModsParallel(mods, 10, async (mod) => {
setModName(mod.name);
try {
await download.mutateAsync(mod);
}
catch (e) {
console.warn(`Failed to download mod ${mod.name}:`, e);
}
finally {
setDownloadedMods(prev => prev + 1);
}
}); else
for (const mod of mods) {
setModName(mod.name);
try {
await download.mutateAsync(mod);
}
catch (e) {
console.warn(`Failed to download mod ${mod.name}:`, e);
}
finally {
setDownloadedMods(prev => prev + 1);
}
if (useParallelModDownloading)
await downloadModsParallel(mods, 10, async (mod) => {
setModName(mod.name);
try {
await download.mutateAsync(mod);
}
// Extract overrides from enabled bundles after all mods are downloaded
if (bundlesPerCluster) {
setModName('Extracting overrides...');
for (const [clusterId, bundles] of Object.entries(bundlesPerCluster)) {
for (const bundle of bundles) {
if (bundle.manifest.enabled) {
try {
await bindings.oneclient.extractBundleOverrides(bundle.path, Number(clusterId));
}
catch (e) {
console.error(`Failed to extract overrides for bundle ${bundle.manifest.name}:`, e);
}
}
}
finally {
setDownloadedMods(prev => prev + 1);
}
}); else
for (const mod of mods) {
setModName(mod.name);
try {
await download.mutateAsync(mod);
}
finally {
setDownloadedMods(prev => prev + 1);
}
}
}
catch (e) {
console.error('Error during mod download:', e);
}
finally {
setAllDone(true);
}
};
downloadAll();
}, [mods]);
useEffect(() => {
if (allDone) {
if (downloadedMods >= mods.length) {
setOpen(false);
navigate({ to: nextPath });
}
}, [allDone, navigate, nextPath, setOpen]);
}, [downloadedMods, mods, navigate, nextPath, setOpen]);
return (
<Overlay.Dialog isDismissable={false}>

View file

@ -9,7 +9,7 @@ import { Button } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { DotsVerticalIcon } from '@untitled-theme/icons-react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { useMemo, useRef, useState } from 'react';
import { useImperativeHandle, useMemo, useRef, useState } from 'react';
import { Button as AriaButton } from 'react-aria-components';
import { twMerge } from 'tailwind-merge';
@ -62,13 +62,38 @@ function RouteComponent() {
);
const bundlesPerCluster = useMemo(() => {
return clusters.reduce((acc, cluster, i) => {
acc[cluster.id] = bundleQueries[i];
return acc;
}, {} as Record<string, Array<ModpackArchive>>);
}, [clusters, bundleQueries]);
const result: Record<number, Array<ModpackArchive>> = {};
clusters.forEach((cluster, i) => {
const selected = selectedClusters.some(sc => sc.mc_version === cluster.mc_version && sc.mc_loader === cluster.mc_loader);
if (selected)
result[cluster.id] = bundleQueries[i];
});
return result;
}, [clusters, bundleQueries, selectedClusters]);
const downloadModsRef = useRef<DownloadModsRef>(null);
const wrappedRef = useRef<DownloadModsRef>(null);
useImperativeHandle(wrappedRef, () => ({
async openDownloadDialog(nextPath?: string) {
// Extract overrides from all enabled bundles before downloading mods
for (const [clusterId, bundles] of Object.entries(bundlesPerCluster)) {
for (const bundle of bundles) {
if (bundle.manifest.enabled) {
try {
await bindings.oneclient.extractBundleOverrides(bundle.path, Number(clusterId));
}
catch (e) {
console.error(`Failed to extract overrides for bundle ${bundle.manifest.name}:`, e);
}
}
}
}
// Then delegate to the actual mod download dialog
downloadModsRef.current?.openDownloadDialog(nextPath);
},
}), [bundlesPerCluster]);
return (
<>
@ -89,13 +114,13 @@ function RouteComponent() {
</div>
<div className="hidden">
<DownloadMods bundlesPerCluster={bundlesPerCluster} modsPerCluster={modsPerCluster} ref={downloadModsRef} />
<DownloadMods modsPerCluster={modsPerCluster} ref={downloadModsRef} />
</div>
</div>
</OverlayScrollbarsComponent>
</div>
</div>
<OnboardingNavigation disableNext={selectedClusters.length === 0} ref={downloadModsRef} />
<OnboardingNavigation disableNext={selectedClusters.length === 0} ref={wrappedRef} />
</>
);
}