Enable remote launcher catalog source
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

This commit is contained in:
2026-05-05 22:46:03 +09:00
parent 9e8fd9e74b
commit 87c56a21d5
11 changed files with 188 additions and 38 deletions

View File

@@ -6,6 +6,8 @@ const ConfigManager = require('./configmanager')
const { DEFAULT_REMOTE_DISTRO_URL, setRemoteDistributionUrl } = require('./distromanager')
const LOCAL_CATALOG_PATH = path.join(__dirname, '..', 'launcher', 'catalog.json')
const ROOT_ASSET_PATH = path.join(__dirname, '..', '..', '..')
const DEFAULT_REMOTE_CATALOG_URL = normalizeNullableText(process.env.LAUNCHER_CATALOG_URL) || 'http://127.0.0.1:8787/catalog.json'
function normalizeText(value){
return typeof value === 'string' ? value.trim() : ''
@@ -28,6 +30,14 @@ function normalizePositiveInteger(value, fallback, minimum = 1){
return fallback
}
function isRemoteSource(source){
return /^https?:\/\//i.test(source)
}
function isAbsoluteFileSystemPath(source){
return path.isAbsolute(source) || /^[a-zA-Z]:[\\/]/.test(source)
}
function deriveFeatureFlags(rawProfile){
const legacyKind = normalizeText(rawProfile?.kind)
@@ -77,7 +87,24 @@ function resolveLegacyServerJar(rawProfile){
return null
}
function toStoredProfile(rawProfile){
function resolveProfileSourceValue(value, catalogSource){
const nextValue = normalizeNullableText(value)
if(nextValue == null){
return null
}
if(isRemoteSource(nextValue) || nextValue.startsWith('file://') || isAbsoluteFileSystemPath(nextValue)){
return nextValue
}
if(isRemoteSource(catalogSource)){
return new URL(nextValue, catalogSource).toString()
}
return path.resolve(ROOT_ASSET_PATH, nextValue)
}
function toStoredProfile(rawProfile, catalogSource){
const flags = deriveFeatureFlags(rawProfile)
const storedProfile = {
id: normalizeText(rawProfile.id),
@@ -85,19 +112,19 @@ function toStoredProfile(rawProfile){
kind: deriveLegacyKind(flags),
description: normalizeText(rawProfile.description),
details: normalizeText(rawProfile.details),
distributionUrl: normalizeNullableText(rawProfile.distributionUrl),
distributionUrl: resolveProfileSourceValue(rawProfile.distributionUrl, catalogSource),
modsEnabled: flags.modsEnabled,
pluginsEnabled: flags.pluginsEnabled,
serverEnabled: flags.serverEnabled,
worldArchiveUrl: normalizeNullableText(rawProfile.worldArchiveUrl),
worldArchiveUrl: resolveProfileSourceValue(rawProfile.worldArchiveUrl, catalogSource),
worldDirectoryName: normalizeNullableText(rawProfile.worldDirectoryName),
serverJarUrl: resolveLegacyServerJar(rawProfile),
serverJarUrl: resolveProfileSourceValue(resolveLegacyServerJar(rawProfile), catalogSource),
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)
artwork: resolveProfileSourceValue(rawProfile.artwork, catalogSource) ?? ''
}
if(!storedProfile.serverEnabled){
@@ -112,8 +139,8 @@ function toStoredProfile(rawProfile){
return storedProfile
}
function normalizeProfile(rawProfile, sourceType = 'catalog'){
const storedProfile = toStoredProfile(rawProfile)
function normalizeProfile(rawProfile, sourceType = 'catalog', catalogSource = LOCAL_CATALOG_PATH){
const storedProfile = toStoredProfile(rawProfile, catalogSource)
const launchIssues = []
const hostIssues = []
@@ -168,19 +195,39 @@ exports.getLocalCatalogPath = function(){
return LOCAL_CATALOG_PATH
}
exports.getDefaultRemoteCatalogUrl = function(){
return DEFAULT_REMOTE_CATALOG_URL
}
function buildCatalogSourceCandidates(configuredSource){
const trimmedSource = normalizeNullableText(configuredSource)
if(trimmedSource != null){
return [trimmedSource]
}
return [DEFAULT_REMOTE_CATALOG_URL, LOCAL_CATALOG_PATH]
}
exports.loadCatalog = async function(){
const configuredSource = ConfigManager.getLibraryCatalogSource()
const source = configuredSource != null && configuredSource.trim().length > 0 ? configuredSource.trim() : LOCAL_CATALOG_PATH
const sourceCandidates = buildCatalogSourceCandidates(configuredSource)
let rawCatalog = {
version: 1,
profiles: []
}
let sourceError = null
let source = sourceCandidates[sourceCandidates.length - 1]
try {
rawCatalog = await readCatalogSource(source)
} catch (error) {
sourceError = error
for(const candidate of sourceCandidates){
try {
rawCatalog = await readCatalogSource(candidate)
source = candidate
sourceError = null
break
} catch (error) {
source = candidate
sourceError = error
}
}
const rawProfiles = Array.isArray(rawCatalog.profiles) ? rawCatalog.profiles : []
@@ -191,7 +238,7 @@ exports.loadCatalog = async function(){
sourceError,
profiles: rawProfiles
.filter((profile) => profile != null && typeof profile.id === 'string' && typeof profile.name === 'string')
.map((profile) => normalizeProfile(profile, 'catalog'))
.map((profile) => normalizeProfile(profile, 'catalog', source))
.sort((left, right) => left.name.localeCompare(right.name, 'ko'))
}
}

View File

@@ -476,7 +476,11 @@ exports.getLibraryCatalogSource = function(){
* @param {string|null} source A remote URL or local path.
*/
exports.setLibraryCatalogSource = function(source){
config.library.catalogSource = source
if(typeof source !== 'string' || source.trim().length === 0){
config.library.catalogSource = null
} else {
config.library.catalogSource = source.trim()
}
}
/**
@@ -998,3 +1002,22 @@ exports.getAllowPrerelease = function(def = false){
exports.setAllowPrerelease = function(allowPrerelease){
config.settings.launcher.allowPrerelease = allowPrerelease
}
/**
* Validate a library catalog source.
*
* @param {string} source A remote URL or local path.
* @returns {boolean} Whether the value is allowed.
*/
exports.validateLibraryCatalogSource = function(source){
const trimmedSource = typeof source === 'string' ? source.trim() : ''
if(trimmedSource.length === 0){
return true
}
if(trimmedSource.includes('://')){
return /^https?:\/\/\S+$/i.test(trimmedSource) || trimmedSource.startsWith('file://')
}
return true
}

View File

@@ -17,7 +17,6 @@ logger.info('Loading..')
// Load ConfigManager
ConfigManager.load()
CatalogManager.applyConfiguredProfile()
// Yuck!
// TODO Fix this
@@ -44,20 +43,32 @@ function onDistroLoad(data){
ipcRenderer.send('distributionIndexDone', data != null)
}
// Ensure Distribution is downloaded and cached.
DistroAPI.getDistribution()
.then(heliosDistro => {
logger.info('Loaded distribution index.')
async function bootstrapDistribution(){
try {
await CatalogManager.getInstalledProfiles()
} catch (err) {
logger.warn('Unable to refresh installed profiles from catalog at startup.', err)
}
onDistroLoad(heliosDistro)
})
.catch(err => {
logger.info('Failed to load an older version of the distribution index.')
logger.info('Application cannot run.')
logger.error(err)
CatalogManager.applyConfiguredProfile()
onDistroLoad(null)
})
// Ensure Distribution is downloaded and cached.
DistroAPI.getDistribution()
.then(heliosDistro => {
logger.info('Loaded distribution index.')
onDistroLoad(heliosDistro)
})
.catch(err => {
logger.info('Failed to load an older version of the distribution index.')
logger.info('Application cannot run.')
logger.error(err)
onDistroLoad(null)
})
}
bootstrapDistribution()
// Clean up temp dir incase previous launches ended unexpectedly.
fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => {

View File

@@ -6,6 +6,7 @@ const {
ensureJavaDirIsRoot
} = require('helios-core/java')
const CatalogManager = require('./assets/js/catalogmanager')
const DropinModUtil = require('./assets/js/dropinmodutil')
const { MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./assets/js/ipcconstants')
const settingsBackButton = document.getElementById('settingsBackButton')
@@ -326,22 +327,45 @@ function settingsSaveDisabled(v){
settingsNavDone.disabled = v
}
function fullSettingsSave() {
async function refreshCatalogSourceViews(){
await CatalogManager.getInstalledProfiles()
CatalogManager.applyConfiguredProfile()
if(typeof refreshInstallView === 'function'){
await refreshInstallView()
}
if(typeof refreshLibraryView === 'function'){
await refreshLibraryView()
}
if(typeof refreshSelectedProfileButton === 'function'){
refreshSelectedProfileButton()
}
if(typeof refreshServerStatus === 'function'){
await refreshServerStatus(true)
}
}
async function fullSettingsSave() {
const previousCatalogSource = ConfigManager.getLibraryCatalogSource()
saveSettingsValues()
saveModConfiguration()
ConfigManager.save()
saveDropinModConfiguration()
saveShaderpackSettings()
if(previousCatalogSource !== ConfigManager.getLibraryCatalogSource()){
await refreshCatalogSourceViews()
}
}
/* Closes the settings view and saves all data. */
settingsNavDone.onclick = () => {
fullSettingsSave()
settingsNavDone.onclick = async () => {
await fullSettingsSave()
switchView(getCurrentView(), VIEWS.landing)
}
settingsBackButton.onclick = () => {
fullSettingsSave()
settingsBackButton.onclick = async () => {
await fullSettingsSave()
switchView(getCurrentView(), VIEWS.landing)
}

View File

@@ -102,6 +102,9 @@ launcherTabHeaderText = "Launcher Settings"
launcherTabHeaderDesc = "Options related to the launcher itself."
allowPrereleaseTitle = "Allow Pre-Release Updates."
allowPrereleaseDesc = "Pre-Releases include new features which may have not been fully tested or integrated.<br>This will always be true if you are using a pre-release version."
catalogSourceTitle = "Catalog URL"
catalogSourcePlaceholder = "https://your-domain.example/catalog.json"
catalogSourceDesc = "Leave blank to use the automatic default catalog source.<br>For remote operation, enter the catalog.json URL served by the admin site."
dataDirectoryTitle = "Data Directory"
selectDataDirectory = "Select Data Directory"
chooseFolder = "Choose Folder"

View File

@@ -102,6 +102,9 @@ launcherTabHeaderText = "런처 설정"
launcherTabHeaderDesc = "런처와 관련된 설정입니다."
allowPrereleaseTitle = "프리릴리즈 업데이트를 허용합니다."
allowPrereleaseDesc = "프리릴리즈는 안정성이 보장되지 않은 기능을 포함할 수 있습니다.<br>현재 실행 중인 런처가 프리릴리즈 버전이라면, 이 설정은 항상 활성화됩니다."
catalogSourceTitle = "카탈로그 주소"
catalogSourcePlaceholder = "https://your-domain.example/catalog.json"
catalogSourceDesc = "비워두면 기본 카탈로그를 자동으로 찾습니다.<br>원격 운영 시에는 관리자 사이트가 제공하는 catalog.json URL을 입력하세요."
dataDirectoryTitle = "데이터 디렉토리"
selectDataDirectory = "데이터 디렉토리 선택"
chooseFolder = "폴더 선택"