2018-04-25 17:11:10 -07:00
/ * *
* Script for landing . ejs
* /
2018-04-25 14:40:46 -07:00
// Requirements
2018-04-25 23:39:47 -07:00
const cp = require ( 'child_process' )
2018-06-04 20:08:03 -07:00
const crypto = require ( 'crypto' )
2021-10-30 23:20:03 -07:00
const { URL } = require ( 'url' )
2023-02-25 00:09:22 -08:00
const {
MojangRestAPI ,
getServerStatus
} = require ( 'helios-core/mojang' )
const {
RestResponseStatus ,
isDisplayableError ,
2023-03-17 23:49:10 -07:00
validateLocalFile
2023-02-25 00:09:22 -08:00
} = require ( 'helios-core/common' )
const {
FullRepair ,
DistributionIndexProcessor ,
2023-03-17 23:49:10 -07:00
MojangIndexProcessor ,
downloadFile
2023-02-25 00:09:22 -08:00
} = require ( 'helios-core/dl' )
2023-03-12 23:06:58 -07:00
const {
validateSelectedJvm ,
ensureJavaDirIsRoot ,
javaExecFromRoot ,
2023-03-17 23:49:10 -07:00
discoverBestJvmInstallation ,
latestOpenJDK ,
extractJdk
2023-03-12 23:06:58 -07:00
} = require ( 'helios-core/java' )
2018-04-25 14:40:46 -07:00
// Internal Requirements
2018-07-22 08:40:15 -07:00
const DiscordWrapper = require ( './assets/js/discordwrapper' )
const ProcessBuilder = require ( './assets/js/processbuilder' )
2018-04-25 14:40:46 -07:00
// Launch Elements
2018-04-25 23:39:47 -07:00
const launch _content = document . getElementById ( 'launch_content' )
const launch _details = document . getElementById ( 'launch_details' )
const launch _progress = document . getElementById ( 'launch_progress' )
const launch _progress _label = document . getElementById ( 'launch_progress_label' )
const launch _details _text = document . getElementById ( 'launch_details_text' )
const server _selection _button = document . getElementById ( 'server_selection_button' )
2018-04-29 15:05:59 -07:00
const user _text = document . getElementById ( 'user_text' )
2018-04-25 14:40:46 -07:00
2022-12-24 19:17:30 -08:00
const loggerLanding = LoggerUtil . getLogger ( 'Landing' )
2018-08-22 11:21:49 -07:00
2018-04-25 17:11:10 -07:00
/* Launch Progress Wrapper Functions */
/ * *
* Show / hide the loading area .
*
* @ param { boolean } loading True if the loading area should be shown , otherwise false .
* /
function toggleLaunchArea ( loading ) {
if ( loading ) {
launch _details . style . display = 'flex'
launch _content . style . display = 'none'
} else {
launch _details . style . display = 'none'
launch _content . style . display = 'inline-flex'
}
}
/ * *
* Set the details text of the loading area .
*
* @ param { string } details The new text for the loading details .
* /
function setLaunchDetails ( details ) {
launch _details _text . innerHTML = details
}
/ * *
* Set the value of the loading progress bar and display that value .
*
2023-02-25 00:09:22 -08:00
* @ param { number } percent Percentage ( 0 - 100 )
2018-04-25 17:11:10 -07:00
* /
2023-02-25 00:09:22 -08:00
function setLaunchPercentage ( percent ) {
launch _progress . setAttribute ( 'max' , 100 )
launch _progress . setAttribute ( 'value' , percent )
2018-04-25 17:11:10 -07:00
launch _progress _label . innerHTML = percent + '%'
}
/ * *
* Set the value of the OS progress bar and display that on the UI .
*
2023-02-25 00:09:22 -08:00
* @ param { number } percent Percentage ( 0 - 100 )
2018-04-25 17:11:10 -07:00
* /
2023-02-25 00:09:22 -08:00
function setDownloadPercentage ( percent ) {
remote . getCurrentWindow ( ) . setProgressBar ( percent / 100 )
setLaunchPercentage ( percent )
2018-04-25 17:11:10 -07:00
}
2018-04-26 15:41:26 -07:00
/ * *
* Enable or disable the launch button .
*
* @ param { boolean } val True to enable , false to disable .
* /
function setLaunchEnabled ( val ) {
document . getElementById ( 'launch_button' ) . disabled = ! val
}
2018-04-25 14:40:46 -07:00
// Bind launch button
2023-03-17 23:49:10 -07:00
document . getElementById ( 'launch_button' ) . addEventListener ( 'click' , async e => {
2022-12-24 19:17:30 -08:00
loggerLanding . info ( 'Launching game..' )
2023-03-19 16:34:35 -07:00
try {
const server = ( await DistroAPI . getDistribution ( ) ) . getServerById ( ConfigManager . getSelectedServer ( ) )
const jExe = ConfigManager . getJavaExecutable ( ConfigManager . getSelectedServer ( ) )
if ( jExe == null ) {
await asyncSystemScan ( server . effectiveJavaOptions )
} else {
2018-04-25 14:40:46 -07:00
2023-03-19 16:34:35 -07:00
setLaunchDetails ( Lang . queryJS ( 'landing.launch.pleaseWait' ) )
toggleLaunchArea ( true )
setLaunchPercentage ( 0 , 100 )
2018-04-25 14:40:46 -07:00
2023-03-19 16:34:35 -07:00
const details = await validateSelectedJvm ( ensureJavaDirIsRoot ( jExe ) , server . effectiveJavaOptions . supported )
if ( details != null ) {
loggerLanding . info ( 'Jvm Details' , details )
await dlAsync ( )
} else {
await asyncSystemScan ( server . effectiveJavaOptions )
}
2023-03-12 23:06:58 -07:00
}
2023-03-19 16:34:35 -07:00
} catch ( err ) {
loggerLanding . error ( 'Unhandled error in during launch process.' , err )
showLaunchFailure ( 'Error During Launch' , 'See console (CTRL + Shift + i) for more details.' )
2018-04-25 14:40:46 -07:00
}
} )
2018-05-30 13:00:07 -07:00
// Bind settings button
2023-02-24 18:02:18 -08:00
document . getElementById ( 'settingsMediaButton' ) . onclick = async e => {
await prepareSettings ( )
2018-05-30 13:00:07 -07:00
switchView ( getCurrentView ( ) , VIEWS . settings )
}
2018-06-21 09:38:21 -07:00
// Bind avatar overlay button.
2023-02-24 18:02:18 -08:00
document . getElementById ( 'avatarOverlay' ) . onclick = async e => {
await prepareSettings ( )
2018-06-21 09:38:21 -07:00
switchView ( getCurrentView ( ) , VIEWS . settings , 500 , 500 , ( ) => {
settingsNavItemListener ( document . getElementById ( 'settingsNavAccount' ) , false )
} )
}
2018-04-29 15:05:59 -07:00
// Bind selected account
function updateSelectedAccount ( authUser ) {
let username = 'No Account Selected'
2018-07-01 23:01:28 -07:00
if ( authUser != null ) {
if ( authUser . displayName != null ) {
username = authUser . displayName
}
if ( authUser . uuid != null ) {
2021-07-19 08:08:55 -07:00
document . getElementById ( 'avatarContainer' ) . style . backgroundImage = ` url('https://mc-heads.net/body/ ${ authUser . uuid } /right') `
2018-07-01 23:01:28 -07:00
}
2018-04-29 15:05:59 -07:00
}
user _text . innerHTML = username
}
updateSelectedAccount ( ConfigManager . getSelectedAccount ( ) )
2018-04-25 14:40:46 -07:00
// Bind selected server
2018-11-20 02:19:59 -08:00
function updateSelectedServer ( serv ) {
if ( getCurrentView ( ) === VIEWS . settings ) {
2022-11-27 15:13:50 -08:00
fullSettingsSave ( )
2018-04-26 15:41:26 -07:00
}
2023-02-24 18:02:18 -08:00
ConfigManager . setSelectedServer ( serv != null ? serv . rawServer . id : null )
2018-11-20 02:19:59 -08:00
ConfigManager . save ( )
2023-02-24 18:02:18 -08:00
server _selection _button . innerHTML = '\u2022 ' + ( serv != null ? serv . rawServer . name : 'No Server Selected' )
2018-08-06 21:58:32 -07:00
if ( getCurrentView ( ) === VIEWS . settings ) {
2022-11-27 15:13:50 -08:00
animateSettingsTabRefresh ( )
2018-08-06 21:58:32 -07:00
}
2018-11-20 02:19:59 -08:00
setLaunchEnabled ( serv != null )
2018-04-26 15:41:26 -07:00
}
2018-05-06 22:34:57 -07:00
// Real text is set in uibinder.js on distributionIndexDone.
2018-11-20 02:19:59 -08:00
server _selection _button . innerHTML = '\u2022 Loading..'
2023-02-24 18:02:18 -08:00
server _selection _button . onclick = async e => {
2018-04-25 23:39:47 -07:00
e . target . blur ( )
2023-02-24 18:02:18 -08:00
await toggleServerSelection ( true )
2018-08-22 07:54:09 -07:00
}
2018-04-25 14:40:46 -07:00
// Update Mojang Status Color
const refreshMojangStatuses = async function ( ) {
2022-12-24 19:17:30 -08:00
loggerLanding . info ( 'Refreshing Mojang Statuses..' )
2018-05-17 00:11:44 -07:00
2018-04-25 14:40:46 -07:00
let status = 'grey'
2018-07-22 10:31:15 -07:00
let tooltipEssentialHTML = ''
let tooltipNonEssentialHTML = ''
2018-05-17 00:11:44 -07:00
2022-02-06 15:23:44 -08:00
const response = await MojangRestAPI . status ( )
let statuses
if ( response . responseStatus === RestResponseStatus . SUCCESS ) {
statuses = response . data
} else {
loggerLanding . warn ( 'Unable to refresh Mojang service status.' )
statuses = MojangRestAPI . getDefaultStatuses ( )
}
greenCount = 0
greyCount = 0
for ( let i = 0 ; i < statuses . length ; i ++ ) {
const service = statuses [ i ]
if ( service . essential ) {
tooltipEssentialHTML += ` <div class="mojangStatusContainer">
< span class = "mojangStatusIcon" style = "color: ${MojangRestAPI.statusToHex(service.status)};" > & # 8226 ; < / s p a n >
< span class = "mojangStatusName" > $ { service . name } < / s p a n >
< / d i v > `
} else {
tooltipNonEssentialHTML += ` <div class="mojangStatusContainer">
< span class = "mojangStatusIcon" style = "color: ${MojangRestAPI.statusToHex(service.status)};" > & # 8226 ; < / s p a n >
< span class = "mojangStatusName" > $ { service . name } < / s p a n >
< / d i v > `
2018-04-25 14:40:46 -07:00
}
2018-05-17 00:11:44 -07:00
2022-02-06 15:23:44 -08:00
if ( service . status === 'yellow' && status !== 'red' ) {
status = 'yellow'
} else if ( service . status === 'red' ) {
status = 'red'
} else {
if ( service . status === 'grey' ) {
++ greyCount
2018-05-22 05:41:22 -07:00
}
2022-02-06 15:23:44 -08:00
++ greenCount
2018-04-25 14:40:46 -07:00
}
2018-05-17 00:11:44 -07:00
2022-02-06 15:23:44 -08:00
}
if ( greenCount === statuses . length ) {
if ( greyCount === statuses . length ) {
status = 'grey'
} else {
status = 'green'
}
2018-04-25 14:40:46 -07:00
}
2018-05-17 00:11:44 -07:00
document . getElementById ( 'mojangStatusEssentialContainer' ) . innerHTML = tooltipEssentialHTML
document . getElementById ( 'mojangStatusNonEssentialContainer' ) . innerHTML = tooltipNonEssentialHTML
2022-02-06 15:23:44 -08:00
document . getElementById ( 'mojang_status_icon' ) . style . color = MojangRestAPI . statusToHex ( status )
2018-04-25 14:40:46 -07:00
}
2023-02-24 18:02:18 -08:00
const refreshServerStatus = async ( fade = false ) => {
2022-12-24 19:17:30 -08:00
loggerLanding . info ( 'Refreshing Server Status' )
2023-02-24 18:02:18 -08:00
const serv = ( await DistroAPI . getDistribution ( ) ) . getServerById ( ConfigManager . getSelectedServer ( ) )
2018-04-25 14:40:46 -07:00
let pLabel = 'SERVER'
let pVal = 'OFFLINE'
try {
2021-10-30 23:20:03 -07:00
2023-02-24 18:02:18 -08:00
const servStat = await getServerStatus ( 47 , serv . hostname , serv . port )
2021-10-30 23:20:03 -07:00
console . log ( servStat )
pLabel = 'PLAYERS'
pVal = servStat . players . online + '/' + servStat . players . max
2018-04-25 14:40:46 -07:00
} catch ( err ) {
2018-08-22 11:21:49 -07:00
loggerLanding . warn ( 'Unable to refresh server status, assuming offline.' )
loggerLanding . debug ( err )
2018-04-25 14:40:46 -07:00
}
2018-04-26 15:41:26 -07:00
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
}
2018-04-25 14:40:46 -07:00
}
refreshMojangStatuses ( )
2018-05-06 22:34:57 -07:00
// Server Status is refreshed in uibinder.js on distributionIndexDone.
2018-04-25 14:40:46 -07:00
2022-11-06 19:38:15 -08:00
// Refresh statuses every hour. The status page itself refreshes every day so...
let mojangStatusListener = setInterval ( ( ) => refreshMojangStatuses ( true ) , 60 * 60 * 1000 )
2018-04-25 14:40:46 -07:00
// Set refresh rate to once every 5 minutes.
2018-04-26 15:41:26 -07:00
let serverStatusListener = setInterval ( ( ) => refreshServerStatus ( true ) , 300000 )
2018-04-25 14:40:46 -07:00
2019-01-20 03:55:13 -08:00
/ * *
* Shows an error overlay , toggles off the launch area .
*
* @ param { string } title The overlay title .
* @ param { string } desc The overlay description .
* /
function showLaunchFailure ( title , desc ) {
setOverlayContent (
title ,
desc ,
'Okay'
)
setOverlayHandler ( null )
toggleOverlay ( true )
toggleLaunchArea ( false )
}
2018-04-25 14:40:46 -07:00
/* System (Java) Scan */
2018-12-31 07:39:27 -08:00
/ * *
* Asynchronously scan the system for valid Java installations .
*
* @ param { boolean } launchAfter Whether we should begin to launch after scanning .
* /
2023-03-17 23:49:10 -07:00
async function asyncSystemScan ( effectiveJavaOptions , launchAfter = true ) {
2018-04-25 14:40:46 -07:00
2023-03-12 23:06:58 -07:00
setLaunchDetails ( 'Checking system info..' )
2018-04-25 14:40:46 -07:00
toggleLaunchArea ( true )
setLaunchPercentage ( 0 , 100 )
2023-03-12 23:06:58 -07:00
const jvmDetails = await discoverBestJvmInstallation (
ConfigManager . getDataDirectory ( ) ,
2023-03-17 23:49:10 -07:00
effectiveJavaOptions . supported
2023-03-12 23:06:58 -07:00
)
if ( jvmDetails == null ) {
// If the result is null, no valid Java installation was found.
// Show this information to the user.
setOverlayContent (
'No Compatible<br>Java Installation Found' ,
2023-03-17 23:49:10 -07:00
` In order to join WesterosCraft, you need a 64-bit installation of Java ${ effectiveJavaOptions . suggestedMajor } . Would you like us to install a copy? ` ,
2023-03-12 23:06:58 -07:00
'Install Java' ,
'Install Manually'
)
setOverlayHandler ( ( ) => {
setLaunchDetails ( 'Preparing Java Download..' )
toggleOverlay ( false )
2023-03-19 16:34:35 -07:00
try {
downloadJava ( effectiveJavaOptions , launchAfter )
} catch ( err ) {
loggerLanding . error ( 'Unhandled error in Java Download' , err )
showLaunchFailure ( 'Error During Java Download' , 'See console (CTRL + Shift + i) for more details.' )
}
2023-03-12 23:06:58 -07:00
} )
setDismissHandler ( ( ) => {
$ ( '#overlayContent' ) . fadeOut ( 250 , ( ) => {
//$('#overlayDismiss').toggle(false)
2018-04-25 14:40:46 -07:00
setOverlayContent (
2023-03-12 23:06:58 -07:00
'Java is Required<br>to Launch' ,
2023-03-17 23:49:10 -07:00
` A valid x64 installation of Java ${ effectiveJavaOptions . suggestedMajor } is required to launch.<br><br>Please refer to our <a href="https://github.com/dscalzi/HeliosLauncher/wiki/Java-Management#manually-installing-a-valid-version-of-java">Java Management Guide</a> for instructions on how to manually install Java. ` ,
2023-03-12 23:06:58 -07:00
'I Understand' ,
'Go Back'
2018-04-25 14:40:46 -07:00
)
setOverlayHandler ( ( ) => {
2023-03-12 23:06:58 -07:00
toggleLaunchArea ( false )
2018-04-25 14:40:46 -07:00
toggleOverlay ( false )
} )
setDismissHandler ( ( ) => {
2023-03-12 23:06:58 -07:00
toggleOverlay ( false , true )
2018-04-25 14:40:46 -07:00
2023-03-17 23:49:10 -07:00
asyncSystemScan ( effectiveJavaOptions , launchAfter )
2018-04-25 14:40:46 -07:00
} )
2023-03-12 23:06:58 -07:00
$ ( '#overlayContent' ) . fadeIn ( 250 )
} )
} )
toggleOverlay ( true , true )
} else {
// Java installation found, use this to launch the game.
const javaExec = javaExecFromRoot ( jvmDetails . path )
ConfigManager . setJavaExecutable ( ConfigManager . getSelectedServer ( ) , javaExec )
ConfigManager . save ( )
// We need to make sure that the updated value is on the settings UI.
// Just incase the settings UI is already open.
settingsJavaExecVal . value = javaExec
await populateJavaExecDetails ( settingsJavaExecVal . value )
2023-03-17 23:49:10 -07:00
// TODO Callback hell, refactor
2023-03-12 23:06:58 -07:00
// TODO Move this out, separate concerns.
if ( launchAfter ) {
await dlAsync ( )
2018-04-25 14:40:46 -07:00
}
2023-03-12 23:06:58 -07:00
}
2023-03-17 23:49:10 -07:00
}
async function downloadJava ( effectiveJavaOptions , launchAfter = true ) {
// TODO Error handling.
// asset can be null.
const asset = await latestOpenJDK (
effectiveJavaOptions . suggestedMajor ,
ConfigManager . getDataDirectory ( ) ,
effectiveJavaOptions . distribution )
2023-03-19 16:34:35 -07:00
if ( asset == null ) {
throw new Error ( 'Failed to find OpenJDK distribution.' )
}
2023-03-17 23:49:10 -07:00
let received = 0
await downloadFile ( asset . url , asset . path , ( { transferred } ) => {
2023-03-18 00:05:37 -07:00
received = transferred
setDownloadPercentage ( Math . trunc ( ( transferred / asset . size ) * 100 ) )
2023-03-17 23:49:10 -07:00
} )
setDownloadPercentage ( 100 )
if ( received != asset . size ) {
loggerLanding . warn ( ` Java Download: Expected ${ asset . size } bytes but received ${ received } ` )
if ( ! await validateLocalFile ( asset . path , asset . algo , asset . hash ) ) {
log . error ( ` Hashes do not match, ${ asset . id } may be corrupted. ` )
2023-03-19 16:34:35 -07:00
// Don't know how this could happen, but report it.
throw new Error ( 'Downloaded JDK has bad hash, file may be corrupted.' )
2023-03-17 23:49:10 -07:00
}
}
// Extract
// Show installing progress bar.
remote . getCurrentWindow ( ) . setProgressBar ( 2 )
// Wait for extration to complete.
2023-03-18 00:05:37 -07:00
const eLStr = 'Extracting Java'
2023-03-17 23:49:10 -07:00
let dotStr = ''
setLaunchDetails ( eLStr )
const extractListener = setInterval ( ( ) => {
if ( dotStr . length >= 3 ) {
dotStr = ''
} else {
dotStr += '.'
}
setLaunchDetails ( eLStr + dotStr )
} , 750 )
2023-03-18 00:05:37 -07:00
const newJavaExec = await extractJdk ( asset . path )
2023-03-17 23:49:10 -07:00
// Extraction complete, remove the loading from the OS progress bar.
remote . getCurrentWindow ( ) . setProgressBar ( - 1 )
// Extraction completed successfully.
ConfigManager . setJavaExecutable ( ConfigManager . getSelectedServer ( ) , newJavaExec )
ConfigManager . save ( )
clearInterval ( extractListener )
setLaunchDetails ( 'Java Installed!' )
// TODO Callback hell
// Refactor the launch functions
asyncSystemScan ( effectiveJavaOptions , launchAfter )
2018-04-25 14:40:46 -07:00
}
// Keep reference to Minecraft Process
let proc
// Is DiscordRPC enabled
let hasRPC = false
// Joined server regex
2020-06-29 08:28:55 -07:00
// Change this if your server uses something different.
const GAME _JOINED _REGEX = /\[.+\]: Sound engine started/
2020-06-08 11:00:07 -07:00
const GAME _LAUNCH _REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+)$/
const MIN _LINGER = 5000
2018-04-25 14:40:46 -07:00
2023-02-25 00:09:22 -08:00
async function dlAsync ( login = true ) {
2018-04-25 14:40:46 -07:00
// Login parameter is temporary for debug purposes. Allows testing the validation/downloads without
// launching the game.
2023-02-25 00:09:22 -08:00
const loggerLaunchSuite = LoggerUtil . getLogger ( 'LaunchSuite' )
setLaunchDetails ( 'Loading server information..' )
let distro
try {
distro = await DistroAPI . refreshDistributionOrFallback ( )
onDistroRefresh ( distro )
} catch ( err ) {
loggerLaunchSuite . error ( 'Unable to refresh distribution index.' , err )
showLaunchFailure ( 'Fatal Error' , 'Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details.' )
return
}
const serv = distro . getServerById ( ConfigManager . getSelectedServer ( ) )
2018-04-25 14:40:46 -07:00
if ( login ) {
if ( ConfigManager . getSelectedAccount ( ) == null ) {
2018-08-22 11:21:49 -07:00
loggerLanding . error ( 'You must be logged into an account.' )
2018-04-25 14:40:46 -07:00
return
}
}
setLaunchDetails ( 'Please wait..' )
toggleLaunchArea ( true )
setLaunchPercentage ( 0 , 100 )
2023-02-25 00:09:22 -08:00
const fullRepairModule = new FullRepair (
ConfigManager . getCommonDirectory ( ) ,
2023-02-25 00:28:40 -08:00
ConfigManager . getInstanceDirectory ( ) ,
2023-02-25 00:09:22 -08:00
ConfigManager . getLauncherDirectory ( ) ,
ConfigManager . getSelectedServer ( ) ,
DistroAPI . isDevMode ( )
)
2018-08-22 11:21:49 -07:00
2023-02-25 00:09:22 -08:00
fullRepairModule . spawnReceiver ( )
2019-01-04 11:25:27 -08:00
2023-02-25 00:09:22 -08:00
fullRepairModule . childProcess . on ( 'error' , ( err ) => {
2019-01-20 03:55:13 -08:00
loggerLaunchSuite . error ( 'Error during launch' , err )
showLaunchFailure ( 'Error During Launch' , err . message || 'See console (CTRL + Shift + i) for more details.' )
} )
2023-02-25 00:09:22 -08:00
fullRepairModule . childProcess . on ( 'close' , ( code , _signal ) => {
2019-01-20 03:55:13 -08:00
if ( code !== 0 ) {
2023-03-17 23:49:10 -07:00
loggerLaunchSuite . error ( ` Full Repair Module exited with code ${ code } , assuming error. ` )
2019-01-20 03:55:13 -08:00
showLaunchFailure ( 'Error During Launch' , 'See console (CTRL + Shift + i) for more details.' )
}
} )
2018-04-25 14:40:46 -07:00
2023-02-25 00:09:22 -08:00
loggerLaunchSuite . info ( 'Validating files.' )
setLaunchDetails ( 'Validating file integrity..' )
2023-03-19 16:34:35 -07:00
let invalidFileCount = 0
try {
invalidFileCount = await fullRepairModule . verifyFiles ( percent => {
setLaunchPercentage ( percent )
} )
setLaunchPercentage ( 100 )
} catch ( err ) {
loggerLaunchSuite . error ( 'Error during file validation.' )
showLaunchFailure ( 'Error During File Verification' , err . displayable || 'See console (CTRL + Shift + i) for more details.' )
return
}
2018-04-25 14:40:46 -07:00
2023-02-25 00:09:22 -08:00
if ( invalidFileCount > 0 ) {
loggerLaunchSuite . info ( 'Downloading files.' )
setLaunchDetails ( 'Downloading files..' )
2023-03-24 12:46:37 -07:00
setLaunchPercentage ( 0 )
2023-03-19 16:34:35 -07:00
try {
await fullRepairModule . download ( percent => {
setDownloadPercentage ( percent )
} )
setDownloadPercentage ( 100 )
} catch ( err ) {
loggerLaunchSuite . error ( 'Error during file download.' )
showLaunchFailure ( 'Error During File Download' , err . displayable || 'See console (CTRL + Shift + i) for more details.' )
return
}
2023-02-25 00:09:22 -08:00
} else {
loggerLaunchSuite . info ( 'No invalid files, skipping download.' )
}
2018-05-08 03:34:16 -07:00
2023-02-25 00:09:22 -08:00
// Remove download bar.
remote . getCurrentWindow ( ) . setProgressBar ( - 1 )
2018-04-25 14:40:46 -07:00
2023-02-25 00:09:22 -08:00
fullRepairModule . destroyReceiver ( )
2018-04-25 14:40:46 -07:00
2023-02-25 00:09:22 -08:00
setLaunchDetails ( 'Preparing to launch..' )
2019-02-18 03:31:01 -08:00
2023-02-25 00:09:22 -08:00
const mojangIndexProcessor = new MojangIndexProcessor (
ConfigManager . getCommonDirectory ( ) ,
serv . rawServer . minecraftVersion )
const distributionIndexProcessor = new DistributionIndexProcessor (
ConfigManager . getCommonDirectory ( ) ,
distro ,
serv . rawServer . id
)
2019-02-18 03:31:01 -08:00
2023-02-25 00:09:22 -08:00
const forgeData = await distributionIndexProcessor . loadForgeVersionJson ( serv )
const versionData = await mojangIndexProcessor . getVersionJson ( )
2019-02-18 03:31:01 -08:00
2023-02-25 00:09:22 -08:00
if ( login ) {
const authUser = ConfigManager . getSelectedAccount ( )
loggerLaunchSuite . info ( ` Sending selected account ( ${ authUser . displayName } ) to ProcessBuilder. ` )
let pb = new ProcessBuilder ( serv , versionData , forgeData , authUser , remote . app . getVersion ( ) )
setLaunchDetails ( 'Launching game..' )
// 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 ` )
const onLoadComplete = ( ) => {
toggleLaunchArea ( false )
if ( hasRPC ) {
DiscordWrapper . updateDetails ( 'Loading game..' )
2023-03-18 19:07:22 -07:00
proc . stdout . on ( 'data' , gameStateChange )
2018-07-25 19:30:47 -07:00
}
2023-02-25 00:09:22 -08:00
proc . stdout . removeListener ( 'data' , tempListener )
proc . stderr . removeListener ( 'data' , gameErrorListener )
}
const start = Date . now ( )
// Attach a temporary listener to the client output.
// Will wait for a certain bit of text meaning that
// the client application has started, and we can hide
// the progress bar stuff.
const tempListener = function ( data ) {
if ( GAME _LAUNCH _REGEX . test ( data . trim ( ) ) ) {
const diff = Date . now ( ) - start
if ( diff < MIN _LINGER ) {
setTimeout ( onLoadComplete , MIN _LINGER - diff )
} else {
onLoadComplete ( )
2018-04-25 14:40:46 -07:00
}
}
2023-02-25 00:09:22 -08:00
}
2018-04-25 14:40:46 -07:00
2023-02-25 00:09:22 -08:00
// Listener for Discord RPC.
const gameStateChange = function ( data ) {
data = data . trim ( )
if ( SERVER _JOINED _REGEX . test ( data ) ) {
DiscordWrapper . updateDetails ( 'Exploring the Realm!' )
} else if ( GAME _JOINED _REGEX . test ( data ) ) {
DiscordWrapper . updateDetails ( 'Sailing to Westeros!' )
}
}
2018-04-25 14:40:46 -07:00
2023-02-25 00:09:22 -08:00
const gameErrorListener = function ( data ) {
data = data . trim ( )
if ( data . indexOf ( 'Could not find or load main class net.minecraft.launchwrapper.Launch' ) > - 1 ) {
loggerLaunchSuite . error ( 'Game launch failed, LaunchWrapper was not downloaded properly.' )
showLaunchFailure ( 'Error During Launch' , 'The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.<br><br>To fix this issue, temporarily turn off your antivirus software and launch the game again.<br><br>If you have time, please <a href="https://github.com/dscalzi/HeliosLauncher/issues">submit an issue</a> and let us know what antivirus software you use. We\'ll contact them and try to straighten things out.' )
}
2018-04-25 14:40:46 -07:00
}
2023-02-25 00:09:22 -08:00
try {
// Build Minecraft process.
proc = pb . build ( )
// Bind listeners to stdout.
proc . stdout . on ( 'data' , tempListener )
proc . stderr . on ( 'data' , gameErrorListener )
setLaunchDetails ( 'Done. Enjoy the server!' )
// Init Discord Hook
if ( distro . rawDistribution . discord != null && serv . rawServerdiscord != null ) {
DiscordWrapper . initRPC ( distro . rawDistribution . discord , serv . rawServer . discord )
hasRPC = true
proc . on ( 'close' , ( code , signal ) => {
loggerLaunchSuite . info ( 'Shutting down Discord Rich Presence..' )
DiscordWrapper . shutdownRPC ( )
hasRPC = false
proc = null
} )
}
} catch ( err ) {
2018-04-25 14:40:46 -07:00
2023-02-25 00:09:22 -08:00
loggerLaunchSuite . error ( 'Error during launch' , err )
showLaunchFailure ( 'Error During Launch' , 'Please check the console (CTRL + Shift + i) for more details.' )
2018-05-08 17:10:46 -07:00
2023-02-25 00:09:22 -08:00
}
}
2018-05-08 17:10:46 -07:00
2018-05-06 18:45:20 -07:00
}
/ * *
* News Loading Functions
* /
2018-06-20 06:12:44 -07:00
// DOM Cache
const newsContent = document . getElementById ( 'newsContent' )
const newsArticleTitle = document . getElementById ( 'newsArticleTitle' )
const newsArticleDate = document . getElementById ( 'newsArticleDate' )
const newsArticleAuthor = document . getElementById ( 'newsArticleAuthor' )
const newsArticleComments = document . getElementById ( 'newsArticleComments' )
const newsNavigationStatus = document . getElementById ( 'newsNavigationStatus' )
const newsArticleContentScrollable = document . getElementById ( 'newsArticleContentScrollable' )
const nELoadSpan = document . getElementById ( 'nELoadSpan' )
2018-05-06 18:45:20 -07:00
// News slide caches.
let newsActive = false
let newsGlideCount = 0
/ * *
* Show the news UI via a slide animation .
*
* @ param { boolean } up True to slide up , otherwise false .
* /
function slide _ ( up ) {
const lCUpper = document . querySelector ( '#landingContainer > #upper' )
const lCLLeft = document . querySelector ( '#landingContainer > #lower > #left' )
const lCLCenter = document . querySelector ( '#landingContainer > #lower > #center' )
const lCLRight = document . querySelector ( '#landingContainer > #lower > #right' )
const newsBtn = document . querySelector ( '#landingContainer > #lower > #center #content' )
const landingContainer = document . getElementById ( 'landingContainer' )
const newsContainer = document . querySelector ( '#landingContainer > #newsContainer' )
newsGlideCount ++
if ( up ) {
lCUpper . style . top = '-200vh'
lCLLeft . style . top = '-200vh'
lCLCenter . style . top = '-200vh'
lCLRight . style . top = '-200vh'
newsBtn . style . top = '130vh'
newsContainer . style . top = '0px'
//date.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric'})
//landingContainer.style.background = 'rgba(29, 29, 29, 0.55)'
landingContainer . style . background = 'rgba(0, 0, 0, 0.50)'
setTimeout ( ( ) => {
if ( newsGlideCount === 1 ) {
lCLCenter . style . transition = 'none'
newsBtn . style . transition = 'none'
}
newsGlideCount --
} , 2000 )
} else {
setTimeout ( ( ) => {
newsGlideCount --
} , 2000 )
landingContainer . style . background = null
lCLCenter . style . transition = null
newsBtn . style . transition = null
newsContainer . style . top = '100%'
lCUpper . style . top = '0px'
lCLLeft . style . top = '0px'
lCLCenter . style . top = '0px'
lCLRight . style . top = '0px'
newsBtn . style . top = '10px'
}
}
// Bind news button.
document . getElementById ( 'newsButton' ) . onclick = ( ) => {
2018-05-10 02:48:55 -07:00
// Toggle tabbing.
if ( newsActive ) {
2018-06-04 20:08:03 -07:00
$ ( '#landingContainer *' ) . removeAttr ( 'tabindex' )
$ ( '#newsContainer *' ) . attr ( 'tabindex' , '-1' )
2018-05-10 02:48:55 -07:00
} else {
2018-06-04 20:08:03 -07:00
$ ( '#landingContainer *' ) . attr ( 'tabindex' , '-1' )
$ ( '#newsContainer, #newsContainer *, #lower, #lower #center *' ) . removeAttr ( 'tabindex' )
if ( newsAlertShown ) {
$ ( '#newsButtonAlert' ) . fadeOut ( 2000 )
newsAlertShown = false
ConfigManager . setNewsCacheDismissed ( true )
ConfigManager . save ( )
}
2018-05-10 02:48:55 -07:00
}
2018-05-06 18:45:20 -07:00
slide _ ( ! newsActive )
newsActive = ! newsActive
}
// Array to store article meta.
let newsArr = null
// News load animation listener.
let newsLoadingListener = null
/ * *
* Set the news loading animation .
*
* @ param { boolean } val True to set loading animation , otherwise false .
* /
function setNewsLoading ( val ) {
if ( val ) {
const nLStr = 'Checking for News'
let dotStr = '..'
nELoadSpan . innerHTML = nLStr + dotStr
newsLoadingListener = setInterval ( ( ) => {
if ( dotStr . length >= 3 ) {
dotStr = ''
} else {
dotStr += '.'
}
nELoadSpan . innerHTML = nLStr + dotStr
} , 750 )
} else {
if ( newsLoadingListener != null ) {
clearInterval ( newsLoadingListener )
newsLoadingListener = null
}
}
}
// Bind retry button.
newsErrorRetry . onclick = ( ) => {
$ ( '#newsErrorFailed' ) . fadeOut ( 250 , ( ) => {
initNews ( )
$ ( '#newsErrorLoading' ) . fadeIn ( 250 )
} )
}
2018-06-20 06:12:44 -07:00
newsArticleContentScrollable . onscroll = ( e ) => {
if ( e . target . scrollTop > Number . parseFloat ( $ ( '.newsArticleSpacerTop' ) . css ( 'height' ) ) ) {
newsContent . setAttribute ( 'scrolled' , '' )
} else {
newsContent . removeAttribute ( 'scrolled' )
}
}
2018-05-06 18:45:20 -07:00
/ * *
* Reload the news without restarting .
2018-05-10 02:48:55 -07:00
*
* @ returns { Promise . < void > } A promise which resolves when the news
* content has finished loading and transitioning .
2018-05-06 18:45:20 -07:00
* /
2018-05-10 02:48:55 -07:00
function reloadNews ( ) {
return new Promise ( ( resolve , reject ) => {
$ ( '#newsContent' ) . fadeOut ( 250 , ( ) => {
$ ( '#newsErrorLoading' ) . fadeIn ( 250 )
initNews ( ) . then ( ( ) => {
resolve ( )
} )
} )
2018-05-06 18:45:20 -07:00
} )
}
2018-06-04 20:08:03 -07:00
let newsAlertShown = false
/ * *
* Show the news alert indicating there is new news .
* /
function showNewsAlert ( ) {
newsAlertShown = true
$ ( newsButtonAlert ) . fadeIn ( 250 )
}
2018-05-06 18:45:20 -07:00
/ * *
* Initialize News UI . This will load the news and prepare
* the UI accordingly .
2018-05-10 02:48:55 -07:00
*
* @ returns { Promise . < void > } A promise which resolves when the news
* content has finished loading and transitioning .
2018-05-06 18:45:20 -07:00
* /
2018-05-10 02:48:55 -07:00
function initNews ( ) {
2018-05-06 18:45:20 -07:00
2018-05-10 02:48:55 -07:00
return new Promise ( ( resolve , reject ) => {
setNewsLoading ( true )
2018-05-06 18:45:20 -07:00
2018-05-10 02:48:55 -07:00
let news = { }
loadNews ( ) . then ( news => {
2018-05-06 18:45:20 -07:00
2023-03-18 19:07:22 -07:00
newsArr = news ? . articles || null
2018-05-06 18:45:20 -07:00
2018-05-10 02:48:55 -07:00
if ( newsArr == null ) {
// News Loading Failed
setNewsLoading ( false )
2018-05-06 18:45:20 -07:00
2018-05-10 02:48:55 -07:00
$ ( '#newsErrorLoading' ) . fadeOut ( 250 , ( ) => {
$ ( '#newsErrorFailed' ) . fadeIn ( 250 , ( ) => {
resolve ( )
} )
} )
} else if ( newsArr . length === 0 ) {
// No News Articles
setNewsLoading ( false )
2018-05-06 18:45:20 -07:00
2018-06-04 20:08:03 -07:00
ConfigManager . setNewsCache ( {
date : null ,
content : null ,
dismissed : false
} )
ConfigManager . save ( )
2018-05-10 02:48:55 -07:00
$ ( '#newsErrorLoading' ) . fadeOut ( 250 , ( ) => {
$ ( '#newsErrorNone' ) . fadeIn ( 250 , ( ) => {
resolve ( )
} )
} )
} else {
// Success
setNewsLoading ( false )
2018-06-04 20:08:03 -07:00
const lN = newsArr [ 0 ]
const cached = ConfigManager . getNewsCache ( )
let newHash = crypto . createHash ( 'sha1' ) . update ( lN . content ) . digest ( 'hex' )
let newDate = new Date ( lN . date )
let isNew = false
if ( cached . date != null && cached . content != null ) {
if ( new Date ( cached . date ) >= newDate ) {
// Compare Content
if ( cached . content !== newHash ) {
isNew = true
showNewsAlert ( )
} else {
if ( ! cached . dismissed ) {
isNew = true
showNewsAlert ( )
}
}
} else {
isNew = true
showNewsAlert ( )
}
} else {
isNew = true
showNewsAlert ( )
}
if ( isNew ) {
ConfigManager . setNewsCache ( {
date : newDate . getTime ( ) ,
content : newHash ,
dismissed : false
} )
ConfigManager . save ( )
}
2018-05-10 02:48:55 -07:00
const switchHandler = ( forward ) => {
let cArt = parseInt ( newsContent . getAttribute ( 'article' ) )
let nxtArt = forward ? ( cArt >= newsArr . length - 1 ? 0 : cArt + 1 ) : ( cArt <= 0 ? newsArr . length - 1 : cArt - 1 )
displayArticle ( newsArr [ nxtArt ] , nxtArt + 1 )
}
2018-06-30 12:16:31 -07:00
2018-05-10 02:48:55 -07:00
document . getElementById ( 'newsNavigateRight' ) . onclick = ( ) => { switchHandler ( true ) }
document . getElementById ( 'newsNavigateLeft' ) . onclick = ( ) => { switchHandler ( false ) }
$ ( '#newsErrorContainer' ) . fadeOut ( 250 , ( ) => {
displayArticle ( newsArr [ 0 ] , 1 )
$ ( '#newsContent' ) . fadeIn ( 250 , ( ) => {
resolve ( )
} )
} )
}
2018-05-06 18:45:20 -07:00
} )
2018-05-10 02:48:55 -07:00
} )
2018-05-06 18:45:20 -07:00
}
2018-06-30 12:16:31 -07:00
/ * *
* Add keyboard controls to the news UI . Left and right arrows toggle
* between articles . If you are on the landing page , the up arrow will
* open the news UI .
* /
document . addEventListener ( 'keydown' , ( e ) => {
if ( newsActive ) {
if ( e . key === 'ArrowRight' || e . key === 'ArrowLeft' ) {
document . getElementById ( e . key === 'ArrowRight' ? 'newsNavigateRight' : 'newsNavigateLeft' ) . click ( )
}
// Interferes with scrolling an article using the down arrow.
// Not sure of a straight forward solution at this point.
// if(e.key === 'ArrowDown'){
// document.getElementById('newsButton').click()
// }
} else {
if ( getCurrentView ( ) === VIEWS . landing ) {
if ( e . key === 'ArrowUp' ) {
document . getElementById ( 'newsButton' ) . click ( )
}
}
}
} )
2018-05-06 18:45:20 -07:00
/ * *
* Display a news article on the UI .
*
* @ param { Object } articleObject The article meta object .
* @ param { number } index The article index .
* /
function displayArticle ( articleObject , index ) {
newsArticleTitle . innerHTML = articleObject . title
newsArticleTitle . href = articleObject . link
newsArticleAuthor . innerHTML = 'by ' + articleObject . author
newsArticleDate . innerHTML = articleObject . date
newsArticleComments . innerHTML = articleObject . comments
newsArticleComments . href = articleObject . commentsLink
2018-06-20 06:12:44 -07:00
newsArticleContentScrollable . innerHTML = '<div id="newsArticleContentWrapper"><div class="newsArticleSpacerTop"></div>' + articleObject . content + '<div class="newsArticleSpacerBot"></div></div>'
2018-09-04 23:30:26 -07:00
Array . from ( newsArticleContentScrollable . getElementsByClassName ( 'bbCodeSpoilerButton' ) ) . forEach ( v => {
v . onclick = ( ) => {
const text = v . parentElement . getElementsByClassName ( 'bbCodeSpoilerText' ) [ 0 ]
text . style . display = text . style . display === 'block' ? 'none' : 'block'
}
} )
2018-05-06 18:45:20 -07:00
newsNavigationStatus . innerHTML = index + ' of ' + newsArr . length
newsContent . setAttribute ( 'article' , index - 1 )
}
/ * *
* Load news information from the RSS feed specified in the
* distribution index .
* /
2023-02-24 18:02:18 -08:00
async function loadNews ( ) {
const distroData = await DistroAPI . getDistribution ( )
2023-03-18 19:07:22 -07:00
if ( ! distroData . rawDistribution . rss ) {
loggerLanding . debug ( 'No RSS feed provided.' )
return null
}
2023-02-24 18:02:18 -08:00
const promise = new Promise ( ( resolve , reject ) => {
const newsFeed = distroData . rawDistribution . rss
2018-05-08 17:10:46 -07:00
const newsHost = new URL ( newsFeed ) . origin + '/'
2020-03-18 16:54:46 -07:00
$ . ajax ( {
url : newsFeed ,
success : ( data ) => {
const items = $ ( data ) . find ( 'item' )
const articles = [ ]
for ( let i = 0 ; i < items . length ; i ++ ) {
// JQuery Element
const el = $ ( items [ i ] )
// Resolve date.
const date = new Date ( el . find ( 'pubDate' ) . text ( ) ) . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' , year : 'numeric' , hour : 'numeric' , minute : 'numeric' } )
// Resolve comments.
let comments = el . find ( 'slash\\:comments' ) . text ( ) || '0'
comments = comments + ' Comment' + ( comments === '1' ? '' : 's' )
// Fix relative links in content.
let content = el . find ( 'content\\:encoded' ) . text ( )
let regex = /src="(?!http:\/\/|https:\/\/)(.+?)"/g
let matches
while ( ( matches = regex . exec ( content ) ) ) {
content = content . replace ( ` " ${ matches [ 1 ] } " ` , ` " ${ newsHost + matches [ 1 ] } " ` )
2018-07-22 10:31:15 -07:00
}
2020-03-18 16:54:46 -07:00
let link = el . find ( 'link' ) . text ( )
let title = el . find ( 'title' ) . text ( )
let author = el . find ( 'dc\\:creator' ) . text ( )
// Generate article.
articles . push (
{
link ,
title ,
date ,
author ,
content ,
comments ,
commentsLink : link + '#comments'
}
)
}
resolve ( {
articles
} )
} ,
timeout : 2500
} ) . catch ( err => {
2018-11-08 22:03:28 -08:00
resolve ( {
articles : null
2018-05-06 18:45:20 -07:00
} )
2018-11-08 22:03:28 -08:00
} )
2018-05-06 18:45:20 -07:00
} )
2023-02-24 18:02:18 -08:00
return await promise
2021-07-19 08:08:55 -07:00
}