const fs = require('fs-extra') const got = require('got') const path = require('path') const ConfigManager = require('./configmanager') const { DEFAULT_REMOTE_DISTRO_URL, setRemoteDistributionUrl } = require('./distromanager') const LOCAL_CATALOG_PATH = path.join(__dirname, '..', 'launcher', 'catalog.json') function normalizeText(value){ return typeof value === 'string' ? value.trim() : '' } function normalizeNullableText(value){ const nextValue = normalizeText(value) return nextValue.length > 0 ? nextValue : null } function toStoredProfile(rawProfile){ const kind = normalizeText(rawProfile.kind) || 'modpack' const storedProfile = { id: normalizeText(rawProfile.id), name: normalizeText(rawProfile.name), kind, description: normalizeText(rawProfile.description), details: normalizeText(rawProfile.details), distributionUrl: normalizeNullableText(rawProfile.distributionUrl), defaultServerAddress: normalizeText(rawProfile.defaultServerAddress), allowCustomServerAddress: rawProfile.allowCustomServerAddress === true, worldArchiveUrl: normalizeNullableText(rawProfile.worldArchiveUrl), worldDirectoryName: normalizeNullableText(rawProfile.worldDirectoryName), serverBundleUrl: normalizeNullableText(rawProfile.serverBundleUrl), serverDirectoryName: normalizeText(rawProfile.serverDirectoryName) || 'server', serverLaunchCommand: normalizeNullableText(rawProfile.serverLaunchCommand), serverWorkingDirectory: normalizeNullableText(rawProfile.serverWorkingDirectory), serverPort: Number.isFinite(Number(rawProfile.serverPort)) ? Number(rawProfile.serverPort) : 25565, tunnelCommand: normalizeNullableText(rawProfile.tunnelCommand), tunnelAddressRegex: normalizeNullableText(rawProfile.tunnelAddressRegex), artwork: normalizeText(rawProfile.artwork) } if(kind !== 'map'){ storedProfile.worldArchiveUrl = null storedProfile.worldDirectoryName = null } if(kind !== 'server-pack'){ storedProfile.serverBundleUrl = null storedProfile.serverDirectoryName = 'server' storedProfile.serverLaunchCommand = null storedProfile.serverWorkingDirectory = null storedProfile.serverPort = 25565 storedProfile.tunnelCommand = null storedProfile.tunnelAddressRegex = null } return storedProfile } function normalizeProfile(rawProfile, sourceType = 'catalog'){ const storedProfile = toStoredProfile(rawProfile) const launchIssues = [] const hostIssues = [] if(storedProfile.distributionUrl == null){ launchIssues.push('distribution URL이 필요합니다.') } if(storedProfile.kind === 'map'){ if(storedProfile.worldArchiveUrl == null){ launchIssues.push('맵 ZIP 또는 로컬 월드 경로가 필요합니다.') } if(storedProfile.worldDirectoryName == null){ launchIssues.push('월드 폴더 이름이 필요합니다.') } } if(storedProfile.kind === 'server-pack' && storedProfile.serverBundleUrl == null){ hostIssues.push('로컬 호스팅을 하려면 서버 번들 ZIP 또는 디렉터리 경로가 필요합니다.') } const launchReady = launchIssues.length === 0 const hostReady = storedProfile.kind === 'server-pack' ? hostIssues.length === 0 : false return { ...storedProfile, sourceType, isCustom: sourceType === 'custom', configured: launchReady, launchReady, launchIssues, hostReady, hostIssues } } function resolveCatalogFileSource(source){ if(source.startsWith('file://')){ return decodeURIComponent(source.substring('file://'.length)) } return path.resolve(source) } async function readCatalogSource(source){ if(/^https?:\/\//i.test(source)){ return got(source, { responseType: 'json' }).json() } return fs.readJson(resolveCatalogFileSource(source)) } exports.getLocalCatalogPath = function(){ return LOCAL_CATALOG_PATH } exports.loadCatalog = async function(){ const configuredSource = ConfigManager.getLibraryCatalogSource() const source = configuredSource != null && configuredSource.trim().length > 0 ? configuredSource.trim() : LOCAL_CATALOG_PATH let rawCatalog = { version: 1, profiles: [] } let sourceError = null try { rawCatalog = await readCatalogSource(source) } catch (error) { sourceError = error } const rawProfiles = Array.isArray(rawCatalog.profiles) ? rawCatalog.profiles : [] return { version: rawCatalog.version ?? 1, source, sourceError, profiles: rawProfiles .filter((profile) => profile != null && typeof profile.id === 'string' && typeof profile.name === 'string') .map((profile) => normalizeProfile(profile, 'catalog')) .sort((left, right) => left.name.localeCompare(right.name, 'ko')) } } exports.getInstalledProfiles = async function(){ const catalog = await exports.loadCatalog() const installedProfiles = ConfigManager.getInstalledLibraryProfiles() return installedProfiles.map((installedProfile) => { const latestProfile = catalog.profiles.find((profile) => profile.id === installedProfile.id) return latestProfile != null ? { ...installedProfile, ...latestProfile, installedAt: installedProfile.installedAt } : installedProfile }) } exports.installProfile = async function(profileId){ const catalog = await exports.loadCatalog() const profile = catalog.profiles.find((entry) => entry.id === profileId) if(profile == null){ throw new Error(`Unknown profile: ${profileId}`) } const installedProfile = { ...profile, installedAt: new Date().toISOString() } ConfigManager.upsertInstalledLibraryProfile(installedProfile) if(ConfigManager.getSelectedLibraryProfile() == null){ ConfigManager.setSelectedLibraryProfile(profile.id) } ConfigManager.save() return installedProfile } exports.removeProfile = function(profileId){ ConfigManager.removeInstalledLibraryProfile(profileId) ConfigManager.save() } exports.selectProfile = function(profileId){ ConfigManager.setSelectedLibraryProfile(profileId) ConfigManager.save() } exports.getSelectedProfileId = function(){ return ConfigManager.getSelectedLibraryProfile() } exports.getSelectedProfileSync = function(){ const selectedProfileId = ConfigManager.getSelectedLibraryProfile() if(selectedProfileId == null){ return null } return ConfigManager.getInstalledLibraryProfile(selectedProfileId) } exports.resolveServerAddress = function(profile){ if(profile == null){ return null } const manualAddress = ConfigManager.getLibraryServerAddressOverride(profile.id) if(manualAddress != null && manualAddress.length > 0){ return manualAddress } if(typeof profile.defaultServerAddress === 'string' && profile.defaultServerAddress.length > 0){ return profile.defaultServerAddress } return null } exports.setServerAddressOverride = function(profileId, address){ ConfigManager.setLibraryServerAddressOverride(profileId, address) ConfigManager.save() } exports.applyConfiguredProfile = function(){ const selectedProfile = exports.getSelectedProfileSync() const distributionUrl = selectedProfile?.distributionUrl ?? DEFAULT_REMOTE_DISTRO_URL setRemoteDistributionUrl(distributionUrl) return selectedProfile }