Add launcher catalog workflow and smoke tests
This commit is contained in:
234
app/assets/js/catalogmanager.js
Normal file
234
app/assets/js/catalogmanager.js
Normal file
@@ -0,0 +1,234 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user