Refactor launcher profiles and port automation
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 21:52:17 +09:00
parent e266387784
commit 9786cfe031
22 changed files with 1558 additions and 798 deletions

View File

@@ -5,8 +5,7 @@
// Requirements
const { URL } = require('url')
const {
MojangRestAPI,
getServerStatus
MojangRestAPI
} = require('helios-core/mojang')
const {
RestResponseStatus,
@@ -32,7 +31,9 @@ const {
const AuthManager = require('./assets/js/authmanager')
const CatalogManager = require('./assets/js/catalogmanager')
const DiscordWrapper = require('./assets/js/discordwrapper')
const PortManager = require('./assets/js/portmanager')
const ProcessBuilder = require('./assets/js/processbuilder')
const ServerRuntime = require('./assets/js/serverruntime')
// Launch Elements
const launch_content = document.getElementById('launch_content')
@@ -47,6 +48,7 @@ const avatarContainer = document.getElementById('avatarContainer')
const accountMenu = document.getElementById('accountMenu')
const accountMenuName = document.getElementById('accountMenuName')
const accountMenuLogoutButton = document.getElementById('accountMenuLogoutButton')
const portStatusTooltip = document.getElementById('portStatusTooltip')
const loggerLanding = LoggerUtil.getLogger('Landing')
@@ -188,7 +190,7 @@ function refreshSelectedProfileButton(){
}
function isSelectedMapReady(profile){
if(profile == null || profile.kind !== 'map'){
if(profile == null || profile.serverEnabled === true){
return false
}
@@ -200,6 +202,24 @@ function isSelectedMapReady(profile){
)
}
function updateLandingStatusDisplay(label, value, tone, tooltip, fade = false){
const applyValues = () => {
document.getElementById('landingPlayerLabel').innerHTML = label
document.getElementById('player_count').innerHTML = value
document.getElementById('player_count').dataset.tone = tone
portStatusTooltip.textContent = tooltip
}
if(fade){
$('#server_status_wrapper').fadeOut(250, () => {
applyValues()
$('#server_status_wrapper').fadeIn(500)
})
} else {
applyValues()
}
}
// Bind launch button
document.getElementById('launch_button').addEventListener('click', async e => {
loggerLanding.info('Launching game..')
@@ -394,70 +414,45 @@ const refreshServerStatus = async (fade = false) => {
let pLabel = Lang.queryJS('landing.profileStatus.label')
let pVal = Lang.queryJS('landing.selectedProfile.noSelection')
let pTone = 'info'
let tooltip = '라이브러리에서 실행할 프로필을 먼저 선택하세요.'
if(selectedProfile == null){
if(fade){
$('#server_status_wrapper').fadeOut(250, () => {
document.getElementById('landingPlayerLabel').innerHTML = pLabel
document.getElementById('player_count').innerHTML = pVal
$('#server_status_wrapper').fadeIn(500)
})
} else {
document.getElementById('landingPlayerLabel').innerHTML = pLabel
document.getElementById('player_count').innerHTML = pVal
}
updateLandingStatusDisplay(pLabel, pVal, pTone, tooltip, fade)
return
}
if(selectedProfile.kind === 'map'){
if(selectedProfile.serverEnabled !== true){
pLabel = Lang.queryJS('landing.mapStatus.label')
pVal = isSelectedMapReady(selectedProfile)
? Lang.queryJS('landing.mapStatus.ready')
: Lang.queryJS('landing.mapStatus.notReady')
if(fade){
$('#server_status_wrapper').fadeOut(250, () => {
document.getElementById('landingPlayerLabel').innerHTML = pLabel
document.getElementById('player_count').innerHTML = pVal
$('#server_status_wrapper').fadeIn(500)
})
} else {
document.getElementById('landingPlayerLabel').innerHTML = pLabel
document.getElementById('player_count').innerHTML = pVal
}
pTone = selectedProfile.launchReady ? 'success' : 'error'
tooltip = selectedProfile.launchReady
? '이 프로필은 맵 실행 준비가 끝났습니다.'
: (selectedProfile.launchIssues?.join(' / ') || '맵 실행 준비가 아직 끝나지 않았습니다.')
updateLandingStatusDisplay(pLabel, pVal, pTone, tooltip, fade)
return
}
pLabel = Lang.queryJS('landing.serverStatus.server')
pVal = Lang.queryJS('landing.serverStatus.offline')
try {
const distro = await DistroAPI.getDistribution()
const serv = distro?.getServerById(ConfigManager.getSelectedServer())
?? (typeof distro?.getMainServer === 'function' ? distro.getMainServer() : null)
if(serv == null){
throw new Error('No server available for selected profile.')
}
const servStat = await getServerStatus(47, serv.hostname, serv.port)
pLabel = Lang.queryJS('landing.serverStatus.players')
pVal = servStat.players.online + '/' + servStat.players.max
pLabel = Lang.queryJS('landing.portStatus.label')
const portState = await PortManager.ensurePortAvailability(selectedProfile)
pVal = portState.summary
pTone = portState.tone
tooltip = selectedProfile.hostIssues?.length > 0
? `${portState.message} / ${selectedProfile.hostIssues.join(' / ')}`
: portState.message
} catch (err) {
loggerLanding.warn('Unable to refresh server status, assuming offline.')
loggerLanding.warn('Unable to refresh port status.')
loggerLanding.debug(err)
pLabel = Lang.queryJS('landing.portStatus.label')
pVal = Lang.queryJS('landing.portStatus.failed')
pTone = 'error'
tooltip = err instanceof Error ? err.message : '자동 포트 개방 상태를 확인하지 못했습니다.'
}
if(fade){
$('#server_status_wrapper').fadeOut(250, () => {
document.getElementById('landingPlayerLabel').innerHTML = pLabel
document.getElementById('player_count').innerHTML = pVal
$('#server_status_wrapper').fadeIn(500)
})
} else {
document.getElementById('landingPlayerLabel').innerHTML = pLabel
document.getElementById('player_count').innerHTML = pVal
}
updateLandingStatusDisplay(pLabel, pVal, pTone, tooltip, fade)
}
refreshMojangStatuses()
@@ -468,6 +463,10 @@ let mojangStatusListener = setInterval(() => refreshMojangStatuses(true), 60*60*
// Set refresh rate to once every 5 minutes.
let serverStatusListener = setInterval(() => refreshServerStatus(true), 300000)
window.addEventListener('beforeunload', () => {
PortManager.cleanupAll().catch(() => {})
})
/**
* Shows an error overlay, toggles off the launch area.
*
@@ -747,10 +746,34 @@ async function dlAsync(login = true) {
if(login) {
const authUser = ConfigManager.getSelectedAccount()
const selectedProfile = CatalogManager.getSelectedProfileSync()
loggerLaunchSuite.info(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`)
let pb = new ProcessBuilder(serv, versionData, modLoaderData, authUser, remote.app.getVersion())
setLaunchDetails(Lang.queryJS('landing.dlAsync.launchingGame'))
if(selectedProfile?.serverEnabled === true && CatalogManager.shouldHostLocally(selectedProfile)){
if(!selectedProfile.hostReady){
showLaunchFailure(
Lang.queryJS('landing.localServer.missingJarTitle'),
Lang.queryJS('landing.localServer.missingJarText')
)
return
}
setLaunchDetails(Lang.queryJS('landing.localServer.starting'))
try {
await ServerRuntime.startHostedProfile(selectedProfile)
refreshServerStatus(true)
} catch (error) {
loggerLaunchSuite.error('Failed to start local server.', error)
showLaunchFailure(
Lang.queryJS('landing.localServer.failureTitle'),
error instanceof Error ? error.message : Lang.queryJS('landing.localServer.failureText')
)
return
}
}
// const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/
const SERVER_JOINED_REGEX = new RegExp(`\\[.+\\]: \\[CHAT\\] ${authUser.displayName} joined the game`)