Files
minecraft_launcher/app/assets/js/catalogmanager.js
claude-bot 9786cfe031
Some checks failed
Build / release (macos-latest) (push) Has been cancelled
Build / release (ubuntu-latest) (push) Has been cancelled
Build / release (windows-latest) (push) Has been cancelled
Windows Smoke Test / windows-smoke (push) Has been cancelled
Refactor launcher profiles and port automation
2026-05-05 21:52:17 +09:00

297 lines
8.8 KiB
JavaScript

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 normalizeBoolean(value){
return value === true
}
function normalizePositiveInteger(value, fallback, minimum = 1){
const parsed = Number.parseInt(String(value ?? ''), 10)
if(Number.isFinite(parsed) && parsed >= minimum){
return parsed
}
return fallback
}
function deriveFeatureFlags(rawProfile){
const legacyKind = normalizeText(rawProfile?.kind)
let modsEnabled = normalizeBoolean(rawProfile?.modsEnabled)
let pluginsEnabled = normalizeBoolean(rawProfile?.pluginsEnabled)
let serverEnabled = normalizeBoolean(rawProfile?.serverEnabled)
if(legacyKind === 'modpack'){
modsEnabled = true
} else if(legacyKind === 'server-pack'){
pluginsEnabled = true
serverEnabled = true
}
if(pluginsEnabled){
serverEnabled = true
}
return {
modsEnabled,
pluginsEnabled,
serverEnabled
}
}
function deriveLegacyKind(flags){
if(flags.serverEnabled){
return 'server-pack'
}
if(flags.modsEnabled){
return 'modpack'
}
return 'map'
}
function resolveLegacyServerJar(rawProfile){
const directValue = normalizeNullableText(rawProfile?.serverJarUrl)
if(directValue != null){
return directValue
}
const legacyBundle = normalizeNullableText(rawProfile?.serverBundleUrl)
if(legacyBundle != null && legacyBundle.toLowerCase().endsWith('.jar')){
return legacyBundle
}
return null
}
function toStoredProfile(rawProfile){
const flags = deriveFeatureFlags(rawProfile)
const storedProfile = {
id: normalizeText(rawProfile.id),
name: normalizeText(rawProfile.name),
kind: deriveLegacyKind(flags),
description: normalizeText(rawProfile.description),
details: normalizeText(rawProfile.details),
distributionUrl: normalizeNullableText(rawProfile.distributionUrl),
modsEnabled: flags.modsEnabled,
pluginsEnabled: flags.pluginsEnabled,
serverEnabled: flags.serverEnabled,
worldArchiveUrl: normalizeNullableText(rawProfile.worldArchiveUrl),
worldDirectoryName: normalizeNullableText(rawProfile.worldDirectoryName),
serverJarUrl: resolveLegacyServerJar(rawProfile),
serverDirectoryName: normalizeText(rawProfile.serverDirectoryName) || 'server',
serverPort: Number.isFinite(Number(rawProfile.serverPort)) ? Number(rawProfile.serverPort) : 25565,
serverMemoryMb: normalizePositiveInteger(rawProfile.serverMemoryMb, 4096, 512),
serverMaxPlayers: normalizePositiveInteger(rawProfile.serverMaxPlayers, 20, 1),
serverWhitelistEnabled: normalizeBoolean(rawProfile.serverWhitelistEnabled),
artwork: normalizeText(rawProfile.artwork)
}
if(!storedProfile.serverEnabled){
storedProfile.serverJarUrl = null
storedProfile.serverDirectoryName = 'server'
storedProfile.serverPort = 25565
storedProfile.serverMemoryMb = 4096
storedProfile.serverMaxPlayers = 20
storedProfile.serverWhitelistEnabled = false
}
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.worldArchiveUrl == null){
launchIssues.push('맵 ZIP 또는 로컬 월드 경로가 필요합니다.')
}
if(storedProfile.worldDirectoryName == null){
launchIssues.push('월드 폴더 이름이 필요합니다.')
}
if(storedProfile.serverEnabled && storedProfile.serverJarUrl == null){
hostIssues.push('로컬 서버를 시작하려면 버킷 JAR 업로드가 필요합니다.')
}
const launchReady = launchIssues.length === 0
const hostReady = storedProfile.serverEnabled ? 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()
const mergedProfiles = installedProfiles.map((installedProfile) => {
const latestProfile = catalog.profiles.find((profile) => profile.id === installedProfile.id)
return latestProfile != null
? {
...installedProfile,
...latestProfile,
installedAt: installedProfile.installedAt
}
: installedProfile
})
ConfigManager.setInstalledLibraryProfiles(mergedProfiles)
ConfigManager.save()
return mergedProfiles
}
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
}
return null
}
exports.setServerAddressOverride = function(profileId, address){
ConfigManager.setLibraryServerAddressOverride(profileId, address)
ConfigManager.save()
}
exports.shouldHostLocally = function(profile){
if(profile == null || profile.serverEnabled !== true){
return false
}
return exports.resolveServerAddress(profile) == null
}
exports.applyConfiguredProfile = function(){
const selectedProfile = exports.getSelectedProfileSync()
const distributionUrl = selectedProfile?.distributionUrl ?? DEFAULT_REMOTE_DISTRO_URL
setRemoteDistributionUrl(distributionUrl)
return selectedProfile
}