diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 5c29e01..d229036 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -2464,7 +2464,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) class="app-contents" :class="{ 'sidebar-enabled': sidebarVisible, - 'studio-mode': route.name === 'FileStudio' || route.name === 'MultiplayerServerFileStudio', + 'studio-mode': route.name === 'FileStudio', 'disable-advanced-rendering': !themeStore.advancedRendering, 'has-custom-background': themeStore.customBackgroundPath && !themeStore.transparentBackground, 'has-transparent-background': themeStore.transparentBackground, diff --git a/apps/app-frontend/src/components/multiplayer/servers/CreateServerModal.vue b/apps/app-frontend/src/components/multiplayer/servers/CreateServerModal.vue deleted file mode 100644 index d4d1292..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/CreateServerModal.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/EulaModal.vue b/apps/app-frontend/src/components/multiplayer/servers/EulaModal.vue deleted file mode 100644 index b810c9c..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/EulaModal.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/ServerCard.vue b/apps/app-frontend/src/components/multiplayer/servers/ServerCard.vue deleted file mode 100644 index 2e294a7..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/ServerCard.vue +++ /dev/null @@ -1,263 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/ServerConsole.vue b/apps/app-frontend/src/components/multiplayer/servers/ServerConsole.vue deleted file mode 100644 index f880cf6..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/ServerConsole.vue +++ /dev/null @@ -1,212 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/ServerDetail.vue b/apps/app-frontend/src/components/multiplayer/servers/ServerDetail.vue deleted file mode 100644 index aaae1a1..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/ServerDetail.vue +++ /dev/null @@ -1,505 +0,0 @@ - - - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/ServerFileStudio.vue b/apps/app-frontend/src/components/multiplayer/servers/ServerFileStudio.vue deleted file mode 100644 index 6105eab..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/ServerFileStudio.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/ServerFilesPanel.vue b/apps/app-frontend/src/components/multiplayer/servers/ServerFilesPanel.vue deleted file mode 100644 index 2ffd1c2..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/ServerFilesPanel.vue +++ /dev/null @@ -1,265 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/ServerIcon.vue b/apps/app-frontend/src/components/multiplayer/servers/ServerIcon.vue deleted file mode 100644 index f5c5875..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/ServerIcon.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/ServerPropertiesEditor.vue b/apps/app-frontend/src/components/multiplayer/servers/ServerPropertiesEditor.vue deleted file mode 100644 index 30ab6e2..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/ServerPropertiesEditor.vue +++ /dev/null @@ -1,741 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/ServerSettingsPanel.vue b/apps/app-frontend/src/components/multiplayer/servers/ServerSettingsPanel.vue deleted file mode 100644 index 29b2ef5..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/ServerSettingsPanel.vue +++ /dev/null @@ -1,347 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/ServersOverview.vue b/apps/app-frontend/src/components/multiplayer/servers/ServersOverview.vue deleted file mode 100644 index bf715f7..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/ServersOverview.vue +++ /dev/null @@ -1,182 +0,0 @@ - - - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/create-server-flow.ts b/apps/app-frontend/src/components/multiplayer/servers/create-server-flow.ts deleted file mode 100644 index 0a14153..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/create-server-flow.ts +++ /dev/null @@ -1,466 +0,0 @@ -import { RefreshCwIcon } from '@modrinth/assets' -import { - isServerTypeSupported, - requiredJavaMajorVersion, - SERVER_TYPES, - type ServerTypeId, - setEulaAccepted, -} from '@modrinth/server' -import { - createContext, - defineMessages, - type MultiStageModal, - type StageConfigInput, - useVIntl, -} from '@modrinth/ui' -import { computed, markRaw, type Ref, ref } from 'vue' -import type { ComponentExposed } from 'vue-component-type-helpers' - -import { - javaMajorFromVersion, - toErrorMessage, -} from '@/components/multiplayer/servers/server-flow-utils' -import { getServerInstallStrategy, runServerInstall } from '@/composables/server-install' -import { refresh as refreshServerList } from '@/composables/useServers' -import { find_filtered_jres, get_java_default_versions, get_max_memory } from '@/helpers/jre' -import { get_game_versions, get_loader_versions } from '@/helpers/metadata' -import { type ServerManifestData, servers } from '@/helpers/servers' -import { injectDownloadManager } from '@/providers/download-manager' - -import InstallStage from './stages/InstallStage.vue' -import SetupStage from './stages/SetupStage.vue' -import TypeStage from './stages/TypeStage.vue' - -export type InstallPhase = - | 'idle' - | 'preparing' - | 'downloading' - | 'first-run' - | 'eula' - | 'error' - | 'done' - -export interface JavaSelection { - path: string - version: string -} - -export interface LoaderVersionOption { - id: string - stable: boolean -} - -export interface CreateServerFlowContext> { - modal: Ref | null> - stageConfigs: StageConfigInput[] - formatMessage: ReturnType['formatMessage'] - - serverType: Ref - availableGameVersions: Ref - selectedGameVersion: Ref - showSnapshots: Ref - loaderVersions: Ref - selectedLoaderVersion: Ref - isVersionsLoading: Ref - versionsError: Ref - - name: Ref - selectedJava: Ref - memoryMb: Ref - maxMemoryMb: Ref - - installPhase: Ref - downloadProgress: Ref<{ downloaded: number; total: number | null } | null> - installLog: Ref - installError: Ref - eulaText: Ref - createdServer: Ref - showEulaModal: Ref - - /** Registered by the configure stage to persist server.properties before finishing. */ - saveServerProperties: Ref<(() => Promise) | null> - - needsLoaderVersion: Ref - typeSupported: Ref - canContinueFromType: Ref - - loadVersions: () => Promise - loadLoaderVersions: () => Promise - loadDefaultJava: () => Promise - beginInstall: () => Promise - retryInstall: () => Promise - acceptEula: () => Promise - declineEula: () => void - reset: () => void -} - -/** Concrete context used by the vanilla (non-modpack) server creation flow. */ -export type CreateServerFlowContextValue = CreateServerFlowContext - -export const [injectCreateServerFlow, provideCreateServerFlow] = - createContext('CreateServerFlow') - -export function createCreateServerFlowContext( - modal: Ref | null>, -): CreateServerFlowContextValue { - const { formatMessage } = useVIntl() - - // [SERVER-DOWNLOAD-BRIDGE] Capture the download manager once during Vue - // setup context. Vue's inject() only works in the synchronous setup - // scope — after any `await` the injection context is lost. We store the - // reference here and pass it explicitly to the shared download bridge so - // the vanilla server download appears in the sidebar like the modpack flow. - let downloadManager: ReturnType | null = null - try { - downloadManager = injectDownloadManager() - } catch { - // Not inside a provider tree — server downloads will not appear in sidebar. - } - - const wizardMessages = defineMessages({ - typeStageTitle: { id: 'app.servers.wizard.type-title', defaultMessage: 'Server type' }, - setupStageTitle: { id: 'app.servers.wizard.setup-title', defaultMessage: 'Setup' }, - installStageTitle: { id: 'app.servers.wizard.install-title', defaultMessage: 'Install' }, - configureStageTitle: { id: 'app.servers.wizard.configure-title', defaultMessage: 'Configure' }, - next: { id: 'app.servers.wizard.next', defaultMessage: 'Next' }, - retry: { id: 'app.servers.wizard.retry', defaultMessage: 'Retry' }, - finish: { id: 'app.servers.wizard.finish', defaultMessage: 'Finish' }, - javaTooOld: { - id: 'app.servers.wizard.java-too-old', - defaultMessage: - 'Java {selected} cannot run this game version; Java {required} or newer is required.', - }, - }) - - const serverType = ref('vanilla') - const availableGameVersions = ref([]) - const selectedGameVersion = ref('') - const showSnapshots = ref(false) - const loaderVersions = ref([]) - const selectedLoaderVersion = ref('') - const isVersionsLoading = ref(false) - const versionsError = ref(null) - - const name = ref('') - const selectedJava = ref({ path: '', version: '' }) - const memoryMb = ref(2048) - const maxMemoryMb = ref(8192) - - const installPhase = ref('idle') - const downloadProgress = ref<{ downloaded: number; total: number | null } | null>(null) - const installLog = ref([]) - const installError = ref(null) - const eulaText = ref('') - const createdServer = ref(null) - const showEulaModal = ref(false) - const saveServerProperties = ref<(() => Promise) | null>(null) - - const needsLoaderVersion = computed( - () => SERVER_TYPES[serverType.value]?.needsLoaderVersion ?? false, - ) - const typeSupported = computed(() => isServerTypeSupported(serverType.value)) - - async function loadVersions() { - isVersionsLoading.value = true - versionsError.value = null - try { - const manifest = (await get_game_versions()) as { - latest: { release: string } - versions: { id: string; type: string; url: string }[] - } - const all = manifest.versions - availableGameVersions.value = all - .filter((entry) => (showSnapshots.value ? true : entry.type === 'release')) - .map((entry) => entry.id) - if (!availableGameVersions.value.includes(selectedGameVersion.value)) { - selectedGameVersion.value = - manifest.latest.release && availableGameVersions.value.includes(manifest.latest.release) - ? manifest.latest.release - : availableGameVersions.value[0] - } - await loadLoaderVersions() - } catch (error) { - versionsError.value = toErrorMessage(error) - } finally { - isVersionsLoading.value = false - } - } - - async function loadLoaderVersions() { - selectedLoaderVersion.value = '' - loaderVersions.value = [] - if (serverType.value !== 'fabric' || !selectedGameVersion.value) return - try { - const manifest = (await get_loader_versions('fabric', selectedGameVersion.value)) as { - gameVersions: Array<{ id: string; loaders: LoaderVersionOption[] }> - } - const entry = manifest.gameVersions.find((game) => game.id === selectedGameVersion.value) - loaderVersions.value = entry?.loaders ?? [] - selectedLoaderVersion.value = loaderVersions.value[0]?.id ?? '' - } catch { - loaderVersions.value = [] - } - } - - /** Prefills the Java path from the instance-level defaults, falling back to a scan. */ - async function loadDefaultJava() { - if (selectedJava.value.path !== '') return - const major = requiredJavaMajorVersion(selectedGameVersion.value || '1.21') - try { - const defaults = (await get_java_default_versions()) as Array<{ - parsed_version: number - version: string - path: string - }> - const match = - defaults.find((entry) => entry.parsed_version === major) ?? - defaults.find((entry) => entry.parsed_version >= major) - if (match) { - selectedJava.value = { path: match.path, version: match.version } - return - } - } catch { - // Fall through to a filtered scan - } - try { - const javas = (await find_filtered_jres(major)) as JavaSelection[] - if (javas.length > 0) selectedJava.value = javas[0] - } catch { - // Leave empty; the user picks manually in the setup stage - } - } - - async function loadMaxMemory() { - try { - const maxKiB = (await get_max_memory()) as number - maxMemoryMb.value = Math.max(1024, Math.floor(maxKiB / 1024)) - } catch { - maxMemoryMb.value = 8192 - } - } - - async function beginInstall() { - if (installPhase.value === 'downloading' || installPhase.value === 'first-run') return - installPhase.value = 'preparing' - installError.value = null - installLog.value = [] - downloadProgress.value = null - try { - const requiredJava = requiredJavaMajorVersion(selectedGameVersion.value) - const selectedMajor = javaMajorFromVersion(selectedJava.value.version) - if ( - selectedJava.value.path !== '' && - selectedMajor !== null && - selectedMajor < requiredJava - ) { - throw new Error( - formatMessage(wizardMessages.javaTooOld, { - selected: selectedMajor, - required: requiredJava, - }), - ) - } - const manifest = await servers.create({ - name: name.value, - serverType: serverType.value, - gameVersion: selectedGameVersion.value, - loaderVersion: serverType.value === 'fabric' ? selectedLoaderVersion.value : undefined, - javaPath: selectedJava.value.path || undefined, - memoryMb: memoryMb.value, - }) - createdServer.value = manifest - - // [SERVER-INSTALL] The shared orchestrator owns the sidebar download - // job, progress/log event forwarding, and cancellation. Each server - // type supplies a `ServerInstallStrategy` that knows how to obtain its - // launcher files; vanilla/Fabric/Paper download a jar, Forge runs its - // installer. This is the single reuse point for every server type. - const strategy = getServerInstallStrategy(serverType.value) - await runServerInstall({ - serverId: manifest.id, - name: name.value, - inputs: { - gameVersion: selectedGameVersion.value, - loaderVersion: selectedLoaderVersion.value || undefined, - javaPath: selectedJava.value.path || undefined, - memoryMb: memoryMb.value, - }, - strategy, - downloadManager, - onProgress: (progress) => { - downloadProgress.value = progress - }, - onLog: (line) => { - installLog.value.push(line) - if (installLog.value.length > 500) { - installLog.value.splice(0, installLog.value.length - 500) - } - }, - }) - - // [SERVER-EULA] Like the modpack flow, the server is not auto-started. - // A code-created `eula.txt` (eula=false) is written so the manual start - // gate (useServerLifecycle) can offer the EULA without booting the jar. - const eula = setEulaAccepted('', false) - await servers.writeFile(manifest.id, 'eula.txt', eula).catch(() => {}) - installPhase.value = 'done' - } catch (error) { - installPhase.value = 'error' - installError.value = toErrorMessage(error) - // A half-installed server must not linger in the list; retrying starts over. - if (createdServer.value) { - const failed = createdServer.value - createdServer.value = null - await servers.delete(failed.id).catch(() => {}) - void refreshServerList() - } - } - } - - function retryInstall(): Promise { - installPhase.value = 'idle' - return beginInstall() - } - - async function acceptEula() { - if (!createdServer.value) return - try { - const updated = setEulaAccepted(eulaText.value, true) - await servers.writeFile(createdServer.value.id, 'eula.txt', updated) - showEulaModal.value = false - installPhase.value = 'done' - } catch (error) { - installError.value = toErrorMessage(error) - installPhase.value = 'error' - showEulaModal.value = false - } - } - - function declineEula() { - showEulaModal.value = false - modal.value?.hide() - } - - function reset() { - serverType.value = 'vanilla' - selectedGameVersion.value = '' - selectedLoaderVersion.value = '' - loaderVersions.value = [] - name.value = '' - selectedJava.value = { path: '', version: '' } - memoryMb.value = 2048 - installPhase.value = 'idle' - installLog.value = [] - installError.value = null - downloadProgress.value = null - eulaText.value = '' - createdServer.value = null - showEulaModal.value = false - saveServerProperties.value = null - void loadVersions() - void loadMaxMemory() - } - - const canContinueFromType = computed( - () => - typeSupported.value && - selectedGameVersion.value !== '' && - (!needsLoaderVersion.value || selectedLoaderVersion.value !== ''), - ) - - const stageConfigs: StageConfigInput[] = [ - { - id: 'type', - stageContent: markRaw(TypeStage), - title: (ctx) => ctx.formatMessage(wizardMessages.typeStageTitle), - cannotNavigateForward: (ctx) => !ctx.canContinueFromType.value, - leftButtonConfig: () => null, - rightButtonConfig: (ctx) => ({ - label: ctx.formatMessage(wizardMessages.next), - color: 'brand', - disabled: !ctx.canContinueFromType.value, - onClick: () => ctx.modal.value?.nextStage(), - }), - }, - { - id: 'setup', - stageContent: markRaw(SetupStage), - title: (ctx) => ctx.formatMessage(wizardMessages.setupStageTitle), - cannotNavigateForward: (ctx) => ctx.name.value.trim() === '', - leftButtonConfig: () => null, - rightButtonConfig: (ctx) => ({ - label: ctx.formatMessage(wizardMessages.next), - color: 'brand', - disabled: ctx.name.value.trim() === '', - onClick: async () => { - await ctx.loadDefaultJava() - ctx.modal.value?.nextStage() - }, - }), - }, - { - id: 'install', - stageContent: markRaw(InstallStage), - title: (ctx) => ctx.formatMessage(wizardMessages.installStageTitle), - cannotNavigateForward: (ctx) => ctx.installPhase.value !== 'done', - // Downloads continue in the background once the wizard closes; only - // the first-run boot locks closing until the server reaches its EULA gate. - disableClose: (ctx) => ctx.installPhase.value === 'first-run', - leftButtonConfig: () => null, - rightButtonConfig: (ctx) => ({ - label: ctx.formatMessage( - ctx.installPhase.value === 'error' ? wizardMessages.retry : wizardMessages.finish, - ), - color: 'brand', - icon: ctx.installPhase.value === 'error' ? RefreshCwIcon : null, - iconPosition: 'after', - disabled: ctx.installPhase.value !== 'done' && ctx.installPhase.value !== 'error', - onClick: () => { - if (ctx.installPhase.value === 'error') { - ctx.retryInstall() - return - } - // Server is ready — close the wizard so the host can navigate to it. - ctx.modal.value?.hide() - }, - }), - }, - ] - - return { - modal, - stageConfigs, - formatMessage, - serverType, - availableGameVersions, - selectedGameVersion, - showSnapshots, - loaderVersions, - selectedLoaderVersion, - isVersionsLoading, - versionsError, - name, - selectedJava, - memoryMb, - maxMemoryMb, - installPhase, - downloadProgress, - installLog, - installError, - eulaText, - createdServer, - showEulaModal, - saveServerProperties, - needsLoaderVersion, - typeSupported, - canContinueFromType, - loadVersions, - loadLoaderVersions, - loadDefaultJava, - beginInstall, - retryInstall, - acceptEula, - declineEula, - reset, - } -} diff --git a/apps/app-frontend/src/components/multiplayer/servers/modpack/CreateModpackServerModal.vue b/apps/app-frontend/src/components/multiplayer/servers/modpack/CreateModpackServerModal.vue deleted file mode 100644 index 31a6551..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/modpack/CreateModpackServerModal.vue +++ /dev/null @@ -1,121 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/modpack/create-modpack-server-flow.ts b/apps/app-frontend/src/components/multiplayer/servers/modpack/create-modpack-server-flow.ts deleted file mode 100644 index 1da85f1..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/modpack/create-modpack-server-flow.ts +++ /dev/null @@ -1,537 +0,0 @@ -import type { Labrinth } from '@modrinth/api-client' -import { RefreshCwIcon } from '@modrinth/assets' -import { type ServerTypeId, setEulaAccepted } from '@modrinth/server' -import { - createContext, - defineMessages, - type MultiStageModal, - type StageConfigInput, - useVIntl, -} from '@modrinth/ui' -import { computed, markRaw, type Ref, ref } from 'vue' -import type { ComponentExposed } from 'vue-component-type-helpers' - -import { startModpackServerInstall } from '@/composables/useServerInstalls' -import { refresh as refreshServerList } from '@/composables/useServers' -import { find_filtered_jres, get_java_default_versions, get_max_memory } from '@/helpers/jre' -import { get_loader_versions } from '@/helpers/metadata' -import { serverEventListener, type ServerManifestData, servers } from '@/helpers/servers' -import { injectDownloadManager } from '@/providers/download-manager' - -import type { CreateServerFlowContext, JavaSelection } from '../create-server-flow' -import { - javaMajorFromVersion, - resolveServerLauncher, - toErrorMessage, - waitForServerStop, -} from '../server-flow-utils' -import ModpackInstallStage from './stages/ModpackInstallStage.vue' -import ModpackSetupStage from './stages/ModpackSetupStage.vue' - -export type ModpackInstallPhase = - | 'idle' - | 'preparing' - | 'downloading' - | 'first-run' - | 'eula' - | 'error' - | 'done' - -export interface ModpackServerOptions { - project: Labrinth.Projects.v2.Project - version: Labrinth.Versions.v2.Version -} - -export interface ModpackServerFlowContext extends CreateServerFlowContext { - modpackTitle: Ref - modpackVersionNumber: Ref - modpackIconUrl: Ref - loaderLabel: Ref - loaderSupported: Ref - gameVersionLabel: Ref - setPack: (project: Labrinth.Projects.v2.Project, version: Labrinth.Versions.v2.Version) => void -} - -export const [injectModpackServerFlow, provideModpackServerFlow] = - createContext('ModpackServerFlow') - -const MODPACK_SERVER_TYPES: Record = { - fabric: { type: 'fabric', label: 'Fabric' }, - quilt: { type: 'quilt', label: 'Quilt' }, - neoforge: { type: 'neoforge', label: 'NeoForge' }, - forge: { type: 'forge', label: 'Forge' }, -} - -/** Loaders whose server launcher the app can download and boot directly. */ -const SUPPORTED_MODPACK_LOADERS: ServerTypeId[] = ['vanilla', 'fabric', 'quilt', 'forge'] - -export function resolveModpackLoader(loaders: string[]): { type: ServerTypeId; label: string } { - for (const loader of loaders) { - const entry = MODPACK_SERVER_TYPES[loader.toLowerCase()] - if (entry) return entry - } - return { type: 'vanilla', label: 'Vanilla' } -} - -export function createModpackServerFlowContext( - modal: Ref | null>, -): ModpackServerFlowContext { - const { formatMessage } = useVIntl() - - // [SERVER-DOWNLOAD-BRIDGE] Capture the download manager once during Vue - // setup context. Vue's inject() only works in the synchronous setup - // scope — after any `await` the injection context is lost. We store - // the reference here and pass it explicitly to `startModpackServerInstall`. - let downloadManager: ReturnType | null = null - try { - downloadManager = injectDownloadManager() - } catch { - // Not inside a provider tree — server downloads will not appear in sidebar. - } - - const wizardMessages = defineMessages({ - setupTitle: { id: 'app.servers.wizard.setup-title', defaultMessage: 'Setup' }, - installTitle: { id: 'app.servers.wizard.install-title', defaultMessage: 'Install' }, - configureTitle: { id: 'app.servers.wizard.configure-title', defaultMessage: 'Configure' }, - next: { id: 'app.servers.wizard.next', defaultMessage: 'Next' }, - retry: { id: 'app.servers.wizard.retry', defaultMessage: 'Retry' }, - finish: { id: 'app.servers.wizard.finish', defaultMessage: 'Finish' }, - javaTooOld: { - id: 'app.servers.wizard.java-too-old', - defaultMessage: - 'Java {selected} cannot run this game version; Java {required} or newer is required.', - }, - firstRunCrashed: { - id: 'app.servers.modpack.first-run-crashed', - defaultMessage: - 'The server crashed during its first start. Check that your selected Java version is compatible, then try again.', - }, - }) - - const project = ref(null) - const version = ref(null) - - const serverType = ref('vanilla') - const availableGameVersions = ref([]) - const selectedGameVersion = ref('') - const showSnapshots = ref(false) - const loaderVersions = ref<{ id: string; stable: boolean }[]>([]) - const selectedLoaderVersion = ref('') - const isVersionsLoading = ref(false) - const versionsError = ref(null) - - const name = ref('') - const selectedJava = ref({ path: '', version: '' }) - const memoryMb = ref(2048) - const maxMemoryMb = ref(8192) - - const installPhase = ref('idle') - const downloadProgress = ref<{ downloaded: number; total: number | null } | null>(null) - const installLog = ref([]) - const installError = ref(null) - const eulaText = ref('') - const createdServer = ref(null) - const showEulaModal = ref(false) - const saveServerProperties = ref<(() => Promise) | null>(null) - let installSession = 0 - - const modpackTitle = ref('') - const modpackVersionNumber = ref('') - const modpackIconUrl = ref(undefined) - const loaderLabel = ref('') - const loaderSupported = ref(false) - const gameVersionLabel = ref('') - - const needsLoaderVersion = computed( - () => serverType.value === 'fabric' || serverType.value === 'quilt', - ) - const typeSupported = computed(() => loaderSupported.value) - const canContinueFromType = computed(() => loaderSupported.value) - - function setPack( - packProject: Labrinth.Projects.v2.Project, - packVersion: Labrinth.Versions.v2.Version, - ) { - project.value = packProject - version.value = packVersion - modpackTitle.value = packProject.title - modpackVersionNumber.value = packVersion.version_number ?? '' - modpackIconUrl.value = packProject.icon_url ?? undefined - - const gameVersion = packVersion.game_versions?.[0] ?? packProject.game_versions?.[0] ?? '' - // Merge both the project-level and version-level loader declarations. - // Modpack versions frequently leave `version.loaders` empty (the project - // field is the reliable source); the authoritative source is the mrpack's - // `modrinth.index.json` dependencies, but that is only available after - // download. See resolveModpackLoader's fallback note. - const loaderCandidates = [...(packProject.loaders ?? []), ...(packVersion.loaders ?? [])] - const loader = resolveModpackLoader(loaderCandidates) - serverType.value = loader.type - loaderLabel.value = loader.label - gameVersionLabel.value = gameVersion - selectedGameVersion.value = gameVersion - availableGameVersions.value = gameVersion ? [gameVersion] : [] - loaderSupported.value = SUPPORTED_MODPACK_LOADERS.includes(loader.type) - - // Default the server name to ` ` so different - // versions of the same modpack produce distinct server names instead of - // colliding. A short uid is appended only if a name collision remains - // (see beginInstall), mirroring the direct-server id style. - name.value = `${packProject.title} ${packVersion.version_number ?? ''}`.trim() - selectedLoaderVersion.value = '' - loaderVersions.value = [] - } - - async function loadVersions() { - // The modpack fixes the game version; nothing to load. - } - - async function loadLoaderVersions() { - selectedLoaderVersion.value = '' - loaderVersions.value = [] - if (!needsLoaderVersion.value || !selectedGameVersion.value) return - try { - const manifest = (await get_loader_versions(serverType.value, selectedGameVersion.value)) as { - gameVersions: Array<{ id: string; loaders: { id: string; stable: boolean }[] }> - } - const entry = manifest.gameVersions.find((game) => game.id === selectedGameVersion.value) - loaderVersions.value = entry?.loaders ?? [] - const stable = loaderVersions.value.find((option) => option.stable) ?? loaderVersions.value[0] - selectedLoaderVersion.value = stable?.id ?? '' - } catch { - loaderVersions.value = [] - } - } - - async function loadDefaultJava() { - if (selectedJava.value.path !== '') return - const major = javaMajorFromVersion(selectedGameVersion.value || '1.21') ?? 21 - try { - const defaults = (await get_java_default_versions()) as Array<{ - parsed_version: number - version: string - path: string - }> - const match = - defaults.find((entry) => entry.parsed_version === major) ?? - defaults.find((entry) => entry.parsed_version >= major) - if (match) { - selectedJava.value = { path: match.path, version: match.version } - return - } - } catch { - // Fall through to a filtered scan - } - try { - const javas = (await find_filtered_jres(major)) as JavaSelection[] - if (javas.length > 0) selectedJava.value = javas[0] - } catch { - // Leave empty; the user picks manually in the setup stage - } - } - - async function loadMaxMemory() { - try { - const maxKiB = (await get_max_memory()) as number - maxMemoryMb.value = Math.max(1024, Math.floor(maxKiB / 1024)) - } catch { - maxMemoryMb.value = 8192 - } - } - - async function beginInstall() { - if (installPhase.value === 'downloading' || installPhase.value === 'first-run') return - if (!project.value || !version.value) return - if (!loaderSupported.value) return - - // A closed wizard leaves its install promise running in the background. - // Reopening the wizard starts a fresh session; stale sessions must stop - // touching the shared state once their token is superseded. - const session = ++installSession - const isStale = () => installSession !== session - - installPhase.value = 'preparing' - installError.value = null - installLog.value = [] - downloadProgress.value = null - try { - await loadLoaderVersions() - if (isStale()) return - - const requiredJava = javaMajorFromVersion(selectedGameVersion.value) ?? 21 - const selectedMajor = javaMajorFromVersion(selectedJava.value.version) - if ( - selectedJava.value.path !== '' && - selectedMajor !== null && - selectedMajor < requiredJava - ) { - throw new Error( - formatMessage(wizardMessages.javaTooOld, { - selected: selectedMajor, - required: requiredJava, - }), - ) - } - - if (!createdServer.value) { - // Ensure the chosen name is unique among existing servers. When it - // collides we append a short uid (mirroring the direct-server id - // style) so duplicate modpack versions stay distinguishable; if the - // name is free, no suffix is added. - let finalName = name.value.trim() - try { - const existing = await servers.list() - const taken = new Set(existing.map((server) => server.name.trim().toLowerCase())) - if (taken.has(finalName.toLowerCase())) { - const uid = Math.random().toString(36).slice(2, 6) - finalName = `${finalName} ${uid}` - } - } catch { - // Best-effort uniqueness; the backend id already disambiguates. - } - - const manifest = await servers.create({ - name: finalName, - serverType: serverType.value, - gameVersion: selectedGameVersion.value, - loaderVersion: needsLoaderVersion.value ? selectedLoaderVersion.value : undefined, - javaPath: selectedJava.value.path || undefined, - memoryMb: memoryMb.value, - }) - if (isStale()) { - await servers.delete(manifest.id).catch(() => {}) - void refreshServerList() - return - } - createdServer.value = manifest - } - const serverId = createdServer.value.id - - // Past this point the server directory exists and the backend tracks - // install state on its manifest, so failures leave a retryable entry - // instead of being cleaned up. Only pre-install resolution errors - // (no launcher, no pack file) still remove the stub. - let dispatched = false - const unlistenEvents = await serverEventListener((id, payload) => { - if (id !== serverId || isStale()) return - if (payload.event === 'download_progress') { - downloadProgress.value = { - downloaded: payload.downloaded, - total: payload.total ?? null, - } - } else if (payload.event === 'log') { - installLog.value.push(payload.line) - if (installLog.value.length > 500) { - installLog.value.splice(0, installLog.value.length - 500) - } - } - }) - try { - const jar = await resolveServerLauncher( - serverType.value, - selectedGameVersion.value, - selectedLoaderVersion.value, - ) - if (!jar) { - throw new Error( - `No server launcher available for ${loaderLabel.value} on ${selectedGameVersion.value}`, - ) - } - - const primaryFile = - version.value.files.find((file) => file.primary) ?? version.value.files[0] - if (!primaryFile?.url) { - throw new Error('Modpack has no downloadable file') - } - - // The download runs through the shared background runner, so closing - // the wizard keeps it going; progress renders from the shared registry. - dispatched = true - installPhase.value = 'downloading' - // [SERVER-DOWNLOAD-BRIDGE] Pass the download manager reference - // captured during setup so the synthetic job appears in sidebar. - await startModpackServerInstall( - serverId, - { - mrpackUrl: primaryFile.url, - mrpackSha1: primaryFile.hashes?.sha1, - jarUrl: jar.url, - jarFilename: jar.filename, - jarSha1: jar.sha1, - modpackProjectId: project.value.id, - modpackVersionId: version.value.id, - modpackTitle: `${modpackTitle.value} ${modpackVersionNumber.value}`.trim(), - modpackIconUrl: modpackIconUrl.value, - }, - downloadManager, - ) - if (isStale()) return - - // Modpack installation complete, no auto-start. - // User will click "Start" later, which will handle EULA check via tryStartServer. - // A code-created `eula.txt` (eula=false) is written so the manual start - // gate can offer the EULA without booting the jar. - const eula = setEulaAccepted('', false) - await servers.writeFile(serverId, 'eula.txt', eula).catch(() => {}) - installPhase.value = 'done' - } catch (error) { - if (!dispatched && createdServer.value) { - const failed = createdServer.value - createdServer.value = null - await servers.delete(failed.id).catch(() => {}) - void refreshServerList() - } - throw error - } finally { - unlistenEvents() - } - } catch (error) { - installPhase.value = 'error' - installError.value = toErrorMessage(error) - } - } - - function retryInstall(): Promise { - installPhase.value = 'idle' - return beginInstall() - } - - async function acceptEula() { - if (!createdServer.value) return - try { - const updated = setEulaAccepted(eulaText.value, true) - await servers.writeFile(createdServer.value.id, 'eula.txt', updated) - showEulaModal.value = false - installPhase.value = 'done' - // Start the server after accepting EULA - await servers.start(createdServer.value.id) - // Wait for server to stop (crash or normal) - const stopped = await waitForServerStop(createdServer.value.id) - if (stopped?.event === 'stopped' && stopped.crashed) { - throw new Error(formatMessage(wizardMessages.firstRunCrashed)) - } - installPhase.value = 'done' - } catch (error) { - installError.value = toErrorMessage(error) - installPhase.value = 'error' - showEulaModal.value = false - } - } - - function declineEula() { - showEulaModal.value = false - modal.value?.hide() - } - - function reset() { - installSession++ - installPhase.value = 'idle' - installLog.value = [] - installError.value = null - eulaText.value = '' - createdServer.value = null - showEulaModal.value = false - saveServerProperties.value = null - selectedJava.value = { path: '', version: '' } - memoryMb.value = 2048 - void loadMaxMemory() - } - - const stageConfigs: StageConfigInput[] = [ - { - id: 'setup', - stageContent: markRaw(ModpackSetupStage), - title: (ctx: ModpackServerFlowContext) => ctx.formatMessage(wizardMessages.setupTitle), - cannotNavigateForward: (ctx: ModpackServerFlowContext) => - ctx.name.value.trim() === '' || !ctx.canContinueFromType.value, - leftButtonConfig: () => null, - rightButtonConfig: (ctx: ModpackServerFlowContext) => ({ - label: ctx.formatMessage(wizardMessages.next), - color: 'brand', - disabled: ctx.name.value.trim() === '' || !ctx.canContinueFromType.value, - onClick: async () => { - await ctx.loadDefaultJava() - ctx.modal.value?.nextStage() - }, - }), - }, - { - id: 'install', - stageContent: markRaw(ModpackInstallStage), - title: (ctx: ModpackServerFlowContext) => ctx.formatMessage(wizardMessages.installTitle), - cannotNavigateForward: (ctx: ModpackServerFlowContext) => ctx.installPhase.value !== 'done', - // Downloads continue in the background once the wizard closes; only - // the first-run boot locks closing. - disableClose: (ctx: ModpackServerFlowContext) => ctx.installPhase.value === 'first-run', - leftButtonConfig: () => null, - rightButtonConfig: (ctx: ModpackServerFlowContext) => ({ - label: ctx.formatMessage( - ctx.installPhase.value === 'error' - ? wizardMessages.retry - : ctx.installPhase.value === 'done' - ? wizardMessages.finish - : wizardMessages.next, - ), - color: 'brand', - icon: ctx.installPhase.value === 'error' ? RefreshCwIcon : null, - iconPosition: 'after', - disabled: ctx.installPhase.value !== 'done' && ctx.installPhase.value !== 'error', - onClick: () => { - if (ctx.installPhase.value === 'error') { - void ctx.retryInstall() - return - } - if (ctx.installPhase.value === 'done') { - ctx.modal.value?.hide() - return - } - ctx.modal.value?.nextStage() - }, - }), - }, - ] - - return { - modal, - stageConfigs, - formatMessage, - serverType, - availableGameVersions, - selectedGameVersion, - showSnapshots, - loaderVersions, - selectedLoaderVersion, - isVersionsLoading, - versionsError, - name, - selectedJava, - memoryMb, - maxMemoryMb, - installPhase, - downloadProgress, - installLog, - installError, - eulaText, - createdServer, - showEulaModal, - saveServerProperties, - needsLoaderVersion, - typeSupported, - canContinueFromType, - modpackTitle, - modpackVersionNumber, - modpackIconUrl, - loaderLabel, - loaderSupported, - gameVersionLabel, - loadVersions, - loadLoaderVersions, - loadDefaultJava, - beginInstall, - retryInstall, - acceptEula, - declineEula, - reset, - setPack, - } -} diff --git a/apps/app-frontend/src/components/multiplayer/servers/modpack/stages/ModpackInstallStage.vue b/apps/app-frontend/src/components/multiplayer/servers/modpack/stages/ModpackInstallStage.vue deleted file mode 100644 index e701c93..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/modpack/stages/ModpackInstallStage.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/modpack/stages/ModpackSetupStage.vue b/apps/app-frontend/src/components/multiplayer/servers/modpack/stages/ModpackSetupStage.vue deleted file mode 100644 index 7312bc5..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/modpack/stages/ModpackSetupStage.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/server-flow-utils.ts b/apps/app-frontend/src/components/multiplayer/servers/server-flow-utils.ts deleted file mode 100644 index 0c3264e..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/server-flow-utils.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { - type FabricInstallerVersionsResponse, - fabricInstallerVersionsUrl, - FORGE_MAVEN_URL, - forgePromotionsSlimUrl, - latestStablePaperBuild, - type PaperBuildsResponse, - paperBuildsUrl, - quiltInstallerVersionsUrl, - resolveServerJar, - type ServerJarDownload, - type ServerTypeId, - type VanillaVersionInfo, -} from '@modrinth/server' -import { getVersion } from '@tauri-apps/api/app' -import { fetch as tauriFetch } from '@tauri-apps/plugin-http' -import { type as osType } from '@tauri-apps/plugin-os' - -import { get_game_versions } from '@/helpers/metadata' -import { serverEventListener, type ServerEventPayload } from '@/helpers/servers' - -/** Best-effort conversion of an unknown error into a user-presentable string. */ -export function toErrorMessage(error: unknown): string { - if (error instanceof Error) return error.message - if (typeof error === 'string') return error - if (error && typeof error === 'object') { - const record = error as Record - for (const key of ['message', 'error', 'description'] as const) { - if (typeof record[key] === 'string') return record[key] - } - } - try { - return JSON.stringify(error) - } catch { - return String(error) - } -} - -/** Extracts the Java major version from strings like `17`, `1.8`, or `21.0.1`. */ -export function javaMajorFromVersion(version: string): number | null { - const parts = version - .split(/[._]/) - .map(Number) - .filter((value) => Number.isInteger(value) && value >= 0) - if (parts.length === 0) return null - if (parts[0] === 1 && parts.length > 1) return parts[1] - return parts[0] -} - -/** - * Waits for the server to emit a `stopped` event, resolving with the payload - * or `null` after a timeout. Used to run the first start during setup and know - * when the JVM has exited. - */ -export async function waitForServerStop(serverId: string): Promise { - return new Promise((resolve) => { - void serverEventListener((eventServerId, payload) => { - if (eventServerId !== serverId || payload.event !== 'stopped') return - resolve(payload) - }).then((unlisten) => { - setTimeout( - () => { - unlisten() - resolve(null) - }, - 10 * 60 * 1000, - ) - }) - }) -} - -let userAgentPromise: Promise | null = null - -/** - * Identifying User-Agent, required by services like the PaperMC downloads API. - * Mirrors the format used by the Rust backend. - */ -function launcherUserAgent(): Promise { - userAgentPromise ??= Promise.all([getVersion(), osType()]).then( - ([version, platform]) => - `garbage-human-studio/axolotl/${version} (${platform}; +https://www.ghs.red)`, - ) - userAgentPromise = userAgentPromise.catch( - () => 'garbage-human-studio/axolotl (+https://www.ghs.red)', - ) - return userAgentPromise -} - -export async function fetchJson(url: string): Promise { - const response = await tauriFetch(url, { - headers: { 'User-Agent': await launcherUserAgent() }, - }) - if (!response.ok) throw new Error('GET ' + url + ' failed: ' + response.status) - return (await response.json()) as T -} - -/** - * Resolves the server launcher jar download for a modpack server. Vanilla - * pulls the Mojang server jar; Fabric and Quilt use their meta service launcher - * jars with the newest stable installer. - */ -export async function resolveServerLauncher( - type: ServerTypeId, - gameVersion: string, - loaderVersion?: string, -): Promise { - switch (type) { - case 'vanilla': { - const manifest = (await get_game_versions()) as { - versions: { id: string; url: string }[] - } - const entry = manifest.versions.find((v) => v.id === gameVersion) - if (!entry) return null - const versionInfo = await fetchJson(entry.url) - return resolveServerJar('vanilla', { gameVersion, vanillaVersionInfo: versionInfo }) - } - case 'fabric': - case 'quilt': { - const installers = await fetchJson( - type === 'fabric' ? fabricInstallerVersionsUrl() : quiltInstallerVersionsUrl(), - ) - const installerVersion = installers[0]?.version - return resolveServerJar(type, { gameVersion, loaderVersion, installerVersion }) - } - case 'paper': { - const builds = await fetchJson(paperBuildsUrl(gameVersion)) - const build = latestStablePaperBuild(builds) - if (!build) return null - return resolveServerJar(type, { gameVersion, paperBuild: build }) - } - case 'forge': { - // The Forge "launcher" is the installer jar; the backend runs it - // headlessly (`--installServer`) to materialize the server files. - const promos = await fetchJson<{ promos: Record }>(forgePromotionsSlimUrl()) - const build = - promos.promos[`${gameVersion}-recommended`] ?? promos.promos[`${gameVersion}-latest`] - if (!build) return null - const filename = `forge-${gameVersion}-${build}-installer.jar` - return { - url: `${FORGE_MAVEN_URL}/${gameVersion}-${build}/${filename}`, - filename, - sha1: undefined, - } - } - default: - return null - } -} diff --git a/apps/app-frontend/src/components/multiplayer/servers/server-status.ts b/apps/app-frontend/src/components/multiplayer/servers/server-status.ts deleted file mode 100644 index 68309f6..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/server-status.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ServerStatus } from '@modrinth/server' -import { defineMessages } from '@modrinth/ui' - -export const serverStatusMessages = defineMessages({ - created: { id: 'app.servers.status.created', defaultMessage: 'Not set up' }, - eulaPending: { id: 'app.servers.status.eula-pending', defaultMessage: 'EULA pending' }, - ready: { id: 'app.servers.status.ready', defaultMessage: 'Ready' }, - starting: { id: 'app.servers.status.starting', defaultMessage: 'Starting' }, - running: { id: 'app.servers.status.running', defaultMessage: 'Running' }, - crashed: { id: 'app.servers.status.crashed', defaultMessage: 'Crashed' }, -}) - -export interface ServerStatusMeta { - label: (typeof serverStatusMessages)[keyof typeof serverStatusMessages] - color: string -} - -export const SERVER_STATUS_META: Record = { - created: { label: serverStatusMessages.created, color: 'text-secondary' }, - eula_pending: { label: serverStatusMessages.eulaPending, color: 'text-orange' }, - ready: { label: serverStatusMessages.ready, color: 'text-brand' }, - starting: { label: serverStatusMessages.starting, color: 'text-orange' }, - running: { label: serverStatusMessages.running, color: 'text-green' }, - crashed: { label: serverStatusMessages.crashed, color: 'text-red' }, -} - -/** Idle/closed states that should not render a status tag. */ -export function isServerStatusVisible(status: ServerStatus): boolean { - return status !== 'created' && status !== 'ready' -} diff --git a/apps/app-frontend/src/components/multiplayer/servers/server-type.ts b/apps/app-frontend/src/components/multiplayer/servers/server-type.ts deleted file mode 100644 index 14d8a58..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/server-type.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ServerTypeId } from '@modrinth/server' - -/** Color, monogram and icon used to badge a server type across cards and the wizard. */ -export interface ServerTypeMeta { - colorVar: string - monogram: string - icon?: string -} - -const typeIcon = (name: string) => - new URL(`../../../assets/instance-icons/${name}`, import.meta.url).href - -const PLATFORM_ID = (id: ServerTypeId) => `var(--color-platform-${id})` - -export const SERVER_TYPE_META: Record = { - vanilla: { colorVar: 'var(--color-brand)', monogram: 'V', icon: typeIcon('Mojang.svg') }, - fabric: { colorVar: PLATFORM_ID('fabric'), monogram: 'F', icon: typeIcon('Fabric.png') }, - paper: { colorVar: PLATFORM_ID('paper'), monogram: 'P', icon: typeIcon('Paper.svg') }, - forge: { colorVar: PLATFORM_ID('forge'), monogram: 'Fo', icon: typeIcon('Forge.jpeg') }, - neoforge: { colorVar: PLATFORM_ID('neoforge'), monogram: 'N' }, - quilt: { colorVar: PLATFORM_ID('quilt'), monogram: 'Q' }, -} diff --git a/apps/app-frontend/src/components/multiplayer/servers/stages/ConfigureStage.vue b/apps/app-frontend/src/components/multiplayer/servers/stages/ConfigureStage.vue deleted file mode 100644 index 167af3d..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/stages/ConfigureStage.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/stages/InstallStage.vue b/apps/app-frontend/src/components/multiplayer/servers/stages/InstallStage.vue deleted file mode 100644 index 360c2b6..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/stages/InstallStage.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/stages/SetupStage.vue b/apps/app-frontend/src/components/multiplayer/servers/stages/SetupStage.vue deleted file mode 100644 index e94e236..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/stages/SetupStage.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/apps/app-frontend/src/components/multiplayer/servers/stages/TypeStage.vue b/apps/app-frontend/src/components/multiplayer/servers/stages/TypeStage.vue deleted file mode 100644 index 75cde19..0000000 --- a/apps/app-frontend/src/components/multiplayer/servers/stages/TypeStage.vue +++ /dev/null @@ -1,164 +0,0 @@ - - - diff --git a/apps/app-frontend/src/pages/Multiplayer.vue b/apps/app-frontend/src/pages/Multiplayer.vue index c8466a4..f816099 100644 --- a/apps/app-frontend/src/pages/Multiplayer.vue +++ b/apps/app-frontend/src/pages/Multiplayer.vue @@ -1,5 +1,5 @@