landing.js 36 KB

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