Files
Starlight_Lancher/apps/app-frontend/src/helpers/curseforge.ts
T

511 lines
13 KiB
TypeScript

import { invoke } from '@tauri-apps/api/core'
import type { InstallJobSnapshot } from './install'
export type ContentProvider = 'modrinth' | 'curseforge'
export interface CurseForgeCapability {
status: 'missing_key' | 'ready' | 'unauthorized'
configured: boolean
}
export interface CurseForgeSearchRequest {
classId: number
categoryId?: number
categoryIds?: number[]
searchFilter?: string
gameVersion?: string
modLoaderType?: number
sortField?: number
sortOrder?: 'asc' | 'desc'
index?: number
pageSize?: number
}
export interface UnifiedSearchHit {
provider: 'curseforge'
project_id: string
slug?: string
author: string
author_url?: string
title: string
description: string
project_type: string
categories: string[]
versions: string[]
downloads: number
icon_url?: string
date_created: string
date_modified: string
latest_version?: string
gallery: string[]
website_url?: string
source_url?: string
allow_mod_distribution?: boolean
}
export interface UnifiedSearchResponse {
provider: 'curseforge'
hits: UnifiedSearchHit[]
offset: number
limit: number
total_hits: number
}
export interface CurseForgeFilesRequest {
gameVersion?: string
modLoaderType?: number
gameVersionTypeId?: number
index?: number
pageSize?: number
}
export interface CurseForgeProject {
id: number
name: string
slug: string
summary: string
downloadCount: number
mainFileId: number
classId?: number
dateCreated: string
dateModified: string
dateReleased: string
allowModDistribution?: boolean
gamePopularityRank?: number
logo?: { thumbnailUrl: string; url: string }
authors: Array<{ id: number; name: string; url: string }>
categories: Array<{ id: number; name: string; slug: string; iconUrl?: string }>
screenshots: Array<{ id: number; title: string; url: string; thumbnailUrl: string }>
latestFilesIndexes: Array<{
gameVersion: string
fileId: number
filename: string
releaseType: number
gameVersionTypeId?: number
modLoader?: number
}>
links: {
websiteUrl?: string
wikiUrl?: string
issuesUrl?: string
sourceUrl?: string
}
}
export interface CurseForgeCategory {
id: number
gameId: number
name: string
slug: string
url: string
iconUrl?: string
dateModified: string
isClass?: boolean | null
classId?: number
parentCategoryId?: number
displayIndex?: number
}
export interface CurseForgeFile {
id: number
modId: number
isAvailable: boolean
displayName: string
fileName: string
releaseType: number
fileDate: string
fileLength: number
hashes: Array<{ value: string; algo: number }>
fileFingerprint: number
downloadCount: number
downloadUrl?: string
gameVersions: string[]
dependencies: Array<{ modId: number; relationType: number }>
}
export function hasCompatibleCurseForgeFile(files: CurseForgeFile[], gameVersion: string) {
return files.some((file) => file.isAvailable && file.gameVersions.includes(gameVersion))
}
export function getCurseForgeImageUrl(source?: string | null, width = 256): string | undefined {
if (!source) return undefined
try {
const url = new URL(source)
if (url.protocol !== 'https:' || !url.hostname.endsWith('forgecdn.net')) return source
const proxy = new URL('https://images.weserv.nl/')
proxy.searchParams.set('url', source)
proxy.searchParams.set('w', String(width))
proxy.searchParams.set('fit', 'contain')
proxy.searchParams.set('output', 'webp')
return proxy.toString()
} catch {
return source
}
}
export interface CurseForgeFilesResponse {
files: CurseForgeFile[]
pagination: {
index: number
pageSize: number
resultCount: number
totalCount: number
}
}
export interface CurseForgeInstallRequest {
instanceId: string
projectId: number
fileId: number
projectType: string
ownershipKind?: 'pack_managed' | 'user_added'
manualOperationKind?: 'pack_install' | 'pack_update' | 'content_install' | 'content_update'
gameVersion?: string
modLoaderType?: number
worldName?: string
installDependencies?: boolean
excludedDependencyProjectIds?: number[]
forceDependencyProjectIds?: number[]
dependencyPlanId?: string
}
export interface CurseForgeWorldInstallRequest {
instanceId: string
projectId: number
fileId: number
}
export interface CurseForgeInstallResult {
installed: Array<{
projectId: number
fileId: number
relativePath: string
dependency: boolean
}>
manualDownloads: Array<{
projectId: number
fileId: number
fileName: string
ownershipKind: 'pack_managed' | 'user_added'
operationKind: 'pack_install' | 'pack_update' | 'content_install' | 'content_update'
websiteUrl?: string
projectType: string
projectSlug: string
targetFolder: string
hashes: Array<{ value: string; algo: number }>
fileLength: number
fileFingerprint: number
}>
failedDownloads: Array<{
projectId: number
fileId: number
fileName: string
reason: string
}>
optionalDependencies: number[]
incompatibleDependencies: number[]
skippedDependencies?: Array<{
projectId: number
fileId: number | null
reason: string
}>
}
export interface CurseForgeInstallPreview {
planId: string
primary: {
projectId: number
fileId: number
title: string
versionNumber: string
fileName: string
size: number
requiredByProjectIds: number[]
iconUrl?: string | null
}
dependencies: Array<{
projectId: number
fileId: number
title: string
versionNumber: string
fileName: string
size: number
requiredByProjectIds: number[]
iconUrl?: string | null
versionMismatch?: boolean
selectionReason?: 'native_strict_match' | 'sha1_verified_modrinth_fallback'
required?: boolean
}>
modrinthFallbacks?: Array<{
projectId: string
versionId: string
title: string
versionNumber: string
parentProjectId: number
iconUrl?: string | null
required?: boolean
}>
skipped: Array<{
projectId: number
fileId: number | null
reason: string
}>
optionalDependencies: number[]
incompatibleDependencies: number[]
}
export interface CurseForgeManualDownloadImport {
projectId: number
fileId: number
relativePath: string
}
export type CurseForgeManualDownloadImportErrorKind =
| 'not_pending'
| 'verification_failed'
| 'other'
export interface CurseForgeManualDownloadScanResult {
downloadDirectory?: string | null
imported: CurseForgeManualDownloadImport[]
errors: Array<{
projectId: number
fileId: number
message: string
}>
}
export interface CurseForgeModpackInstallResult {
content: CurseForgeInstallResult
overridesWritten: number
minecraftVersion: string
loader?: string
}
export function summarizeCurseForgeInstall(result: CurseForgeInstallResult) {
const installed = result.installed?.length ?? 0
const manual = result.manualDownloads?.length ?? 0
const failed = result.failedDownloads?.length ?? 0
const optional = result.optionalDependencies?.length ?? 0
const incompatible = result.incompatibleDependencies?.length ?? 0
return { installed, manual, failed, optional, incompatible }
}
function getErrorMessage(error: unknown): string | null {
if (error instanceof Error) return error.message
if (typeof error === 'string') return error
if (typeof error !== 'object' || error === null || !('message' in error)) return null
return String(error.message)
}
export function classifyCurseForgeManualDownloadImportError(
error: unknown,
): CurseForgeManualDownloadImportErrorKind {
const message = getErrorMessage(error)
if (message?.includes('The selected CurseForge file is not pending for this instance')) {
return 'not_pending'
}
if (message?.includes('The selected file does not match the required CurseForge file')) {
return 'verification_failed'
}
return 'other'
}
export function getCurseForgeDownloadFailureDetails(error: unknown): string | null {
const message = getErrorMessage(error)
if (!message) return null
const normalized = message.toLowerCase()
const isDownloadFailure = normalized.includes('download failed after')
const isCurseForge =
normalized.includes('curseforge') ||
normalized.includes('forgecdn.net') ||
normalized.includes('forgecdn')
return isDownloadFailure && isCurseForge ? message : null
}
export function getCurseForgeCapability() {
return invoke<CurseForgeCapability>('plugin:curseforge|curseforge_capability')
}
export function validateCurseForgeCredentials() {
return invoke<CurseForgeCapability>('plugin:curseforge|curseforge_validate_credentials')
}
export function searchCurseForgeProjects(request: CurseForgeSearchRequest, requestId?: string) {
return invoke<UnifiedSearchResponse>('plugin:curseforge|curseforge_search_projects', {
request,
requestId,
})
}
export function getCurseForgeProject(projectId: number) {
return invoke<CurseForgeProject>('plugin:curseforge|curseforge_get_project', { projectId })
}
export function getCurseForgeProjects(projectIds: number[], cacheBehaviour?: CacheBehaviour) {
return invoke<CurseForgeProject[]>('plugin:curseforge|curseforge_get_projects', {
projectIds,
cacheBehaviour,
})
}
export function getCurseForgeChangelog(projectId: number, fileId: number) {
return invoke<string>('plugin:curseforge|curseforge_get_changelog', {
projectId,
fileId,
})
}
export function getCurseForgeDescription(projectId: number) {
return invoke<string>('plugin:curseforge|curseforge_get_description', {
projectId,
})
}
export function getCurseForgeFiles(projectId: number, request: CurseForgeFilesRequest) {
return invoke<CurseForgeFilesResponse>('plugin:curseforge|curseforge_get_files', {
projectId,
request,
})
}
export function getCurseForgeFile(projectId: number, fileId: number) {
return invoke<CurseForgeFile>('plugin:curseforge|curseforge_get_file', {
projectId,
fileId,
})
}
export function getCurseForgeDownloadUrl(projectId: number, fileId: number) {
return invoke<string | null>('plugin:curseforge|curseforge_get_download_url', {
projectId,
fileId,
})
}
export function getCurseForgeCategories(classId?: number) {
return invoke<CurseForgeCategory[]>('plugin:curseforge|curseforge_get_categories', { classId })
}
export function installCurseForgeFile(request: CurseForgeInstallRequest) {
return invoke<CurseForgeInstallResult>('plugin:curseforge|curseforge_install_file', { request })
}
export function previewCurseForgeFile(request: CurseForgeInstallRequest) {
return invoke<CurseForgeInstallPreview>('plugin:curseforge|curseforge_preview_install_file', {
request,
})
}
export function queueCurseForgeFile(
request: CurseForgeInstallRequest,
display: { title: string; iconUrl?: string | null },
) {
return invoke<InstallJobSnapshot>('plugin:instance|instance_queue_curseforge_content', {
request,
displayTitle: display.title,
displayIcon: display.iconUrl ?? null,
})
}
export function queueCurseForgeWorld(
request: CurseForgeWorldInstallRequest,
display: { title: string; iconUrl?: string | null },
) {
return invoke<InstallJobSnapshot>('plugin:instance|instance_queue_curseforge_world', {
request,
displayTitle: display.title,
displayIcon: display.iconUrl ?? null,
})
}
export function updateCurseForgeFile(instanceId: string, relativePath: string) {
return invoke<CurseForgeInstallResult>('plugin:curseforge|curseforge_update_installed_file', {
instanceId,
relativePath,
})
}
export function switchCurseForgeFileVersion(
instanceId: string,
relativePath: string,
fileId: number,
) {
return invoke<CurseForgeInstallResult>(
'plugin:curseforge|curseforge_switch_installed_file_version',
{
instanceId,
relativePath,
fileId,
},
)
}
export function recognizeCurseForgeFiles(instanceId: string) {
return invoke<{
scanned: number
matched: number
linked: CurseForgeInstallResult['installed']
unmatchedPaths: string[]
}>('plugin:curseforge|curseforge_recognize_instance_files', { instanceId })
}
export function importCurseForgeManualDownloads(instanceId: string, scanDirectory?: string | null) {
return invoke<CurseForgeManualDownloadScanResult>(
'plugin:curseforge|curseforge_import_manual_downloads',
{ instanceId, scanDirectory: scanDirectory ?? null },
)
}
export function listPendingCurseForgeManualDownloads(instanceId: string) {
return invoke<CurseForgeInstallResult['manualDownloads']>(
'plugin:curseforge|curseforge_list_pending_manual_downloads',
{ instanceId },
)
}
export function importPendingCurseForgeManualDownloadFile(
instanceId: string,
projectId: number,
fileId: number,
sourcePath: string,
) {
return invoke<CurseForgeManualDownloadImport>(
'plugin:curseforge|curseforge_import_pending_manual_download_file',
{ instanceId, projectId, fileId, sourcePath },
)
}
export function configureCurseForgeManualDownloadWatcher(
enabled: boolean,
scanDirectory?: string | null,
) {
return invoke<string | null>('plugin:curseforge|curseforge_configure_manual_download_watcher', {
enabled,
scanDirectory: scanDirectory ?? null,
})
}
export function installCurseForgeModpack(request: {
instanceId: string
projectId: number
fileId: number
installOptional?: boolean
}) {
return invoke<CurseForgeModpackInstallResult>('plugin:curseforge|curseforge_install_modpack', {
request,
})
}
export function updateManagedCurseForgeModpack(instanceId: string, fileId: number) {
return invoke<InstallJobSnapshot>('plugin:curseforge|curseforge_update_managed_modpack', {
instanceId,
fileId,
})
}