import type { Archon } from '@modrinth/api-client' import { computed, type ComputedRef, type Ref, ref, type ShallowRef, watch } from 'vue' import type { ComponentExposed } from 'vue-component-type-helpers' import { useDebugLogger } from '#ui/composables/debug-logger' import { formatLoaderLabel } from '#ui/utils/loaders' import { createContext } from '../../../providers' import type { ImportableLauncher } from '../../../providers/instance-import' import type { MultiStageModal, StageConfigInput } from '../../base' import type { ComboboxOption } from '../../base/Combobox.vue' import { stageConfigs } from './stages' export type FlowType = 'world' | 'server-onboarding' | 'reset-server' | 'instance' export type SetupType = 'modpack' | 'custom' | 'vanilla' export type Gamemode = 'survival' | 'creative' | 'hardcore' export type Difficulty = 'peaceful' | 'easy' | 'normal' | 'hard' export type LoaderVersionType = 'stable' | 'latest' | 'other' export type GeneratorSettingsMode = 'default' | 'flat' | 'custom' export interface ModpackSelection { projectId: string versionId: string name: string iconUrl?: string } export interface ModpackSearchHit { title: string iconUrl?: string latestVersion?: string } export interface ModpackSearchResult { hits: { project_id: string title: string icon_url: string latest_version?: string }[] total_hits: number offset: number limit: number } export const flowTypeHeadings: Record = { world: 'Create world', 'server-onboarding': 'Set up server', 'reset-server': 'Reset server', instance: 'Create instance', } export interface CreationFlowContextValue { // Flow flowType: FlowType // Configuration availableLoaders: string[] showSnapshotToggle: boolean disableClose: boolean isInitialSetup: boolean // Initial values initialLoader: string | null initialGameVersion: string | null // State setupType: Ref isImportMode: Ref worldName: Ref gamemode: Ref difficulty: Ref worldSeed: Ref worldTypeOption: Ref generateStructures: Ref generatorSettingsMode: Ref generatorSettingsCustom: Ref // Instance-specific state instanceName: Ref autoInstanceName: ComputedRef instanceIcon: Ref instanceIconUrl: Ref instanceIconPath: Ref // Loader/version state (custom setup) selectedLoader: Ref selectedGameVersion: Ref loaderVersionType: Ref selectedLoaderVersion: Ref hideLoaderChips: ComputedRef hideLoaderVersion: ComputedRef showSnapshots: Ref // Modpack state modpackSelection: Ref modpackFile: Ref modpackFilePath: Ref // Modpack search state (persisted across stage navigation) modpackSearchProjectId: Ref modpackSearchVersionId: Ref modpackSearchOptions: Ref[]> modpackVersionOptions: Ref[]> modpackSearchHits: Ref> // Import state (instance flow only) importLaunchers: Ref importSelectedInstances: Ref>> importSearchQuery: Ref // Confirm stage hardReset: Ref // Loading state (set when finish() is called, cleared on reset) loading: Ref // Backup state (set by InlineBackupCreator in reset-server flow) isBackingUp: Ref cancelBackup: Ref<(() => void) | null> // Modal modal: ShallowRef | null> stageConfigs: StageConfigInput[] // Callbacks onBack: (() => void) | null // Methods reset: (instanceCount?: number) => Promise setSetupType: (type: SetupType) => void setImportMode: () => void browseModpacks: () => void finish: () => void buildProperties: () => Archon.Content.v1.PropertiesFields // Platform-provided search searchModpacks: (query: string, limit?: number) => Promise getProjectVersions: (projectId: string) => Promise<{ id: string }[]> } export const [injectCreationFlowContext, provideCreationFlowContext] = createContext('CreationFlowModal') // TODO: replace with actual world count from the world list once available let worldCounter = 0 export interface CreationFlowOptions { availableLoaders?: string[] showSnapshotToggle?: boolean disableClose?: boolean isInitialSetup?: boolean initialLoader?: string initialGameVersion?: string fetchExistingInstanceNames?: () => Promise onBack?: () => void searchModpacks?: (query: string, limit?: number) => Promise getProjectVersions?: (projectId: string) => Promise<{ id: string }[]> } export function createCreationFlowContext( modal: ShallowRef | null>, flowType: FlowType, emit: { browseModpacks: () => void create: (config: CreationFlowContextValue) => void }, options: CreationFlowOptions = {}, ): CreationFlowContextValue { const debug = useDebugLogger('CreationFlow') const availableLoaders = options.availableLoaders ?? ['fabric', 'neoforge', 'forge', 'quilt'] const showSnapshotToggle = options.showSnapshotToggle ?? false const disableClose = options.disableClose ?? false const isInitialSetup = options.isInitialSetup ?? false const initialLoader = options.initialLoader ?? null const initialGameVersion = options.initialGameVersion ?? null const onBack = options.onBack ?? null const setupType = ref(null) const isImportMode = ref(false) const worldName = ref('') const gamemode = ref('survival') const difficulty = ref('normal') const worldSeed = ref('') const worldTypeOption = ref('minecraft:normal') const generateStructures = ref(true) const generatorSettingsMode = ref('default') const generatorSettingsCustom = ref('') // Instance-specific state const instanceName = ref('') const existingInstanceNames = ref([]) const fetchExistingInstanceNames = options.fetchExistingInstanceNames ?? null const instanceIcon = ref(null) const instanceIconUrl = ref(null) const instanceIconPath = ref(null) // Revoke old object URL when icon is cleared to avoid memory leaks watch(instanceIconUrl, (_newUrl, oldUrl) => { if (oldUrl && oldUrl.startsWith('blob:')) { URL.revokeObjectURL(oldUrl) } }) const selectedLoader = ref(null) const selectedGameVersion = ref(null) const loaderVersionType = ref('stable') const selectedLoaderVersion = ref(null) const showSnapshots = ref(false) const autoInstanceName = computed(() => { const loader = selectedLoader.value const version = selectedGameVersion.value if (!version) return '' const loaderName = loader ? formatLoaderLabel(loader) : 'Vanilla' const baseName = `${loaderName} ${version}` const names = new Set(existingInstanceNames.value) if (!names.has(baseName)) return baseName let counter = 1 while (names.has(`${baseName} (${counter})`)) { counter++ } return `${baseName} (${counter})` }) const modpackSelection = ref(null) const modpackFile = ref(null) const modpackFilePath = ref(null) // Modpack search state (persisted across stage navigation) const modpackSearchProjectId = ref() const modpackSearchVersionId = ref() const modpackSearchOptions = ref[]>([]) const modpackVersionOptions = ref[]>([]) const modpackSearchHits = ref>({}) // Import state (instance flow only) const importLaunchers = ref([]) const importSelectedInstances = ref>>({}) const importSearchQuery = ref('') const hardReset = ref(isInitialSetup) const loading = ref(false) const isBackingUp = ref(false) const cancelBackup = ref<(() => void) | null>(null) // hideLoaderChips: hides the entire loader chips section (only for vanilla world type in world/server flows) const hideLoaderChips = computed(() => setupType.value === 'vanilla') // hideLoaderVersion: hides the loader version section (vanilla world type OR vanilla selected as loader chip) const hideLoaderVersion = computed( () => setupType.value === 'vanilla' || selectedLoader.value === 'vanilla', ) async function reset() { if (fetchExistingInstanceNames) { existingInstanceNames.value = await fetchExistingInstanceNames() } setupType.value = null isImportMode.value = false worldCounter++ worldName.value = flowType === 'world' ? `World ${worldCounter}` : '' gamemode.value = 'survival' difficulty.value = 'normal' worldSeed.value = '' worldTypeOption.value = 'minecraft:normal' generateStructures.value = true generatorSettingsMode.value = 'default' generatorSettingsCustom.value = '' // Instance-specific instanceName.value = '' instanceIconUrl.value = null instanceIcon.value = null instanceIconPath.value = null selectedLoader.value = null selectedGameVersion.value = null loaderVersionType.value = 'stable' selectedLoaderVersion.value = null showSnapshots.value = false modpackSelection.value = null modpackFile.value = null modpackFilePath.value = null modpackSearchProjectId.value = undefined modpackSearchVersionId.value = undefined modpackSearchOptions.value = [] modpackVersionOptions.value = [] modpackSearchHits.value = {} // Import state importLaunchers.value = [] importSelectedInstances.value = {} importSearchQuery.value = '' hardReset.value = isInitialSetup loading.value = false isBackingUp.value = false cancelBackup.value = null } function setSetupType(type: SetupType) { debug('setSetupType:', type) isImportMode.value = false setupType.value = type if (type === 'modpack') { modal.value?.setStage('modpack') } else { // both custom and vanilla go to custom-setup // vanilla just hides loader chips via hideLoaderChips computed modal.value?.setStage('custom-setup') } } function setImportMode() { isImportMode.value = true setupType.value = null modal.value?.setStage('import-instance') } function browseModpacks() { modal.value?.hide() emit.browseModpacks() } function finish() { debug('finish() called, state:', { setupType: setupType.value, selectedLoader: selectedLoader.value, selectedGameVersion: selectedGameVersion.value, selectedLoaderVersion: selectedLoaderVersion.value, modpackSelection: modpackSelection.value, hasModpackFile: !!modpackFile.value, }) loading.value = true emit.create(contextValue) } function buildProperties(): Archon.Content.v1.PropertiesFields { const isHardcore = gamemode.value === 'hardcore' const known: Archon.Content.v1.KnownPropertiesFields = { gamemode: isHardcore ? 'survival' : gamemode.value, hardcore: isHardcore ? 'true' : 'false', difficulty: difficulty.value, level_seed: worldSeed.value || null, level_type: worldTypeOption.value, generate_structures: String(generateStructures.value), } if (generatorSettingsMode.value === 'flat') { known.generator_settings = '' } else if (generatorSettingsMode.value === 'custom' && generatorSettingsCustom.value) { known.generator_settings = generatorSettingsCustom.value } return { known } } const searchModpacks = options.searchModpacks! const getProjectVersions = options.getProjectVersions! const resolvedStageConfigs = disableClose ? stageConfigs.map((stage) => ({ ...stage, disableClose: true })) : stageConfigs const contextValue: CreationFlowContextValue = { flowType, availableLoaders, showSnapshotToggle, disableClose, isInitialSetup, initialLoader, initialGameVersion, setupType, isImportMode, worldName, gamemode, difficulty, worldSeed, worldTypeOption, generateStructures, generatorSettingsMode, generatorSettingsCustom, instanceName, autoInstanceName, instanceIcon, instanceIconUrl, instanceIconPath, selectedLoader, selectedGameVersion, loaderVersionType, selectedLoaderVersion, hideLoaderChips, hideLoaderVersion, showSnapshots, modpackSelection, modpackFile, modpackFilePath, modpackSearchProjectId, modpackSearchVersionId, modpackSearchOptions, modpackVersionOptions, modpackSearchHits, importLaunchers, importSelectedInstances, importSearchQuery, hardReset, loading, isBackingUp, cancelBackup, modal, stageConfigs: resolvedStageConfigs, onBack, reset, setSetupType, setImportMode, browseModpacks, finish, buildProperties, searchModpacks, getProjectVersions, } return contextValue }