feat: instances v2 (#6431)

* feat: base of instances v2

* feat: use old profiles with compat layer

* prototype: instances v2

* fix: install_from using profile

* fix: skins migration fix

* fix: frontend still using profile path

* fix: add update proj multiselect guard

* fix: cargo fmt

* fix: content missing fields

* feat: break up app-lib/api/instance.rs

* fix: check_content_updates mismatch

* fix: updater modal cleanup w/new structure

* feat: better update all handling

* fix: remove preview_update_all

* fix: feedback on bulk update + lint

* fix: rem transitions

* fix: change to jsonb

* feat: app db backup after update

* fix: lint

* fix: sqlx prepare + use sqlx macros

* fix: lint

* fix: bugs

* feat: defuck the installing process up

* fix: bug of hell

* fix: shear

* fix: fmt

* fix: install progress spacing + change mc/content/overrides to bytes

* fix: lint

* fix: prepr

* fix: navtabs anim not working in app

* fix: worlds.vue improvements + browse page fixes

* feat: optimise queries + adapter fns

* fix: lint

* fix: lint

* feat: shared modrinth-content-management crate (#6469)

* feat: disable warnings setting

* feat: add instances shortcuts (#6329)

* Add modrinth://launch deep link to start a profile

Support external profile launching via modrinth://launch/{profile_path} for integrations such as Stream Deck.

* Change route to /launch/profile/{id} for future extensibility

* fix: ensure profile path is url decoded

* fix: URL-decode profile path from deep link

* fix: use urlencoding crate for URL decoding

* feat: implement app instance shortcuts

* feat: change windows shortcut creation to use windows api instead

* feat: implement creating a shortcut launching world/server

* format

* fmt

* fix multiline inline tables

* pnpm prepr

* feat: move create shortcut to last item

* refactor: split up shortcuts.rs for individual platforms

* refactor: turn profile launch url into url type

* use string literal and add safety comment

* pt2

* refactor: rename anything that's profile into instance

* update mac shortcut

---------

Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>

---------

Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-06-25 21:19:29 +00:00
committed by GitHub
co-authored by DJCheesusReal Truman Gao
parent ef4044534f
commit 734720e11e
353 changed files with 24745 additions and 9771 deletions
@@ -6,12 +6,18 @@ import type { Router } from 'vue-router'
import { trackEvent } from '@/helpers/analytics'
import { get_project, get_project_v3, get_version } from '@/helpers/cache.js'
import { install_to_existing_profile } from '@/helpers/pack.js'
import { create, edit, edit_icon, get, install as installProfile, list } from '@/helpers/profile.js'
import {
install_create_instance,
install_create_modpack_instance,
install_existing_instance,
installJobInstanceId,
wait_for_install_job,
} from '@/helpers/install'
import { edit, get, list } from '@/helpers/instance'
import type { GameInstance } from '@/helpers/types'
import { ensureManagedServerWorldExists, getServerAddress } from '@/helpers/worlds'
import { start_join_server } from '@/helpers/worlds.ts'
import { handleSevereError } from '@/store/error.js'
import { ensureManagedServerWorldExists, getServerAddress } from '@/store/install.js'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface ModalRef<TShow extends (...args: any[]) => void = () => void> {
@@ -99,14 +105,14 @@ export function createServerInstall(opts: {
return installingServerProjects.value.includes(projectId)
}
async function joinServer(profilePath: string, serverAddress: string | null) {
async function joinServer(instanceId: string, serverAddress: string | null) {
if (!serverAddress) return
await start_join_server(profilePath, serverAddress)
await start_join_server(instanceId, serverAddress)
}
async function findInstalledInstance(projectId: string) {
const packs = await list()
return packs.find((pack) => pack.linked_data?.project_id === projectId) ?? null
return packs.find((pack) => pack.link?.project_id === projectId) ?? null
}
async function createVanillaInstance(
@@ -114,30 +120,32 @@ export function createServerInstall(opts: {
gameVersion: string,
serverAddress: string | null,
) {
const profilePath = await create(
project.title,
const job = await install_create_instance({
name: project.title,
gameVersion,
'vanilla',
null,
project.icon_url ?? null,
false,
{
loader: 'vanilla',
loaderVersion: null,
iconPath: project.icon_url ?? null,
link: {
type: 'server_project',
project_id: project.id,
version_id: '',
locked: true,
},
)
})
const instanceId = installJobInstanceId(job)
if (!instanceId) return null
await ensureManagedServerWorldExists(profilePath, project.title, serverAddress)
await wait_for_install_job(job.job_id)
await ensureManagedServerWorldExists(instanceId, project.title, serverAddress)
return profilePath
return instanceId
}
async function updateVanillaGameVersion(instance: GameInstance, targetGameVersion: string) {
if (instance.game_version === targetGameVersion) return
await edit(instance.path, { game_version: targetGameVersion })
await installProfile(instance.path, false)
await edit(instance.id, { game_version: targetGameVersion })
const job = await install_existing_instance(instance.id, false)
await wait_for_install_job(job.job_id)
}
function showModpackInstallSuccess(project: GameInstance, serverAddress: string | null) {
@@ -152,14 +160,14 @@ export function createServerInstall(opts: {
label: 'Launch game',
action: async () => {
try {
await joinServer(project.path, serverAddress)
await joinServer(project.id, serverAddress)
trackEvent('InstanceStart', {
loader: project.loader,
game_version: project.game_version,
source: 'ServerProject',
})
} catch (err) {
handleSevereError(err, { profilePath: project.path })
handleSevereError(err, { instanceId: project.id })
}
},
color: 'brand' as const,
@@ -168,7 +176,7 @@ export function createServerInstall(opts: {
: []),
{
label: 'Instance',
action: () => opts.router.push(`/instance/${encodeURIComponent(project.path)}`),
action: () => opts.router.push(`/instance/${encodeURIComponent(project.id)}`),
},
],
autoCloseMs: null,
@@ -187,14 +195,14 @@ export function createServerInstall(opts: {
label: 'Launch game',
action: async () => {
try {
if (serverAddress) await start_join_server(instance.path, serverAddress)
if (serverAddress) await start_join_server(instance.id, serverAddress)
trackEvent('InstanceStart', {
loader: instance.loader,
game_version: instance.game_version,
source: 'ServerProject',
})
} catch (err) {
handleSevereError(err, { profilePath: instance.path })
handleSevereError(err, { instanceId: instance.id })
}
},
color: 'brand' as const,
@@ -203,7 +211,7 @@ export function createServerInstall(opts: {
: []),
{
label: 'Instance',
action: () => opts.router.push(`/instance/${encodeURIComponent(instance.path)}`),
action: () => opts.router.push(`/instance/${encodeURIComponent(instance.id)}`),
},
],
autoCloseMs: null,
@@ -211,8 +219,8 @@ export function createServerInstall(opts: {
}
/**
* Server projects that use modpack content have linked_data.project_id as
* the server project id and linked_data.version_id as the modpack content version id.
* Server projects that use modpack content have link.project_id as
* the server project id and link.version_id as the modpack content version id.
* The modpack content version can be of the same server project, or from a different project.
*/
async function installServerProject(serverProjectId: string) {
@@ -229,46 +237,32 @@ export function createServerInstall(opts: {
const contentVersionId = content.version_id
const contentVersion = await get_version(contentVersionId, 'bypass')
const contentProjectId = contentVersion.project_id
const gameVersion = contentVersion.game_versions?.[0] ?? ''
const profilePath = await create(
project.title,
gameVersion,
'vanilla',
null,
project.icon_url,
true,
const createJob = await install_create_modpack_instance(
{
project_id: serverProjectId,
type: 'fromVersionId',
project_id: contentProjectId,
version_id: contentVersionId,
locked: true,
title: project.title,
},
{
name: project.title,
iconPath: project.icon_url ?? null,
link: {
type: 'server_project_modpack',
server_project_id: serverProjectId,
content_project_id: contentProjectId,
content_version_id: contentVersionId,
project_id: serverProjectId,
version_id: contentVersionId,
},
},
)
const instanceId = installJobInstanceId(createJob)
if (!instanceId) return
// Save the icon path before pack install overwrites it
const profileBeforeInstall = await get(profilePath)
const originalIconPath = profileBeforeInstall?.icon_path ?? null
await install_to_existing_profile(
contentProjectId,
contentVersionId,
project.title,
profilePath,
)
// Pack install overwrites name, icon, and linked_data with the content project's values.
// Restore them to point to the server project.
await edit(profilePath, {
name: project.title,
linked_data: {
project_id: serverProjectId,
version_id: contentVersionId,
locked: true,
},
})
await edit_icon(profilePath, originalIconPath)
await ensureManagedServerWorldExists(profilePath, project.title, serverAddress)
await wait_for_install_job(createJob.job_id)
await ensureManagedServerWorldExists(instanceId, project.title, serverAddress)
}
/**
@@ -303,9 +297,13 @@ export function createServerInstall(opts: {
if (installingServerProjects.value.includes(projectId)) return
startInstallingServer(projectId)
try {
const path = await createVanillaInstance(project, recommendedGameVersion, serverAddress)
if (path) {
instance = await get(path)
const instanceId = await createVanillaInstance(
project,
recommendedGameVersion,
serverAddress,
)
if (instanceId) {
instance = await get(instanceId)
if (instance) showModpackInstallSuccess(instance, serverAddress)
}
} finally {
@@ -324,10 +322,10 @@ export function createServerInstall(opts: {
if (!instance) return
await ensureManagedServerWorldExists(instance.path, project.title, serverAddress)
await ensureManagedServerWorldExists(instance.id, project.title, serverAddress)
// Update existing instance if needed
if (isModpack && instance.linked_data?.version_id !== modpackVersionId) {
if (isModpack && instance.link?.version_id !== modpackVersionId) {
updateToPlayModalRef?.show(instance, modpackVersionId, () => {
showUpdateSuccess(instance, serverAddress)
})
@@ -347,14 +345,14 @@ export function createServerInstall(opts: {
// Join server
try {
await joinServer(instance.path, serverAddress)
await joinServer(instance.id, serverAddress)
trackEvent('InstanceStart', {
loader: instance.loader,
game_version: instance.game_version,
source: 'ServerProject',
})
} catch (err) {
handleSevereError(err, { profilePath: instance.path })
handleSevereError(err, { instanceId: instance.id })
}
}