landing.js 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131
  1. /**
  2. * Script for landing.ejs
  3. */
  4. // Requirements
  5. const cp = require('child_process')
  6. const crypto = require('crypto')
  7. const {URL} = require('url')
  8. // Internal Requirements
  9. const DiscordWrapper = require('./assets/js/discordwrapper')
  10. const Mojang = require('./assets/js/mojang')
  11. const ProcessBuilder = require('./assets/js/processbuilder')
  12. const ServerStatus = require('./assets/js/serverstatus')
  13. // Launch Elements
  14. const launch_content = document.getElementById('launch_content')
  15. const launch_details = document.getElementById('launch_details')
  16. const launch_progress = document.getElementById('launch_progress')
  17. const launch_progress_label = document.getElementById('launch_progress_label')
  18. const launch_details_text = document.getElementById('launch_details_text')
  19. const server_selection_button = document.getElementById('server_selection_button')
  20. const user_text = document.getElementById('user_text')
  21. const loggerLanding = LoggerUtil('%c[Landing]', 'color: #000668; font-weight: bold')
  22. /* Launch Progress Wrapper Functions */
  23. /**
  24. * Show/hide the loading area.
  25. *
  26. * @param {boolean} loading True if the loading area should be shown, otherwise false.
  27. */
  28. function toggleLaunchArea(loading){
  29. if(loading){
  30. launch_details.style.display = 'flex'
  31. launch_content.style.display = 'none'
  32. } else {
  33. launch_details.style.display = 'none'
  34. launch_content.style.display = 'inline-flex'
  35. }
  36. }
  37. /**
  38. * Set the details text of the loading area.
  39. *
  40. * @param {string} details The new text for the loading details.
  41. */
  42. function setLaunchDetails(details){
  43. launch_details_text.innerHTML = details
  44. }
  45. /**
  46. * Set the value of the loading progress bar and display that value.
  47. *
  48. * @param {number} value The progress value.
  49. * @param {number} max The total size.
  50. * @param {number|string} percent Optional. The percentage to display on the progress label.
  51. */
  52. function setLaunchPercentage(value, max, percent = ((value/max)*100)){
  53. launch_progress.setAttribute('max', max)
  54. launch_progress.setAttribute('value', value)
  55. launch_progress_label.innerHTML = percent + '%'
  56. }
  57. /**
  58. * Set the value of the OS progress bar and display that on the UI.
  59. *
  60. * @param {number} value The progress value.
  61. * @param {number} max The total download size.
  62. * @param {number|string} percent Optional. The percentage to display on the progress label.
  63. */
  64. function setDownloadPercentage(value, max, percent = ((value/max)*100)){
  65. remote.getCurrentWindow().setProgressBar(value/max)
  66. setLaunchPercentage(value, max, percent)
  67. }
  68. /**
  69. * Enable or disable the launch button.
  70. *
  71. * @param {boolean} val True to enable, false to disable.
  72. */
  73. function setLaunchEnabled(val){
  74. document.getElementById('launch_button').disabled = !val
  75. }
  76. // Bind launch button
  77. document.getElementById('launch_button').addEventListener('click', function(e){
  78. loggerLanding.log('Launching game..')
  79. const mcVersion = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion()
  80. const jExe = ConfigManager.getJavaExecutable()
  81. if(jExe == null){
  82. asyncSystemScan(mcVersion)
  83. } else {
  84. setLaunchDetails('Please wait..')
  85. toggleLaunchArea(true)
  86. setLaunchPercentage(0, 100)
  87. const jg = new JavaGuard(mcVersion)
  88. jg._validateJavaBinary(jExe).then((v) => {
  89. loggerLanding.log('Java version meta', v)
  90. if(v.valid){
  91. dlAsync()
  92. } else {
  93. asyncSystemScan(mcVersion)
  94. }
  95. })
  96. }
  97. })
  98. // Bind settings button
  99. document.getElementById('settingsMediaButton').onclick = (e) => {
  100. prepareSettings()
  101. switchView(getCurrentView(), VIEWS.settings)
  102. }
  103. // Bind avatar overlay button.
  104. document.getElementById('avatarOverlay').onclick = (e) => {
  105. prepareSettings()
  106. switchView(getCurrentView(), VIEWS.settings, 500, 500, () => {
  107. settingsNavItemListener(document.getElementById('settingsNavAccount'), false)
  108. })
  109. }
  110. // Bind selected account
  111. function updateSelectedAccount(authUser){
  112. let username = 'No Account Selected'
  113. if(authUser != null){
  114. if(authUser.displayName != null){
  115. username = authUser.displayName
  116. }
  117. if(authUser.uuid != null){
  118. document.getElementById('avatarContainer').style.backgroundImage = `url('https://crafatar.com/renders/body/${authUser.uuid}')`
  119. }
  120. }
  121. user_text.innerHTML = username
  122. }
  123. updateSelectedAccount(ConfigManager.getSelectedAccount())
  124. // Bind selected server
  125. function updateSelectedServer(serv){
  126. if(getCurrentView() === VIEWS.settings){
  127. saveAllModConfigurations()
  128. }
  129. ConfigManager.setSelectedServer(serv != null ? serv.getID() : null)
  130. ConfigManager.save()
  131. server_selection_button.innerHTML = '\u2022 ' + (serv != null ? serv.getName() : 'No Server Selected')
  132. if(getCurrentView() === VIEWS.settings){
  133. animateModsTabRefresh()
  134. }
  135. setLaunchEnabled(serv != null)
  136. }
  137. // Real text is set in uibinder.js on distributionIndexDone.
  138. server_selection_button.innerHTML = '\u2022 Loading..'
  139. server_selection_button.onclick = (e) => {
  140. e.target.blur()
  141. toggleServerSelection(true)
  142. }
  143. // Update Mojang Status Color
  144. const refreshMojangStatuses = async function(){
  145. loggerLanding.log('Refreshing Mojang Statuses..')
  146. let status = 'grey'
  147. let tooltipEssentialHTML = ''
  148. let tooltipNonEssentialHTML = ''
  149. try {
  150. const statuses = await Mojang.status()
  151. greenCount = 0
  152. greyCount = 0
  153. for(let i=0; i<statuses.length; i++){
  154. const service = statuses[i]
  155. if(service.essential){
  156. tooltipEssentialHTML += `<div class="mojangStatusContainer">
  157. <span class="mojangStatusIcon" style="color: ${Mojang.statusToHex(service.status)};">&#8226;</span>
  158. <span class="mojangStatusName">${service.name}</span>
  159. </div>`
  160. } else {
  161. tooltipNonEssentialHTML += `<div class="mojangStatusContainer">
  162. <span class="mojangStatusIcon" style="color: ${Mojang.statusToHex(service.status)};">&#8226;</span>
  163. <span class="mojangStatusName">${service.name}</span>
  164. </div>`
  165. }
  166. if(service.status === 'yellow' && status !== 'red'){
  167. status = 'yellow'
  168. } else if(service.status === 'red'){
  169. status = 'red'
  170. } else {
  171. if(service.status === 'grey'){
  172. ++greyCount
  173. }
  174. ++greenCount
  175. }
  176. }
  177. if(greenCount === statuses.length){
  178. if(greyCount === statuses.length){
  179. status = 'grey'
  180. } else {
  181. status = 'green'
  182. }
  183. }
  184. } catch (err) {
  185. loggerLanding.warn('Unable to refresh Mojang service status.')
  186. loggerLanding.debug(err)
  187. }
  188. document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML
  189. document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML
  190. document.getElementById('mojang_status_icon').style.color = Mojang.statusToHex(status)
  191. }
  192. const refreshServerStatus = async function(fade = false){
  193. loggerLanding.log('Refreshing Server Status')
  194. const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer())
  195. let pLabel = 'SERVER'
  196. let pVal = 'OFFLINE'
  197. try {
  198. const serverURL = new URL('my://' + serv.getAddress())
  199. const servStat = await ServerStatus.getStatus(serverURL.hostname, serverURL.port)
  200. if(servStat.online){
  201. pLabel = 'PLAYERS'
  202. pVal = servStat.onlinePlayers + '/' + servStat.maxPlayers
  203. }
  204. } catch (err) {
  205. loggerLanding.warn('Unable to refresh server status, assuming offline.')
  206. loggerLanding.debug(err)
  207. }
  208. if(fade){
  209. $('#server_status_wrapper').fadeOut(250, () => {
  210. document.getElementById('landingPlayerLabel').innerHTML = pLabel
  211. document.getElementById('player_count').innerHTML = pVal
  212. $('#server_status_wrapper').fadeIn(500)
  213. })
  214. } else {
  215. document.getElementById('landingPlayerLabel').innerHTML = pLabel
  216. document.getElementById('player_count').innerHTML = pVal
  217. }
  218. }
  219. refreshMojangStatuses()
  220. // Server Status is refreshed in uibinder.js on distributionIndexDone.
  221. // Set refresh rate to once every 5 minutes.
  222. let mojangStatusListener = setInterval(() => refreshMojangStatuses(true), 300000)
  223. let serverStatusListener = setInterval(() => refreshServerStatus(true), 300000)
  224. /**
  225. * Shows an error overlay, toggles off the launch area.
  226. *
  227. * @param {string} title The overlay title.
  228. * @param {string} desc The overlay description.
  229. */
  230. function showLaunchFailure(title, desc){
  231. setOverlayContent(
  232. title,
  233. desc,
  234. 'Okay'
  235. )
  236. setOverlayHandler(null)
  237. toggleOverlay(true)
  238. toggleLaunchArea(false)
  239. }
  240. /* System (Java) Scan */
  241. let sysAEx
  242. let scanAt
  243. let extractListener
  244. /**
  245. * Asynchronously scan the system for valid Java installations.
  246. *
  247. * @param {string} mcVersion The Minecraft version we are scanning for.
  248. * @param {boolean} launchAfter Whether we should begin to launch after scanning.
  249. */
  250. function asyncSystemScan(mcVersion, launchAfter = true){
  251. setLaunchDetails('Please wait..')
  252. toggleLaunchArea(true)
  253. setLaunchPercentage(0, 100)
  254. const loggerSysAEx = LoggerUtil('%c[SysAEx]', 'color: #353232; font-weight: bold')
  255. const forkEnv = JSON.parse(JSON.stringify(process.env))
  256. forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory()
  257. // Fork a process to run validations.
  258. sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [
  259. 'JavaGuard',
  260. mcVersion
  261. ], {
  262. env: forkEnv,
  263. stdio: 'pipe'
  264. })
  265. // Stdout
  266. sysAEx.stdio[1].setEncoding('utf8')
  267. sysAEx.stdio[1].on('data', (data) => {
  268. loggerSysAEx.log(data)
  269. })
  270. // Stderr
  271. sysAEx.stdio[2].setEncoding('utf8')
  272. sysAEx.stdio[2].on('data', (data) => {
  273. loggerSysAEx.log(data)
  274. })
  275. sysAEx.on('message', (m) => {
  276. if(m.context === 'validateJava'){
  277. if(m.result == null){
  278. // If the result is null, no valid Java installation was found.
  279. // Show this information to the user.
  280. setOverlayContent(
  281. 'No Compatible<br>Java Installation Found',
  282. 'In order to join WesterosCraft, you need a 64-bit installation of Java 8. Would you like us to install a copy? By installing, you accept <a href="http://www.oracle.com/technetwork/java/javase/terms/license/index.html">Oracle\'s license agreement</a>.',
  283. 'Install Java',
  284. 'Install Manually'
  285. )
  286. setOverlayHandler(() => {
  287. setLaunchDetails('Preparing Java Download..')
  288. sysAEx.send({task: 'execute', function: '_enqueueOracleJRE', argsArr: [ConfigManager.getDataDirectory()]})
  289. toggleOverlay(false)
  290. })
  291. setDismissHandler(() => {
  292. $('#overlayContent').fadeOut(250, () => {
  293. //$('#overlayDismiss').toggle(false)
  294. setOverlayContent(
  295. 'Java is Required<br>to Launch',
  296. 'A valid x64 installation of Java 8 is required to launch.<br><br>Please refer to our <a href="https://github.com/WesterosCraftCode/ElectronLauncher/wiki/Java-Management#manually-installing-a-valid-version-of-java">Java Management Guide</a> for instructions on how to manually install Java.',
  297. 'I Understand',
  298. 'Go Back'
  299. )
  300. setOverlayHandler(() => {
  301. toggleLaunchArea(false)
  302. toggleOverlay(false)
  303. })
  304. setDismissHandler(() => {
  305. toggleOverlay(false, true)
  306. asyncSystemScan()
  307. })
  308. $('#overlayContent').fadeIn(250)
  309. })
  310. })
  311. toggleOverlay(true, true)
  312. } else {
  313. // Java installation found, use this to launch the game.
  314. ConfigManager.setJavaExecutable(m.result)
  315. ConfigManager.save()
  316. // We need to make sure that the updated value is on the settings UI.
  317. // Just incase the settings UI is already open.
  318. settingsJavaExecVal.value = m.result
  319. populateJavaExecDetails(settingsJavaExecVal.value)
  320. if(launchAfter){
  321. dlAsync()
  322. }
  323. sysAEx.disconnect()
  324. }
  325. } else if(m.context === '_enqueueOracleJRE'){
  326. if(m.result === true){
  327. // Oracle JRE enqueued successfully, begin download.
  328. setLaunchDetails('Downloading Java..')
  329. sysAEx.send({task: 'execute', function: 'processDlQueues', argsArr: [[{id:'java', limit:1}]]})
  330. } else {
  331. // Oracle JRE enqueue failed. Probably due to a change in their website format.
  332. // User will have to follow the guide to install Java.
  333. setOverlayContent(
  334. 'Unexpected Issue:<br>Java Download Failed',
  335. 'Unfortunately we\'ve encountered an issue while attempting to install Java. You will need to manually install a copy. Please check out our <a href="http://westeroscraft.wikia.com/wiki/Troubleshooting_Guide">Troubleshooting Guide</a> for more details and instructions.',
  336. 'I Understand'
  337. )
  338. setOverlayHandler(() => {
  339. toggleOverlay(false)
  340. toggleLaunchArea(false)
  341. })
  342. toggleOverlay(true)
  343. sysAEx.disconnect()
  344. }
  345. } else if(m.context === 'progress'){
  346. switch(m.data){
  347. case 'download':
  348. // Downloading..
  349. setDownloadPercentage(m.value, m.total, m.percent)
  350. break
  351. }
  352. } else if(m.context === 'complete'){
  353. switch(m.data){
  354. case 'download': {
  355. // Show installing progress bar.
  356. remote.getCurrentWindow().setProgressBar(2)
  357. // Wait for extration to complete.
  358. const eLStr = 'Extracting'
  359. let dotStr = ''
  360. setLaunchDetails(eLStr)
  361. extractListener = setInterval(() => {
  362. if(dotStr.length >= 3){
  363. dotStr = ''
  364. } else {
  365. dotStr += '.'
  366. }
  367. setLaunchDetails(eLStr + dotStr)
  368. }, 750)
  369. break
  370. }
  371. case 'java':
  372. // Download & extraction complete, remove the loading from the OS progress bar.
  373. remote.getCurrentWindow().setProgressBar(-1)
  374. // Extraction completed successfully.
  375. ConfigManager.setJavaExecutable(m.args[0])
  376. ConfigManager.save()
  377. if(extractListener != null){
  378. clearInterval(extractListener)
  379. extractListener = null
  380. }
  381. setLaunchDetails('Java Installed!')
  382. if(launchAfter){
  383. dlAsync()
  384. }
  385. sysAEx.disconnect()
  386. break
  387. }
  388. }
  389. })
  390. // Begin system Java scan.
  391. setLaunchDetails('Checking system info..')
  392. sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getDataDirectory()]})
  393. }
  394. // Keep reference to Minecraft Process
  395. let proc
  396. // Is DiscordRPC enabled
  397. let hasRPC = false
  398. // Joined server regex
  399. const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/
  400. const GAME_JOINED_REGEX = /\[.+\]: Skipping bad option: lastServer:/
  401. const GAME_LAUNCH_REGEX = /^\[.+\]: MinecraftForge .+ Initialized$/
  402. let aEx
  403. let serv
  404. let versionData
  405. let forgeData
  406. let progressListener
  407. function dlAsync(login = true){
  408. // Login parameter is temporary for debug purposes. Allows testing the validation/downloads without
  409. // launching the game.
  410. if(login) {
  411. if(ConfigManager.getSelectedAccount() == null){
  412. loggerLanding.error('You must be logged into an account.')
  413. return
  414. }
  415. }
  416. setLaunchDetails('Please wait..')
  417. toggleLaunchArea(true)
  418. setLaunchPercentage(0, 100)
  419. const loggerAEx = LoggerUtil('%c[AEx]', 'color: #353232; font-weight: bold')
  420. const loggerLaunchSuite = LoggerUtil('%c[LaunchSuite]', 'color: #000668; font-weight: bold')
  421. const forkEnv = JSON.parse(JSON.stringify(process.env))
  422. forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory()
  423. // Start AssetExec to run validations and downloads in a forked process.
  424. aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [
  425. 'AssetGuard',
  426. ConfigManager.getCommonDirectory(),
  427. ConfigManager.getJavaExecutable()
  428. ], {
  429. env: forkEnv,
  430. stdio: 'pipe'
  431. })
  432. // Stdout
  433. aEx.stdio[1].setEncoding('utf8')
  434. aEx.stdio[1].on('data', (data) => {
  435. loggerAEx.log(data)
  436. })
  437. // Stderr
  438. aEx.stdio[2].setEncoding('utf8')
  439. aEx.stdio[2].on('data', (data) => {
  440. loggerAEx.log(data)
  441. })
  442. aEx.on('error', (err) => {
  443. loggerLaunchSuite.error('Error during launch', err)
  444. showLaunchFailure('Error During Launch', err.message || 'See console (CTRL + Shift + i) for more details.')
  445. })
  446. aEx.on('close', (code, signal) => {
  447. if(code !== 0){
  448. loggerLaunchSuite.error(`AssetExec exited with code ${code}, assuming error.`)
  449. showLaunchFailure('Error During Launch', 'See console (CTRL + Shift + i) for more details.')
  450. }
  451. })
  452. // Establish communications between the AssetExec and current process.
  453. aEx.on('message', (m) => {
  454. if(m.context === 'validate'){
  455. switch(m.data){
  456. case 'distribution':
  457. setLaunchPercentage(20, 100)
  458. loggerLaunchSuite.log('Validated distibution index.')
  459. setLaunchDetails('Loading version information..')
  460. break
  461. case 'version':
  462. setLaunchPercentage(40, 100)
  463. loggerLaunchSuite.log('Version data loaded.')
  464. setLaunchDetails('Validating asset integrity..')
  465. break
  466. case 'assets':
  467. setLaunchPercentage(60, 100)
  468. loggerLaunchSuite.log('Asset Validation Complete')
  469. setLaunchDetails('Validating library integrity..')
  470. break
  471. case 'libraries':
  472. setLaunchPercentage(80, 100)
  473. loggerLaunchSuite.log('Library validation complete.')
  474. setLaunchDetails('Validating miscellaneous file integrity..')
  475. break
  476. case 'files':
  477. setLaunchPercentage(100, 100)
  478. loggerLaunchSuite.log('File validation complete.')
  479. setLaunchDetails('Downloading files..')
  480. break
  481. }
  482. } else if(m.context === 'progress'){
  483. switch(m.data){
  484. case 'assets': {
  485. const perc = (m.value/m.total)*20
  486. setLaunchPercentage(40+perc, 100, parseInt(40+perc))
  487. break
  488. }
  489. case 'download':
  490. setDownloadPercentage(m.value, m.total, m.percent)
  491. break
  492. case 'extract': {
  493. // Show installing progress bar.
  494. remote.getCurrentWindow().setProgressBar(2)
  495. // Download done, extracting.
  496. const eLStr = 'Extracting libraries'
  497. let dotStr = ''
  498. setLaunchDetails(eLStr)
  499. progressListener = setInterval(() => {
  500. if(dotStr.length >= 3){
  501. dotStr = ''
  502. } else {
  503. dotStr += '.'
  504. }
  505. setLaunchDetails(eLStr + dotStr)
  506. }, 750)
  507. break
  508. }
  509. }
  510. } else if(m.context === 'complete'){
  511. switch(m.data){
  512. case 'download':
  513. // Download and extraction complete, remove the loading from the OS progress bar.
  514. remote.getCurrentWindow().setProgressBar(-1)
  515. if(progressListener != null){
  516. clearInterval(progressListener)
  517. progressListener = null
  518. }
  519. setLaunchDetails('Preparing to launch..')
  520. break
  521. }
  522. } else if(m.context === 'error'){
  523. switch(m.data){
  524. case 'download':
  525. loggerLaunchSuite.error('Error while downloading:', m.error)
  526. if(m.error.code === 'ENOENT'){
  527. showLaunchFailure(
  528. 'Download Error',
  529. 'Could not connect to the file server. Ensure that you are connected to the internet and try again.'
  530. )
  531. } else {
  532. showLaunchFailure(
  533. 'Download Error',
  534. 'Check the console (CTRL + Shift + i) for more details. Please try again.'
  535. )
  536. }
  537. remote.getCurrentWindow().setProgressBar(-1)
  538. // Disconnect from AssetExec
  539. aEx.disconnect()
  540. break
  541. }
  542. } else if(m.context === 'validateEverything'){
  543. let allGood = true
  544. // If these properties are not defined it's likely an error.
  545. if(m.result.forgeData == null || m.result.versionData == null){
  546. loggerLaunchSuite.error('Error during validation:', m.result)
  547. loggerLaunchSuite.error('Error during launch', m.result.error)
  548. showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.')
  549. allGood = false
  550. }
  551. forgeData = m.result.forgeData
  552. versionData = m.result.versionData
  553. if(login && allGood) {
  554. const authUser = ConfigManager.getSelectedAccount()
  555. loggerLaunchSuite.log(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`)
  556. let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion())
  557. setLaunchDetails('Launching game..')
  558. // Attach a temporary listener to the client output.
  559. // Will wait for a certain bit of text meaning that
  560. // the client application has started, and we can hide
  561. // the progress bar stuff.
  562. const tempListener = function(data){
  563. if(GAME_LAUNCH_REGEX.test(data.trim())){
  564. toggleLaunchArea(false)
  565. if(hasRPC){
  566. DiscordWrapper.updateDetails('Loading game..')
  567. }
  568. proc.stdout.on('data', gameStateChange)
  569. proc.stdout.removeListener('data', tempListener)
  570. proc.stderr.removeListener('data', gameErrorListener)
  571. }
  572. }
  573. // Listener for Discord RPC.
  574. const gameStateChange = function(data){
  575. data = data.trim()
  576. if(SERVER_JOINED_REGEX.test(data)){
  577. DiscordWrapper.updateDetails('Exploring the Realm!')
  578. } else if(GAME_JOINED_REGEX.test(data)){
  579. DiscordWrapper.updateDetails('Sailing to Westeros!')
  580. }
  581. }
  582. const gameErrorListener = function(data){
  583. data = data.trim()
  584. if(data.indexOf('Could not find or load main class net.minecraft.launchwrapper.Launch') > -1){
  585. loggerLaunchSuite.error('Game launch failed, LaunchWrapper was not downloaded properly.')
  586. 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/WesterosCraftCode/ElectronLauncher/issues">submit an issue</a> and let us know what antivirus software you use. We\'ll contact them and try to straighten things out.')
  587. }
  588. }
  589. try {
  590. // Build Minecraft process.
  591. proc = pb.build()
  592. // Bind listeners to stdout.
  593. proc.stdout.on('data', tempListener)
  594. proc.stderr.on('data', gameErrorListener)
  595. setLaunchDetails('Done. Enjoy the server!')
  596. // Init Discord Hook
  597. const distro = DistroManager.getDistribution()
  598. if(distro.discord != null && serv.discord != null){
  599. DiscordWrapper.initRPC(distro.discord, serv.discord)
  600. hasRPC = true
  601. proc.on('close', (code, signal) => {
  602. loggerLaunchSuite.log('Shutting down Discord Rich Presence..')
  603. DiscordWrapper.shutdownRPC()
  604. hasRPC = false
  605. proc = null
  606. })
  607. }
  608. } catch(err) {
  609. loggerLaunchSuite.error('Error during launch', err)
  610. showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.')
  611. }
  612. }
  613. // Disconnect from AssetExec
  614. aEx.disconnect()
  615. }
  616. })
  617. // Begin Validations
  618. // Validate Forge files.
  619. setLaunchDetails('Loading server information..')
  620. refreshDistributionIndex(true, (data) => {
  621. onDistroRefresh(data)
  622. serv = data.getServer(ConfigManager.getSelectedServer())
  623. aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]})
  624. }, (err) => {
  625. loggerLaunchSuite.log('Error while fetching a fresh copy of the distribution index.', err)
  626. refreshDistributionIndex(false, (data) => {
  627. onDistroRefresh(data)
  628. serv = data.getServer(ConfigManager.getSelectedServer())
  629. aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]})
  630. }, (err) => {
  631. loggerLaunchSuite.error('Unable to refresh distribution index.', err)
  632. if(DistroManager.getDistribution() == null){
  633. showLaunchFailure('Fatal Error', 'Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details.')
  634. // Disconnect from AssetExec
  635. aEx.disconnect()
  636. } else {
  637. serv = data.getServer(ConfigManager.getSelectedServer())
  638. aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]})
  639. }
  640. })
  641. })
  642. }
  643. /**
  644. * News Loading Functions
  645. */
  646. // DOM Cache
  647. const newsContent = document.getElementById('newsContent')
  648. const newsArticleTitle = document.getElementById('newsArticleTitle')
  649. const newsArticleDate = document.getElementById('newsArticleDate')
  650. const newsArticleAuthor = document.getElementById('newsArticleAuthor')
  651. const newsArticleComments = document.getElementById('newsArticleComments')
  652. const newsNavigationStatus = document.getElementById('newsNavigationStatus')
  653. const newsArticleContentScrollable = document.getElementById('newsArticleContentScrollable')
  654. const nELoadSpan = document.getElementById('nELoadSpan')
  655. // News slide caches.
  656. let newsActive = false
  657. let newsGlideCount = 0
  658. /**
  659. * Show the news UI via a slide animation.
  660. *
  661. * @param {boolean} up True to slide up, otherwise false.
  662. */
  663. function slide_(up){
  664. const lCUpper = document.querySelector('#landingContainer > #upper')
  665. const lCLLeft = document.querySelector('#landingContainer > #lower > #left')
  666. const lCLCenter = document.querySelector('#landingContainer > #lower > #center')
  667. const lCLRight = document.querySelector('#landingContainer > #lower > #right')
  668. const newsBtn = document.querySelector('#landingContainer > #lower > #center #content')
  669. const landingContainer = document.getElementById('landingContainer')
  670. const newsContainer = document.querySelector('#landingContainer > #newsContainer')
  671. newsGlideCount++
  672. if(up){
  673. lCUpper.style.top = '-200vh'
  674. lCLLeft.style.top = '-200vh'
  675. lCLCenter.style.top = '-200vh'
  676. lCLRight.style.top = '-200vh'
  677. newsBtn.style.top = '130vh'
  678. newsContainer.style.top = '0px'
  679. //date.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric'})
  680. //landingContainer.style.background = 'rgba(29, 29, 29, 0.55)'
  681. landingContainer.style.background = 'rgba(0, 0, 0, 0.50)'
  682. setTimeout(() => {
  683. if(newsGlideCount === 1){
  684. lCLCenter.style.transition = 'none'
  685. newsBtn.style.transition = 'none'
  686. }
  687. newsGlideCount--
  688. }, 2000)
  689. } else {
  690. setTimeout(() => {
  691. newsGlideCount--
  692. }, 2000)
  693. landingContainer.style.background = null
  694. lCLCenter.style.transition = null
  695. newsBtn.style.transition = null
  696. newsContainer.style.top = '100%'
  697. lCUpper.style.top = '0px'
  698. lCLLeft.style.top = '0px'
  699. lCLCenter.style.top = '0px'
  700. lCLRight.style.top = '0px'
  701. newsBtn.style.top = '10px'
  702. }
  703. }
  704. // Bind news button.
  705. document.getElementById('newsButton').onclick = () => {
  706. // Toggle tabbing.
  707. if(newsActive){
  708. $('#landingContainer *').removeAttr('tabindex')
  709. $('#newsContainer *').attr('tabindex', '-1')
  710. } else {
  711. $('#landingContainer *').attr('tabindex', '-1')
  712. $('#newsContainer, #newsContainer *, #lower, #lower #center *').removeAttr('tabindex')
  713. if(newsAlertShown){
  714. $('#newsButtonAlert').fadeOut(2000)
  715. newsAlertShown = false
  716. ConfigManager.setNewsCacheDismissed(true)
  717. ConfigManager.save()
  718. }
  719. }
  720. slide_(!newsActive)
  721. newsActive = !newsActive
  722. }
  723. // Array to store article meta.
  724. let newsArr = null
  725. // News load animation listener.
  726. let newsLoadingListener = null
  727. /**
  728. * Set the news loading animation.
  729. *
  730. * @param {boolean} val True to set loading animation, otherwise false.
  731. */
  732. function setNewsLoading(val){
  733. if(val){
  734. const nLStr = 'Checking for News'
  735. let dotStr = '..'
  736. nELoadSpan.innerHTML = nLStr + dotStr
  737. newsLoadingListener = setInterval(() => {
  738. if(dotStr.length >= 3){
  739. dotStr = ''
  740. } else {
  741. dotStr += '.'
  742. }
  743. nELoadSpan.innerHTML = nLStr + dotStr
  744. }, 750)
  745. } else {
  746. if(newsLoadingListener != null){
  747. clearInterval(newsLoadingListener)
  748. newsLoadingListener = null
  749. }
  750. }
  751. }
  752. // Bind retry button.
  753. newsErrorRetry.onclick = () => {
  754. $('#newsErrorFailed').fadeOut(250, () => {
  755. initNews()
  756. $('#newsErrorLoading').fadeIn(250)
  757. })
  758. }
  759. newsArticleContentScrollable.onscroll = (e) => {
  760. if(e.target.scrollTop > Number.parseFloat($('.newsArticleSpacerTop').css('height'))){
  761. newsContent.setAttribute('scrolled', '')
  762. } else {
  763. newsContent.removeAttribute('scrolled')
  764. }
  765. }
  766. /**
  767. * Reload the news without restarting.
  768. *
  769. * @returns {Promise.<void>} A promise which resolves when the news
  770. * content has finished loading and transitioning.
  771. */
  772. function reloadNews(){
  773. return new Promise((resolve, reject) => {
  774. $('#newsContent').fadeOut(250, () => {
  775. $('#newsErrorLoading').fadeIn(250)
  776. initNews().then(() => {
  777. resolve()
  778. })
  779. })
  780. })
  781. }
  782. let newsAlertShown = false
  783. /**
  784. * Show the news alert indicating there is new news.
  785. */
  786. function showNewsAlert(){
  787. newsAlertShown = true
  788. $(newsButtonAlert).fadeIn(250)
  789. }
  790. /**
  791. * Initialize News UI. This will load the news and prepare
  792. * the UI accordingly.
  793. *
  794. * @returns {Promise.<void>} A promise which resolves when the news
  795. * content has finished loading and transitioning.
  796. */
  797. function initNews(){
  798. return new Promise((resolve, reject) => {
  799. setNewsLoading(true)
  800. let news = {}
  801. loadNews().then(news => {
  802. newsArr = news.articles || null
  803. if(newsArr == null){
  804. // News Loading Failed
  805. setNewsLoading(false)
  806. $('#newsErrorLoading').fadeOut(250, () => {
  807. $('#newsErrorFailed').fadeIn(250, () => {
  808. resolve()
  809. })
  810. })
  811. } else if(newsArr.length === 0) {
  812. // No News Articles
  813. setNewsLoading(false)
  814. ConfigManager.setNewsCache({
  815. date: null,
  816. content: null,
  817. dismissed: false
  818. })
  819. ConfigManager.save()
  820. $('#newsErrorLoading').fadeOut(250, () => {
  821. $('#newsErrorNone').fadeIn(250, () => {
  822. resolve()
  823. })
  824. })
  825. } else {
  826. // Success
  827. setNewsLoading(false)
  828. const lN = newsArr[0]
  829. const cached = ConfigManager.getNewsCache()
  830. let newHash = crypto.createHash('sha1').update(lN.content).digest('hex')
  831. let newDate = new Date(lN.date)
  832. let isNew = false
  833. if(cached.date != null && cached.content != null){
  834. if(new Date(cached.date) >= newDate){
  835. // Compare Content
  836. if(cached.content !== newHash){
  837. isNew = true
  838. showNewsAlert()
  839. } else {
  840. if(!cached.dismissed){
  841. isNew = true
  842. showNewsAlert()
  843. }
  844. }
  845. } else {
  846. isNew = true
  847. showNewsAlert()
  848. }
  849. } else {
  850. isNew = true
  851. showNewsAlert()
  852. }
  853. if(isNew){
  854. ConfigManager.setNewsCache({
  855. date: newDate.getTime(),
  856. content: newHash,
  857. dismissed: false
  858. })
  859. ConfigManager.save()
  860. }
  861. const switchHandler = (forward) => {
  862. let cArt = parseInt(newsContent.getAttribute('article'))
  863. let nxtArt = forward ? (cArt >= newsArr.length-1 ? 0 : cArt + 1) : (cArt <= 0 ? newsArr.length-1 : cArt - 1)
  864. displayArticle(newsArr[nxtArt], nxtArt+1)
  865. }
  866. document.getElementById('newsNavigateRight').onclick = () => { switchHandler(true) }
  867. document.getElementById('newsNavigateLeft').onclick = () => { switchHandler(false) }
  868. $('#newsErrorContainer').fadeOut(250, () => {
  869. displayArticle(newsArr[0], 1)
  870. $('#newsContent').fadeIn(250, () => {
  871. resolve()
  872. })
  873. })
  874. }
  875. })
  876. })
  877. }
  878. /**
  879. * Add keyboard controls to the news UI. Left and right arrows toggle
  880. * between articles. If you are on the landing page, the up arrow will
  881. * open the news UI.
  882. */
  883. document.addEventListener('keydown', (e) => {
  884. if(newsActive){
  885. if(e.key === 'ArrowRight' || e.key === 'ArrowLeft'){
  886. document.getElementById(e.key === 'ArrowRight' ? 'newsNavigateRight' : 'newsNavigateLeft').click()
  887. }
  888. // Interferes with scrolling an article using the down arrow.
  889. // Not sure of a straight forward solution at this point.
  890. // if(e.key === 'ArrowDown'){
  891. // document.getElementById('newsButton').click()
  892. // }
  893. } else {
  894. if(getCurrentView() === VIEWS.landing){
  895. if(e.key === 'ArrowUp'){
  896. document.getElementById('newsButton').click()
  897. }
  898. }
  899. }
  900. })
  901. /**
  902. * Display a news article on the UI.
  903. *
  904. * @param {Object} articleObject The article meta object.
  905. * @param {number} index The article index.
  906. */
  907. function displayArticle(articleObject, index){
  908. newsArticleTitle.innerHTML = articleObject.title
  909. newsArticleTitle.href = articleObject.link
  910. newsArticleAuthor.innerHTML = 'by ' + articleObject.author
  911. newsArticleDate.innerHTML = articleObject.date
  912. newsArticleComments.innerHTML = articleObject.comments
  913. newsArticleComments.href = articleObject.commentsLink
  914. newsArticleContentScrollable.innerHTML = '<div id="newsArticleContentWrapper"><div class="newsArticleSpacerTop"></div>' + articleObject.content + '<div class="newsArticleSpacerBot"></div></div>'
  915. Array.from(newsArticleContentScrollable.getElementsByClassName('bbCodeSpoilerButton')).forEach(v => {
  916. v.onclick = () => {
  917. const text = v.parentElement.getElementsByClassName('bbCodeSpoilerText')[0]
  918. text.style.display = text.style.display === 'block' ? 'none' : 'block'
  919. }
  920. })
  921. newsNavigationStatus.innerHTML = index + ' of ' + newsArr.length
  922. newsContent.setAttribute('article', index-1)
  923. }
  924. /**
  925. * Load news information from the RSS feed specified in the
  926. * distribution index.
  927. */
  928. function loadNews(){
  929. return new Promise((resolve, reject) => {
  930. const distroData = DistroManager.getDistribution()
  931. const newsFeed = distroData.getRSS()
  932. const newsHost = new URL(newsFeed).origin + '/'
  933. $.ajax(
  934. {
  935. url: newsFeed,
  936. success: (data) => {
  937. const items = $(data).find('item')
  938. const articles = []
  939. for(let i=0; i<items.length; i++){
  940. // JQuery Element
  941. const el = $(items[i])
  942. // Resolve date.
  943. const date = new Date(el.find('pubDate').text()).toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric'})
  944. // Resolve comments.
  945. let comments = el.find('slash\\:comments').text() || '0'
  946. comments = comments + ' Comment' + (comments === '1' ? '' : 's')
  947. // Fix relative links in content.
  948. let content = el.find('content\\:encoded').text()
  949. let regex = /src="(?!http:\/\/|https:\/\/)(.+?)"/g
  950. let matches
  951. while((matches = regex.exec(content))){
  952. content = content.replace(`"${matches[1]}"`, `"${newsHost + matches[1]}"`)
  953. }
  954. let link = el.find('link').text()
  955. let title = el.find('title').text()
  956. let author = el.find('dc\\:creator').text()
  957. // Generate article.
  958. articles.push(
  959. {
  960. link,
  961. title,
  962. date,
  963. author,
  964. content,
  965. comments,
  966. commentsLink: link + '#comments'
  967. }
  968. )
  969. }
  970. resolve({
  971. articles
  972. })
  973. },
  974. timeout: 2500
  975. }
  976. ).catch(err => {
  977. resolve({
  978. articles: null
  979. })
  980. })
  981. })
  982. }