import type { Labrinth } from '@modrinth/api-client' import type { AbstractPopupNotificationManager } from '@modrinth/ui' import { createContext } from '@modrinth/ui' import { type Ref, ref } from 'vue' 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_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' // eslint-disable-next-line @typescript-eslint/no-explicit-any interface ModalRef void = () => void> { show: TShow hide: () => void } export interface ServerInstallContext { installingServerProjects: Ref startInstallingServer: (projectId: string) => void stopInstallingServer: (projectId: string) => void isServerInstalling: (projectId: string) => boolean installServerProject: (serverProjectId: string) => Promise playServerProject: (projectId: string) => Promise setInstallToPlayModal: ( ref: ModalRef< ( project: Labrinth.Projects.v3.Project, modpackVersionId: string | null, callback?: () => void, ) => void >, ) => void setUpdateToPlayModal: ( ref: ModalRef< (instance: GameInstance, activeVersionId: string | null, callback?: () => void) => void >, ) => void setAddServerToInstanceModal: ( ref: ModalRef<(serverName: string, serverAddress: string) => void>, ) => void showAddServerToInstanceModal: (serverName: string, serverAddress: string) => void } let _serverInstallSingleton: ServerInstallContext | null = null const [_rawInjectServerInstall, provideServerInstall] = createContext( 'root', 'serverInstall', ) export { provideServerInstall } export function injectServerInstall(): ServerInstallContext { try { return _rawInjectServerInstall() } catch { if (_serverInstallSingleton) return _serverInstallSingleton throw new Error('ServerInstall context not available') } } export function createServerInstall(opts: { router: Router handleError: (err: unknown) => void popupNotificationManager: AbstractPopupNotificationManager }): ServerInstallContext { const installingServerProjects = ref([]) let installToPlayModalRef: ModalRef< ( project: Labrinth.Projects.v3.Project, modpackVersionId: string | null, callback?: () => void, ) => void > | null = null let updateToPlayModalRef: ModalRef< (instance: GameInstance, activeVersionId: string | null, callback?: () => void) => void > | null = null let addServerToInstanceModalRef: ModalRef< (serverName: string, serverAddress: string) => void > | null = null function startInstallingServer(projectId: string) { if (!installingServerProjects.value.includes(projectId)) { installingServerProjects.value.push(projectId) } } function stopInstallingServer(projectId: string) { installingServerProjects.value = installingServerProjects.value.filter((id) => id !== projectId) } function isServerInstalling(projectId: string) { return installingServerProjects.value.includes(projectId) } async function joinServer(instanceId: string, serverAddress: string | null) { if (!serverAddress) return await start_join_server(instanceId, serverAddress) } async function findInstalledInstance(projectId: string) { const packs = await list() return packs.find((pack) => pack.link?.project_id === projectId) ?? null } async function createVanillaInstance( project: Labrinth.Projects.v2.Project, gameVersion: string, serverAddress: string | null, ) { const job = await install_create_instance({ name: project.title, gameVersion, loader: 'vanilla', loaderVersion: null, iconPath: project.icon_url ?? null, link: { type: 'server_project', project_id: project.id, }, }) const instanceId = installJobInstanceId(job) if (!instanceId) return null await wait_for_install_job(job.job_id) await ensureManagedServerWorldExists(instanceId, project.title, serverAddress) return instanceId } async function updateVanillaGameVersion(instance: GameInstance, targetGameVersion: string) { if (instance.game_version === targetGameVersion) return 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) { opts.popupNotificationManager.addPopupNotification({ title: 'Install complete', text: `${project.name} is installed and ready to play.`, type: 'success', buttons: [ ...(serverAddress ? [ { label: 'Launch game', action: async () => { try { await joinServer(project.id, serverAddress) trackEvent('InstanceStart', { loader: project.loader, game_version: project.game_version, source: 'ServerProject', }) } catch (err) { handleSevereError(err, { instanceId: project.id }) } }, color: 'brand' as const, }, ] : []), { label: 'Instance', action: () => opts.router.push(`/instance/${encodeURIComponent(project.id)}`), }, ], autoCloseMs: null, }) } function showUpdateSuccess(instance: GameInstance, serverAddress: string | null) { opts.popupNotificationManager.addPopupNotification({ title: 'Update complete', text: `${instance.name} has been updated and is ready to play.`, type: 'success', buttons: [ ...(serverAddress ? [ { label: 'Launch game', action: async () => { try { 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, { instanceId: instance.id }) } }, color: 'brand' as const, }, ] : []), { label: 'Instance', action: () => opts.router.push(`/instance/${encodeURIComponent(instance.id)}`), }, ], autoCloseMs: null, }) } /** * 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) { const [project, projectV3] = await Promise.all([ get_project(serverProjectId, 'bypass'), get_project_v3(serverProjectId, 'bypass'), ]) const serverAddress = getServerAddress(projectV3?.minecraft_java_server) const content = projectV3?.minecraft_java_server?.content if (!content || content.kind !== 'modpack') return const contentVersionId = content.version_id const contentVersion = await get_version(contentVersionId, 'bypass') const contentProjectId = contentVersion.project_id const createJob = await install_create_modpack_instance( { type: 'fromVersionId', project_id: contentProjectId, version_id: contentVersionId, 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 await wait_for_install_job(createJob.job_id) await ensureManagedServerWorldExists(instanceId, project.title, serverAddress) } /** * Handles logic when clicking "Play" on a server project. This includes: * - Checking if need to install modpack content. If so, opens install to play modal * - Checking if need to update modpack content. If so, open update to play modal * - Checking if need to create instance for vanilla server. If so, creates instance. * - Adding server to worlds list if not already there * - Joining server */ async function playServerProject(projectId: string) { const [project, projectV3] = await Promise.all([ get_project(projectId, 'bypass'), get_project_v3(projectId, 'bypass'), ]) if (projectV3?.minecraft_server == null) { console.warn('playServerProject failed: project is not a server project') return } const content = projectV3?.minecraft_java_server?.content const serverAddress = getServerAddress(projectV3?.minecraft_java_server) const isVanilla = content?.kind === 'vanilla' const isModpack = content?.kind === 'modpack' const modpackVersionId = content?.version_id ?? null const recommendedGameVersion = content?.recommended_game_version let instance = await findInstalledInstance(project.id) if (isVanilla && !instance) { if (installingServerProjects.value.includes(projectId)) return startInstallingServer(projectId) try { const instanceId = await createVanillaInstance( project, recommendedGameVersion, serverAddress, ) if (instanceId) { instance = await get(instanceId) if (instance) showModpackInstallSuccess(instance, serverAddress) } } finally { stopInstallingServer(projectId) } return } if (isModpack && !instance) { installToPlayModalRef?.show(projectV3, modpackVersionId, async () => { const newInstance = await findInstalledInstance(project.id) if (!newInstance) return showModpackInstallSuccess(newInstance, serverAddress) }) return } if (!instance) return await ensureManagedServerWorldExists(instance.id, project.title, serverAddress) // Update existing instance if needed if (isModpack && instance.link?.version_id !== modpackVersionId) { updateToPlayModalRef?.show(instance, modpackVersionId, () => { showUpdateSuccess(instance, serverAddress) }) return } if (isVanilla && instance.game_version !== recommendedGameVersion) { if (installingServerProjects.value.includes(projectId)) return startInstallingServer(projectId) try { await updateVanillaGameVersion(instance, recommendedGameVersion) showUpdateSuccess(instance, serverAddress) } finally { stopInstallingServer(projectId) } return } // Join server try { await joinServer(instance.id, serverAddress) trackEvent('InstanceStart', { loader: instance.loader, game_version: instance.game_version, source: 'ServerProject', }) } catch (err) { handleSevereError(err, { instanceId: instance.id }) } } const context: ServerInstallContext = { installingServerProjects, startInstallingServer, stopInstallingServer, isServerInstalling, installServerProject, playServerProject, setInstallToPlayModal(ref) { installToPlayModalRef = ref }, setUpdateToPlayModal(ref) { updateToPlayModalRef = ref }, setAddServerToInstanceModal(ref) { addServerToInstanceModalRef = ref }, showAddServerToInstanceModal(serverName: string, serverAddress: string) { addServerToInstanceModalRef?.show(serverName, serverAddress) }, } _serverInstallSingleton = context return context }