Files
Starlight_Launcher/apps/app-frontend/src/helpers/instance.ts
T
Disy bc904065c3
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
feat: complete hosted mod sync and launcher interface updates
Add pack sync markers, tagged mod updates, parallel progress, JWT downloads and retry recovery. Include pending onboarding, about scene, compatibility data pack and download fixes.
2026-09-15 19:06:56 +08:00

1117 lines
31 KiB
TypeScript

/**
* All theseus API calls return serialized values (both return values and errors);
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import type { Labrinth } from '@modrinth/api-client'
import {
clearPinnedContentViewPreferences,
type ContentItem,
type ContentOwner,
} from '@modrinth/ui'
import { invoke } from '@tauri-apps/api/core'
import { isOfflineMode } from '@/composables/useNetworkStatus'
import { buildGcCandidateChain, resolveAutoGcStrategy } from '@/helpers/gc/auto-selector'
import { collectGcContext } from '@/helpers/gc/context'
import { detectGcStrategy, GC_STRATEGY_DEFINITIONS } from '@/helpers/gc/strategies'
import type { GcContext, ResolvedGcStrategyId } from '@/helpers/gc/types'
import { setLastGcLaunchReport } from '@/helpers/gc-notice'
import { getInstanceMode, prepareHostedSession } from '@/helpers/hosted-packs'
import { AUTO_GC_PRESET_ARG } from '@/helpers/java-arguments'
import { get_jre, get_memory_status } from '@/helpers/jre.js'
import { get as getSettings } from '@/helpers/settings'
import type { InstallJobSnapshot } from './install'
import { removeInstanceCache } from './instance-cache'
import type {
CacheBehaviour,
ContentFile,
ContentFileProjectType,
GameInstance,
InstanceLoader,
} from './types'
export interface InstancePostUpgradeWarning {
code: import('./instance-upgrade').InstanceUpgradeIssueCode
contentId: string | null
relativePath: string | null
}
export interface InstancePostUpgradeNotice {
instanceId: string
upgradeJobId: string
targetGameVersion: string
consecutiveCleanLaunches: number
warnings: InstancePostUpgradeWarning[]
}
export async function get_post_upgrade_notice(
instanceId: string,
): Promise<InstancePostUpgradeNotice | null> {
return await invoke('plugin:instance|instance_get_post_upgrade_notice', { instanceId })
}
export async function dismiss_post_upgrade_notice(instanceId: string): Promise<void> {
await invoke('plugin:instance|instance_dismiss_post_upgrade_notice', { instanceId })
}
export async function remove(instanceId: string): Promise<void> {
await invoke('plugin:instance|instance_remove', { instanceId })
removeInstanceCache(instanceId)
clearPinnedContentViewPreferences(instanceId)
}
export interface CreateDirectLinkInstanceRequest {
name?: string | null
launcherType: 'HMCL' | 'PCL2' | 'PCL2CE' | 'Generic'
basePath: string
instanceFolder: string
instancePath?: string | null
}
/** Associate an existing `.minecraft/versions/<id>` directory in place. */
export async function create_direct_link(request: CreateDirectLinkInstanceRequest) {
return await invoke<GameInstance>('plugin:instance|instance_create_direct_link', { request })
}
export interface DirectLinkSyncReport {
imported: number
updated: number
removed: number
missing: number
errors: string[]
}
export type ExternalMinecraftRootMode = 'isolated' | 'shared'
export interface ExternalMinecraftRoot {
path: string
mode: ExternalMinecraftRootMode
}
export async function sync_direct_links(
roots: ExternalMinecraftRoot[],
): Promise<DirectLinkSyncReport> {
return await invoke<DirectLinkSyncReport>('plugin:instance|instance_sync_direct_links', {
roots,
})
}
export async function get(instanceId: string): Promise<GameInstance | null> {
return await invoke('plugin:instance|instance_get', { instanceId })
}
export async function get_many(instanceIds: string[]): Promise<GameInstance[]> {
return await invoke('plugin:instance|instance_get_many', { instanceIds })
}
export async function get_projects(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<Record<string, ContentFile>> {
return await invoke('plugin:instance|instance_get_projects', { instanceId, cacheBehaviour })
}
export async function get_installed_project_ids(instanceId: string): Promise<string[]> {
return await invoke('plugin:instance|instance_get_installed_project_ids', { instanceId })
}
export type CoreComponentKind = 'jar_mod' | 'replacement_jar' | 'agent'
export type CoreComponentSource = {
provider: string
projectId: string | null
versionId: string | null
fileId: string | null
pageUrl: string | null
}
export type CoreComponent = {
id: string
kind: CoreComponentKind
fileName: string
relativePath: string
enabled: boolean
removed: boolean
order: number
sha1: string | null
sha256: string | null
source: CoreComponentSource | null
targetGameVersion: string
createdAt: string
modifiedAt: string
failureReason: string | null
}
export type CoreJarPreview = {
outputPath: string
componentCount: number
replacementComponentId: string | null
entries: number
sha1: string
sha256: string
}
export type McArchiveCoreInstallResult =
| { state: 'installed'; component: CoreComponent }
| {
state: 'manual_download'
fileName: string
pageUrl: string | null
expectedSha256: string | null
}
export async function list_core_components(instanceId: string): Promise<CoreComponent[]> {
return await invoke('plugin:instance|instance_list_core_components', { instanceId })
}
export async function add_core_jar_mod(
instanceId: string,
sourcePath: string,
targetGameVersion: string,
source?: CoreComponentSource,
): Promise<CoreComponent> {
return await invoke('plugin:instance|instance_add_core_jar_mod', {
instanceId,
sourcePath,
targetGameVersion,
source,
})
}
export async function replace_core_jar(
instanceId: string,
sourcePath: string,
targetGameVersion: string,
source?: CoreComponentSource,
): Promise<CoreComponent> {
return await invoke('plugin:instance|instance_replace_core_jar', {
instanceId,
sourcePath,
targetGameVersion,
source,
})
}
export async function move_core_component(
instanceId: string,
componentId: string,
direction: -1 | 1,
): Promise<CoreComponent[]> {
return await invoke('plugin:instance|instance_move_core_component', {
instanceId,
componentId,
direction,
})
}
export async function set_core_component_enabled(
instanceId: string,
componentId: string,
enabled: boolean,
): Promise<CoreComponent> {
return await invoke('plugin:instance|instance_set_core_component_enabled', {
instanceId,
componentId,
enabled,
})
}
export async function remove_core_component(
instanceId: string,
componentId: string,
): Promise<void> {
await invoke('plugin:instance|instance_remove_core_component', { instanceId, componentId })
}
export async function restore_core_component(
instanceId: string,
componentId: string,
): Promise<CoreComponent> {
return await invoke('plugin:instance|instance_restore_core_component', {
instanceId,
componentId,
})
}
export async function preview_core_jar(instanceId: string): Promise<CoreJarPreview | null> {
return await invoke('plugin:instance|instance_preview_core_jar', { instanceId })
}
export async function install_mcarchive_modloader(
instanceId: string,
gameVersion: string,
): Promise<McArchiveCoreInstallResult> {
return await invoke('plugin:instance|instance_install_mcarchive_modloader', {
instanceId,
gameVersion,
})
}
export async function import_mcarchive_modloader(
instanceId: string,
gameVersion: string,
sourcePath: string,
): Promise<McArchiveCoreInstallResult> {
return await invoke('plugin:instance|instance_import_mcarchive_modloader', {
instanceId,
gameVersion,
sourcePath,
})
}
export type McArchiveContentInstallRequest = {
projectId: string
projectSlug: string
versionId: string
fileId: string
projectType: ContentFileProjectType
}
export type McArchiveContentInstallResult =
| { state: 'installed'; relativePath: string }
| {
state: 'manual_download'
fileName: string
pageUrl: string | null
expectedSha256: string | null
}
export type PlanetMinecraftContentInstallRequest = {
projectId: string
versionId: string
projectType: ContentFileProjectType
}
export type PlanetMinecraftContentInstallResult =
| { state: 'installed'; relativePath: string }
| { state: 'manual_download'; pageUrl: string; fileName: string | null }
export async function install_mcarchive_content(
instanceId: string,
request: McArchiveContentInstallRequest,
): Promise<McArchiveContentInstallResult> {
return await invoke('plugin:instance|instance_install_mcarchive_content', {
instanceId,
request,
})
}
export async function import_mcarchive_content(
instanceId: string,
request: McArchiveContentInstallRequest,
sourcePath: string,
): Promise<McArchiveContentInstallResult> {
return await invoke('plugin:instance|instance_import_mcarchive_content', {
instanceId,
request,
sourcePath,
})
}
export async function install_planet_minecraft_content(
instanceId: string,
request: PlanetMinecraftContentInstallRequest,
): Promise<PlanetMinecraftContentInstallResult> {
return await invoke('plugin:instance|instance_install_planet_minecraft_content', {
instanceId,
request,
})
}
export async function import_planet_minecraft_content(
instanceId: string,
request: PlanetMinecraftContentInstallRequest,
sourcePath: string,
): Promise<PlanetMinecraftContentInstallResult> {
return await invoke('plugin:instance|instance_import_planet_minecraft_content', {
instanceId,
request,
sourcePath,
})
}
export type InstanceInstallTarget = {
game_version: string
loader: string
}
export type InstanceInstallCandidate = {
id: string
name: string
icon_path?: string | null
game_version: string
loader: InstanceLoader
installed: boolean
compatible: boolean
}
export async function get_install_candidates(
projectId: string,
projectType: string,
targets: InstanceInstallTarget[],
): Promise<InstanceInstallCandidate[]> {
return await invoke('plugin:instance|instance_get_install_candidates', {
projectId,
projectType,
targets,
})
}
// Get content items with rich metadata for an instance
// Returns content items filtered to exclude modpack files (if linked),
// sorted alphabetically by project name
export async function get_content_items(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
const items = await invoke<ContentItem[]>('plugin:instance|instance_get_content_items', {
instanceId,
cacheBehaviour,
})
return items
}
export async function get_content_items_by_paths(
instanceId: string,
paths: string[],
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
return await invoke<ContentItem[]>('plugin:instance|instance_get_content_items_by_paths', {
instanceId,
paths,
cacheBehaviour,
})
}
export type ContentOwnershipKind = 'pack_managed' | 'user_added' | 'local_discovered'
export type PackMemberMaterializationState = 'present' | 'pending_manual' | 'missing' | 'removed'
export type PackMemberOverrideKind = 'none' | 'disabled' | 'removed' | 'version'
export interface InstanceContentSnapshotItem {
fileId: string | null
entryId: string | null
memberId: string | null
ownershipKind: ContentOwnershipKind
materializationState: PackMemberMaterializationState
overrideKind: PackMemberOverrideKind
expectedRelativePath: string
required: boolean
projectType: string
provider: 'modrinth' | 'curseforge' | 'mcarchive' | null
providerProjectId: string | null
providerReleaseId: string | null
content: ContentItem | null
capabilities: {
canToggle: boolean
canDelete: boolean
canUpdate: boolean
canChangeVersion: boolean
canRestorePackDefault: boolean
}
dependency: {
autoDependency: boolean
requiredBy: Array<{
provider: 'modrinth' | 'curseforge' | 'mcarchive' | 'local'
projectId: string
releaseId: string
}>
requires: Array<{
provider: 'modrinth' | 'curseforge' | 'mcarchive' | 'local'
projectId: string
releaseId: string
}>
orphaned: boolean
} | null
}
export interface PendingManualDownload {
id: string
instanceId: string
packMemberId: string | null
contentEntryId: string | null
operationKind: 'pack_install' | 'pack_update' | 'content_install' | 'content_update'
operationTargetId: string | null
projectType: string
provider: 'modrinth' | 'curseforge' | 'mcarchive'
providerProjectId: string
providerReleaseId: string
fileName: string
websiteUrl: string | null
targetRelativePath: string
expectedSha1: string | null
expectedSize: number | null
expectedFingerprint: number | null
state: 'waiting' | 'matched' | 'imported' | 'error' | 'cancelled'
context: Record<string, unknown>
createdAt: string
modifiedAt: string
}
export interface InstanceContentSnapshot {
instanceId: string
revision: number
pack: {
name: string
iconPath: string | null
provider: 'modrinth' | 'curseforge' | 'mcarchive' | null
projectId: string | null
versionId: string | null
reconciled: boolean
canUpdate: boolean
metadata: LinkedModpackInfo | null
} | null
items: InstanceContentSnapshotItem[]
pendingManualDownloads: PendingManualDownload[]
warnings: Array<{
code: string
message: string
provider: 'modrinth' | 'curseforge' | 'mcarchive' | null
}>
}
export async function get_content_snapshot(instanceId: string): Promise<InstanceContentSnapshot> {
return await invoke('plugin:instance|instance_get_content_snapshot', { instanceId })
}
export async function refresh_content(instanceId: string): Promise<InstanceContentSnapshot> {
return await invoke('plugin:instance|instance_refresh_content', { instanceId })
}
export type ContentUpdateScope = 'user_added' | 'pack' | 'item'
export interface ContentUpdatePlan {
id: string
instanceId: string
revision: number
scope: ContentUpdateScope
actions: Array<{
contentId: string
relativePath: string | null
ownershipKind: ContentOwnershipKind
provider: 'modrinth' | 'curseforge' | 'mcarchive'
currentReleaseId: string | null
targetReleaseId: string
}>
warnings: string[]
}
export interface ContentUpdateResolution {
contentId: string
choice: 'keep_override' | 'restore_pack_default'
}
export async function plan_content_updates(
instanceId: string,
scope: ContentUpdateScope,
target?: string,
): Promise<ContentUpdatePlan> {
return await invoke('plugin:instance|instance_plan_content_updates', {
instanceId,
scope,
target,
})
}
export async function apply_content_update_plan(
planId: string,
resolutions: ContentUpdateResolution[] = [],
): Promise<InstanceContentSnapshot> {
return await invoke('plugin:instance|instance_apply_content_update_plan', {
planId,
resolutions,
})
}
// Linked modpack info returned from backend
export interface LinkedModpackInfo {
project: Labrinth.Projects.v2.Project
version: Labrinth.Versions.v2.Version
owner: ContentOwner | null
update:
| {
provider: 'modrinth'
project_id: string
current_version_id: string
target_version_id: string
}
| {
provider: 'curseforge'
project_id: number
current_file_id: number
target_file_id: number
}
| null
update_version: Labrinth.Versions.v2.Version | null
}
// Get linked modpack info for an instance
// Returns project, version, and owner information for the linked modpack,
// or null if the instance is not linked to a modpack
export async function get_linked_modpack_info(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<LinkedModpackInfo | null> {
return await invoke('plugin:instance|instance_get_linked_modpack_info', {
instanceId,
cacheBehaviour,
})
}
// Get content items that are part of the linked modpack
// Returns the modpack's dependencies as ContentItem list
// Returns empty array if the instance is not linked to a modpack
export async function get_linked_modpack_content(
instanceId: string,
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
const items = await invoke<ContentItem[]>('plugin:instance|instance_get_linked_modpack_content', {
instanceId,
cacheBehaviour,
})
return items
}
// Convert a list of dependencies into ContentItems with rich metadata
export async function get_dependencies_as_content_items(
dependencies: Labrinth.Versions.v3.Dependency[],
cacheBehaviour?: CacheBehaviour,
): Promise<ContentItem[]> {
const items = await invoke<ContentItem[]>(
'plugin:instance|instance_get_dependencies_as_content_items',
{
dependencies,
cacheBehaviour,
},
)
return items
}
export async function get_full_path(instanceId: string): Promise<string> {
return await invoke('plugin:instance|instance_get_full_path', { instanceId })
}
export async function get_mod_full_path(instanceId: string, projectPath: string): Promise<string> {
return await invoke('plugin:instance|instance_get_mod_full_path', { instanceId, projectPath })
}
export interface JavaVersion {
parsed_version: number
version: string
architecture: string
path: string
}
export async function get_optimal_jre_key(instanceId: string): Promise<JavaVersion | null> {
return await invoke('plugin:instance|instance_get_optimal_jre_key', { instanceId })
}
export async function list(): Promise<GameInstance[]> {
return await invoke('plugin:instance|instance_list')
}
export type DailyPlaytime = {
date: string
played_seconds: number
session_count: number
top_instance_name?: string | null
}
export async function set_pinned(instanceId: string, pinned: boolean): Promise<GameInstance> {
return await invoke('plugin:instance|instance_set_pinned', { instanceId, pinned })
}
export async function get_daily_playtime(
startDate: string,
endDate: string,
): Promise<DailyPlaytime[]> {
return await invoke('plugin:instance|instance_get_daily_playtime', { startDate, endDate })
}
export type DailyPlaytimeEntry = {
instance_id: string
instance_name: string
played_seconds: number
session_count: number
}
export async function get_daily_playtime_details(date: string): Promise<DailyPlaytimeEntry[]> {
return await invoke('plugin:instance|instance_get_daily_playtime_details', { date })
}
export async function check_installed(instanceId: string, projectId: string): Promise<boolean> {
return await invoke('plugin:instance|instance_check_installed', { instanceId, projectId })
}
export async function update_all(instanceId: string): Promise<Record<string, string>> {
return await invoke('plugin:instance|instance_update_all', { instanceId })
}
// Updates a specified project
export async function update_project(instanceId: string, projectPath: string): Promise<string> {
return await invoke('plugin:instance|instance_update_project', { instanceId, projectPath })
}
export interface ResolutionPreferences {
game_versions?: string[]
loaders?: string[]
}
export interface ResolveContentRequest {
project_id: string
version_id?: string | null
content_type: Labrinth.Content.v3.ContentType
selected?: ResolutionPreferences
excluded_project_ids?: string[]
force_project_ids?: string[]
}
export interface ResolvedContent {
project_id: string
version_id: string
dependent_on_version_id?: string | null
required?: boolean
}
export interface ResolveContentPlan {
primary: ResolvedContent
dependencies: ResolvedContent[]
skipped: Array<{
project_id: string
version_id?: string | null
dependent_on_version_id?: string | null
reason: string
}>
}
export async function add_project_from_version(
instanceId: string,
versionId: string,
): Promise<string> {
return await invoke('plugin:instance|instance_add_project_from_version', {
instanceId,
versionId,
})
}
export async function install_project_with_dependencies(
instanceId: string,
request: ResolveContentRequest,
): Promise<ResolveContentPlan> {
return await invoke('plugin:instance|instance_install_project_with_dependencies', {
instanceId,
request,
})
}
export async function preview_project_with_dependencies(
instanceId: string,
request: ResolveContentRequest,
): Promise<ResolveContentPlan> {
return await invoke('plugin:instance|instance_preview_project_with_dependencies', {
instanceId,
request,
})
}
export async function preview_project_with_dependencies_for_target(
request: ResolveContentRequest,
gameVersion: string,
loader: string,
): Promise<ResolveContentPlan> {
return await invoke('plugin:instance|instance_preview_project_with_dependencies_for_target', {
request,
gameVersion,
loader,
})
}
export async function queue_project_with_dependencies(
instanceId: string,
request: ResolveContentRequest,
display: { title: string; iconUrl?: string | null },
): Promise<InstallJobSnapshot> {
return await invoke('plugin:instance|instance_queue_project_with_dependencies', {
instanceId,
request,
displayTitle: display.title,
displayIcon: display.iconUrl ?? null,
})
}
export async function switch_project_version_with_dependencies(
instanceId: string,
projectPath: string,
versionId: string,
): Promise<string> {
return await invoke('plugin:instance|instance_switch_project_version_with_dependencies', {
instanceId,
projectPath,
versionId,
})
}
// Import a world save into an instance
// Returns the imported world name
export async function import_world_save(
instanceId: string,
sourcePath: string,
innerBase?: string,
): Promise<string> {
return await invoke('plugin:instance|instance_import_world_save', {
instanceId,
sourcePath,
innerBase,
})
}
export async function install_datapack_to_world(
instanceId: string,
worldPath: string,
sourcePath: string,
innerBase?: string,
): Promise<string> {
return await invoke('plugin:instance|instance_install_datapack_to_world', {
instanceId,
worldPath,
sourcePath,
innerBase,
})
}
// Add a project to an instance from a path + project_type
// Returns a path to the new project file
export async function add_project_from_path(
instanceId: string,
projectPath: string,
projectType?: ContentFileProjectType,
innerBase?: string,
): Promise<string> {
return await invoke('plugin:instance|instance_add_project_from_path', {
instanceId,
projectPath,
projectType,
innerBase,
})
}
// Toggle disabling a project
export async function toggle_disable_project(
instanceId: string,
projectPath: string,
desiredEnabled?: boolean,
): Promise<string> {
return await invoke('plugin:instance|instance_toggle_disable_project', {
instanceId,
projectPath,
desiredEnabled,
})
}
export async function toggle_content_entry(
instanceId: string,
contentId: string,
desiredEnabled?: boolean,
): Promise<string> {
return await invoke('plugin:instance|instance_toggle_content_entry', {
instanceId,
contentId,
desiredEnabled,
})
}
export type ContentToggleResult = {
contentId: string
path: string
enabled: boolean
}
export async function toggle_content_entries(
instanceId: string,
contentIds: string[],
desiredEnabled?: boolean,
): Promise<ContentToggleResult[]> {
return await invoke('plugin:instance|instance_toggle_content_entries', {
instanceId,
contentIds,
desiredEnabled,
})
}
// Roll back an updated project to its previous file (kept as a .old backup)
export async function rollback_project(instanceId: string, projectPath: string): Promise<string> {
return await invoke('plugin:instance|instance_rollback_project', {
instanceId,
projectPath,
})
}
// Remove a project
export async function remove_project(instanceId: string, projectPath: string): Promise<void> {
return await invoke('plugin:instance|instance_remove_project', { instanceId, projectPath })
}
export async function remove_content_entry(instanceId: string, contentId: string): Promise<void> {
return await invoke('plugin:instance|instance_remove_content_entry', { instanceId, contentId })
}
export async function update_content_entry(instanceId: string, contentId: string): Promise<string> {
return await invoke('plugin:instance|instance_update_content_entry', { instanceId, contentId })
}
export async function switch_content_entry_version(
instanceId: string,
contentId: string,
versionId: string,
): Promise<string> {
return await invoke('plugin:instance|instance_switch_content_entry_version', {
instanceId,
contentId,
versionId,
})
}
export async function restore_pack_member_default(
instanceId: string,
memberId: string,
): Promise<string | null> {
return await invoke('plugin:instance|instance_restore_pack_member_default', {
instanceId,
memberId,
})
}
// Update a managed Modrinth instance to a specific version
export async function update_managed_modrinth_version(
instanceId: string,
versionId: string,
): Promise<InstallJobSnapshot> {
return await invoke('plugin:instance|instance_update_managed_modrinth_version', {
instanceId,
versionId,
})
}
// Repair a managed Modrinth instance
export async function update_repair_modrinth(instanceId: string): Promise<InstallJobSnapshot> {
return await invoke('plugin:instance|instance_repair_managed_modrinth', { instanceId })
}
// Export an instance to .mrpack
// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs')
// Version id is optional (ie: 1.1.5)
export async function export_instance_mrpack(
instanceId: string,
exportLocation: string,
includedOverrides: string[],
versionId?: string,
description?: string,
name?: string,
): Promise<void> {
return await invoke('plugin:instance|instance_export_mrpack', {
instanceId,
exportLocation,
includedOverrides,
versionId,
description,
name,
})
}
// Given a folder path, populate an array of all the subfolders
// Intended to be used for finding potential override folders
// profile
// -- mods
// -- resourcepacks
// -- file1
// => [mods, resourcepacks]
// allows selection for 'included_overrides' in export_instance_mrpack
export async function get_pack_export_candidates(instanceId: string): Promise<string[]> {
return await invoke('plugin:instance|instance_get_pack_export_candidates', { instanceId })
}
export interface GcLaunchIntent {
active_preset_id: string
block_tokens: string[]
candidate_ids: string[]
candidates: string[][]
}
export interface GcLaunchReport {
preferred_strategy: string
chosen_strategy: string
chosen_args: string[]
pruned_args: string[]
reason_chain: string[]
}
export interface InstanceRunResult {
process: unknown
gc_notice: GcLaunchReport | null
}
export function gcReportFellBack(report: GcLaunchReport): boolean {
return report.chosen_strategy !== report.preferred_strategy || report.pruned_args.length > 0
}
/**
* The Java that will actually run the instance: a per-instance `java_path`
* wins, otherwise the optimal JRE for the game version.
*/
async function resolveEffectiveJava(
instance: Pick<GameInstance, 'java_path'>,
instanceId: string,
): Promise<JavaVersion | null> {
if (instance.java_path) {
try {
return await get_jre(instance.java_path)
} catch {
// Invalid override; fall back to the optimal JRE.
}
}
return await get_optimal_jre_key(instanceId)
}
function normalizeJvmToken(token: string): string {
const stripped = token.replace(/^-+/, '')
const withoutPrefix = stripped.startsWith('XX:') ? stripped.slice(3) : stripped
const withoutModifier =
withoutPrefix.startsWith('+') || withoutPrefix.startsWith('-')
? withoutPrefix.slice(1)
: withoutPrefix
return withoutModifier.split('=')[0] ?? withoutModifier
}
function strategyTokenKeys(strategy: ResolvedGcStrategyId, context: GcContext): Set<string> {
const tokens = GC_STRATEGY_DEFINITIONS[strategy].buildArgs(context).split(/\s+/).filter(Boolean)
return new Set(tokens.map(normalizeJvmToken))
}
/**
* Determine whether the effective Java args contain an auto GC marker or an
* active GC preset block, and if so build the ordered candidate chain for the
* backend to verify against the actual JVM. The args are returned untouched so
* the backend replaces exactly the preset block.
*/
export async function resolveGcLaunchIntent(
instanceId: string,
): Promise<{ args: string[]; gcIntent: GcLaunchIntent | null }> {
const instance = await get(instanceId)
if (!instance) return { args: [], gcIntent: null }
const settings = await getSettings()
const effectiveArgs = instance.extra_launch_args ?? settings.extra_launch_args
const java = await resolveEffectiveJava(instance, instanceId)
const javaMajorVersion = java?.parsed_version ?? null
let modCount = 0
try {
const snapshot = await get_content_snapshot(instanceId)
modCount = snapshot.items.filter(
(item) => item.projectType === 'mod' && item.materializationState === 'present',
).length
} catch {
modCount = 0
}
const memory = instance.memory ?? settings.memory
let allocatedMemoryMb = memory.maximum
if (memory.automatic) {
try {
const status = await get_memory_status(instanceId, memory.maximum, true)
allocatedMemoryMb = status.allocated_mb
} catch {
// Fall back to the configured value when automatic memory info is unavailable.
}
}
const context = await collectGcContext(
allocatedMemoryMb,
instance.loader,
javaMajorVersion,
modCount,
)
let activePresetId: string | null = null
let blockTokens: string[] = []
if (effectiveArgs.includes(AUTO_GC_PRESET_ARG)) {
activePresetId = 'gc-auto'
blockTokens = [AUTO_GC_PRESET_ARG]
} else {
const detected = detectGcStrategy(effectiveArgs.join(' '))
if (detected) {
activePresetId = `gc-${detected}`
const presetKeys = strategyTokenKeys(detected, context)
blockTokens = effectiveArgs.filter((arg) => presetKeys.has(normalizeJvmToken(arg)))
}
}
if (!activePresetId || !blockTokens.length) {
return { args: effectiveArgs, gcIntent: null }
}
const preferred: ResolvedGcStrategyId =
activePresetId === 'gc-auto'
? resolveAutoGcStrategy(context).resolvedStrategy
: (activePresetId.slice('gc-'.length) as ResolvedGcStrategyId)
const { ids, args: candidates } = buildGcCandidateChain(context, preferred)
return {
args: effectiveArgs,
gcIntent: {
active_preset_id: activePresetId,
block_tokens: blockTokens,
candidate_ids: ids,
candidates,
},
}
}
// Run Minecraft using an instance
// Returns the process metadata plus an optional GC fallback report.
export async function run(
instanceId: string,
serverAddress: string | null = null,
): Promise<InstanceRunResult> {
if (await getInstanceMode(instanceId) === 'starlight') await prepareHostedSession()
const { args, gcIntent } = await resolveGcLaunchIntent(instanceId)
const result = await invoke<InstanceRunResult>('plugin:instance|instance_run', {
instanceId,
serverAddress,
offlineMode: isOfflineMode(),
extraLaunchArgs: args,
gcIntent,
})
setLastGcLaunchReport(result.gc_notice)
return result
}
export async function kill(instanceId: string): Promise<void> {
return await invoke('plugin:instance|instance_kill', { instanceId })
}
// Edits an instance
export async function edit(instanceId: string, editInstance: Partial<GameInstance>): Promise<void> {
return await invoke('plugin:instance|instance_edit', { instanceId, editInstance })
}
export async function cache_icon(iconName: string, bytes: number[]): Promise<string> {
return await invoke('plugin:instance|instance_cache_icon', { iconName, bytes })
}
// Edits an instance's icon
export async function edit_icon(instanceId: string, iconPath: string | null): Promise<void> {
return await invoke('plugin:instance|instance_edit_icon', { instanceId, iconPath })
}
export type SymlinkCapability = 'supported' | 'requires_admin' | 'unsupported'
export async function check_symlink_capability(): Promise<SymlinkCapability> {
return await invoke('check_symlink_capability')
}
export async function allow_symlink_target(path: string): Promise<void> {
return await invoke('allow_symlink_target', { path })
}