distromanager.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. const fs = require('fs')
  2. const path = require('path')
  3. const request = require('request')
  4. const ConfigManager = require('./configmanager')
  5. const logger = require('./loggerutil')('%c[DistroManager]', 'color: #a02d2a; font-weight: bold')
  6. /**
  7. * Represents the download information
  8. * for a specific module.
  9. */
  10. class Artifact {
  11. /**
  12. * Parse a JSON object into an Artifact.
  13. *
  14. * @param {Object} json A JSON object representing an Artifact.
  15. *
  16. * @returns {Artifact} The parsed Artifact.
  17. */
  18. static fromJSON(json){
  19. return Object.assign(new Artifact(), json)
  20. }
  21. /**
  22. * Get the MD5 hash of the artifact. This value may
  23. * be undefined for artifacts which are not to be
  24. * validated and updated.
  25. *
  26. * @returns {string} The MD5 hash of the Artifact or undefined.
  27. */
  28. getHash(){
  29. return this.MD5
  30. }
  31. /**
  32. * @returns {number} The download size of the artifact.
  33. */
  34. getSize(){
  35. return this.size
  36. }
  37. /**
  38. * @returns {string} The download url of the artifact.
  39. */
  40. getURL(){
  41. return this.url
  42. }
  43. /**
  44. * @returns {string} The artifact's destination path.
  45. */
  46. getPath(){
  47. return this.path
  48. }
  49. }
  50. exports.Artifact
  51. /**
  52. * Represents a the requirement status
  53. * of a module.
  54. */
  55. class Required {
  56. /**
  57. * Parse a JSON object into a Required object.
  58. *
  59. * @param {Object} json A JSON object representing a Required object.
  60. *
  61. * @returns {Required} The parsed Required object.
  62. */
  63. static fromJSON(json){
  64. if(json == null){
  65. return new Required(true, true)
  66. } else {
  67. return new Required(json.value == null ? true : json.value, json.def == null ? true : json.def)
  68. }
  69. }
  70. constructor(value, def){
  71. this.value = value
  72. this.default = def
  73. }
  74. /**
  75. * Get the default value for a required object. If a module
  76. * is not required, this value determines whether or not
  77. * it is enabled by default.
  78. *
  79. * @returns {boolean} The default enabled value.
  80. */
  81. isDefault(){
  82. return this.default
  83. }
  84. /**
  85. * @returns {boolean} Whether or not the module is required.
  86. */
  87. isRequired(){
  88. return this.value
  89. }
  90. }
  91. exports.Required
  92. /**
  93. * Represents a module.
  94. */
  95. class Module {
  96. /**
  97. * Parse a JSON object into a Module.
  98. *
  99. * @param {Object} json A JSON object representing a Module.
  100. * @param {string} serverid The ID of the server to which this module belongs.
  101. *
  102. * @returns {Module} The parsed Module.
  103. */
  104. static fromJSON(json, serverid){
  105. return new Module(json.id, json.name, json.type, json.required, json.artifact, json.subModules, serverid)
  106. }
  107. /**
  108. * Resolve the default extension for a specific module type.
  109. *
  110. * @param {string} type The type of the module.
  111. *
  112. * @return {string} The default extension for the given type.
  113. */
  114. static _resolveDefaultExtension(type){
  115. switch (type) {
  116. case exports.Types.Library:
  117. case exports.Types.ForgeHosted:
  118. case exports.Types.LiteLoader:
  119. case exports.Types.ForgeMod:
  120. return 'jar'
  121. case exports.Types.LiteMod:
  122. return 'litemod'
  123. case exports.Types.File:
  124. default:
  125. return 'jar' // There is no default extension really.
  126. }
  127. }
  128. constructor(id, name, type, required, artifact, subModules, serverid) {
  129. this.identifier = id
  130. this.type = type
  131. this._resolveMetaData()
  132. this.name = name
  133. this.required = Required.fromJSON(required)
  134. this.artifact = Artifact.fromJSON(artifact)
  135. this._resolveArtifactPath(artifact.path, serverid)
  136. this._resolveSubModules(subModules, serverid)
  137. }
  138. _resolveMetaData(){
  139. try {
  140. const m0 = this.identifier.split('@')
  141. this.artifactExt = m0[1] || Module._resolveDefaultExtension(this.type)
  142. const m1 = m0[0].split(':')
  143. this.artifactClassifier = m1[3] || undefined
  144. this.artifactVersion = m1[2] || '???'
  145. this.artifactID = m1[1] || '???'
  146. this.artifactGroup = m1[0] || '???'
  147. } catch (err) {
  148. // Improper identifier
  149. logger.error('Improper ID for module', this.identifier, err)
  150. }
  151. }
  152. _resolveArtifactPath(artifactPath, serverid){
  153. const pth = artifactPath == null ? path.join(...this.getGroup().split('.'), this.getID(), this.getVersion(), `${this.getID()}-${this.getVersion()}${this.artifactClassifier != undefined ? `-${this.artifactClassifier}` : ''}.${this.getExtension()}`) : artifactPath
  154. switch (this.type){
  155. case exports.Types.Library:
  156. case exports.Types.ForgeHosted:
  157. case exports.Types.LiteLoader:
  158. this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'libraries', pth)
  159. break
  160. case exports.Types.ForgeMod:
  161. case exports.Types.LiteMod:
  162. this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'modstore', pth)
  163. break
  164. case exports.Types.VersionManifest:
  165. this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'versions', this.getIdentifier(), `${this.getIdentifier()}.json`)
  166. break
  167. case exports.Types.VersionJar:
  168. this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'versions', this.getIdentifier(), `${this.getIdentifier()}.jar`)
  169. break
  170. case exports.Types.File:
  171. default:
  172. this.artifact.path = path.join(ConfigManager.getInstanceDirectory(), serverid, pth)
  173. break
  174. }
  175. }
  176. _resolveSubModules(json, serverid){
  177. const arr = []
  178. if(json != null){
  179. for(let sm of json){
  180. arr.push(Module.fromJSON(sm, serverid))
  181. }
  182. }
  183. this.subModules = arr.length > 0 ? arr : null
  184. }
  185. /**
  186. * @returns {string} The full, unparsed module identifier.
  187. */
  188. getIdentifier(){
  189. return this.identifier
  190. }
  191. /**
  192. * @returns {string} The name of the module.
  193. */
  194. getName(){
  195. return this.name
  196. }
  197. /**
  198. * @returns {Required} The required object declared by this module.
  199. */
  200. getRequired(){
  201. return this.required
  202. }
  203. /**
  204. * @returns {Artifact} The artifact declared by this module.
  205. */
  206. getArtifact(){
  207. return this.artifact
  208. }
  209. /**
  210. * @returns {string} The maven identifier of this module's artifact.
  211. */
  212. getID(){
  213. return this.artifactID
  214. }
  215. /**
  216. * @returns {string} The maven group of this module's artifact.
  217. */
  218. getGroup(){
  219. return this.artifactGroup
  220. }
  221. /**
  222. * @returns {string} The identifier without he version or extension.
  223. */
  224. getVersionlessID(){
  225. return this.getGroup() + ':' + this.getID()
  226. }
  227. /**
  228. * @returns {string} The identifier without the extension.
  229. */
  230. getExtensionlessID(){
  231. return this.getIdentifier().split('@')[0]
  232. }
  233. /**
  234. * @returns {string} The version of this module's artifact.
  235. */
  236. getVersion(){
  237. return this.artifactVersion
  238. }
  239. /**
  240. * @returns {string} The classifier of this module's artifact
  241. */
  242. getClassifier(){
  243. return this.artifactClassifier
  244. }
  245. /**
  246. * @returns {string} The extension of this module's artifact.
  247. */
  248. getExtension(){
  249. return this.artifactExt
  250. }
  251. /**
  252. * @returns {boolean} Whether or not this module has sub modules.
  253. */
  254. hasSubModules(){
  255. return this.subModules != null
  256. }
  257. /**
  258. * @returns {Array.<Module>} An array of sub modules.
  259. */
  260. getSubModules(){
  261. return this.subModules
  262. }
  263. /**
  264. * @returns {string} The type of the module.
  265. */
  266. getType(){
  267. return this.type
  268. }
  269. }
  270. exports.Module
  271. /**
  272. * Represents a server configuration.
  273. */
  274. class Server {
  275. /**
  276. * Parse a JSON object into a Server.
  277. *
  278. * @param {Object} json A JSON object representing a Server.
  279. *
  280. * @returns {Server} The parsed Server object.
  281. */
  282. static fromJSON(json){
  283. const mdls = json.modules
  284. json.modules = []
  285. const serv = Object.assign(new Server(), json)
  286. serv._resolveModules(mdls)
  287. return serv
  288. }
  289. _resolveModules(json){
  290. const arr = []
  291. for(let m of json){
  292. arr.push(Module.fromJSON(m, this.getID()))
  293. }
  294. this.modules = arr
  295. }
  296. /**
  297. * @returns {string} The ID of the server.
  298. */
  299. getID(){
  300. return this.id
  301. }
  302. /**
  303. * @returns {string} The name of the server.
  304. */
  305. getName(){
  306. return this.name
  307. }
  308. /**
  309. * @returns {string} The description of the server.
  310. */
  311. getDescription(){
  312. return this.description
  313. }
  314. /**
  315. * @returns {string} The URL of the server's icon.
  316. */
  317. getIcon(){
  318. return this.icon
  319. }
  320. /**
  321. * @returns {string} The version of the server configuration.
  322. */
  323. getVersion(){
  324. return this.version
  325. }
  326. /**
  327. * @returns {string} The IP address of the server.
  328. */
  329. getAddress(){
  330. return this.address
  331. }
  332. /**
  333. * @returns {string} The minecraft version of the server.
  334. */
  335. getMinecraftVersion(){
  336. return this.minecraftVersion
  337. }
  338. /**
  339. * @returns {boolean} Whether or not this server is the main
  340. * server. The main server is selected by the launcher when
  341. * no valid server is selected.
  342. */
  343. isMainServer(){
  344. return this.mainServer
  345. }
  346. /**
  347. * @returns {boolean} Whether or not the server is autoconnect.
  348. * by default.
  349. */
  350. isAutoConnect(){
  351. return this.autoconnect
  352. }
  353. /**
  354. * @returns {Array.<Module>} An array of modules for this server.
  355. */
  356. getModules(){
  357. return this.modules
  358. }
  359. }
  360. exports.Server
  361. /**
  362. * Represents the Distribution Index.
  363. */
  364. class DistroIndex {
  365. /**
  366. * Parse a JSON object into a DistroIndex.
  367. *
  368. * @param {Object} json A JSON object representing a DistroIndex.
  369. *
  370. * @returns {DistroIndex} The parsed Server object.
  371. */
  372. static fromJSON(json){
  373. const servers = json.servers
  374. json.servers = []
  375. const distro = Object.assign(new DistroIndex(), json)
  376. distro._resolveServers(servers)
  377. distro._resolveMainServer()
  378. return distro
  379. }
  380. _resolveServers(json){
  381. const arr = []
  382. for(let s of json){
  383. arr.push(Server.fromJSON(s))
  384. }
  385. this.servers = arr
  386. }
  387. _resolveMainServer(){
  388. for(let serv of this.servers){
  389. if(serv.mainServer){
  390. this.mainServer = serv.id
  391. return
  392. }
  393. }
  394. // If no server declares default_selected, default to the first one declared.
  395. this.mainServer = (this.servers.length > 0) ? this.servers[0].getID() : null
  396. }
  397. /**
  398. * @returns {string} The version of the distribution index.
  399. */
  400. getVersion(){
  401. return this.version
  402. }
  403. /**
  404. * @returns {string} The URL to the news RSS feed.
  405. */
  406. getRSS(){
  407. return this.rss
  408. }
  409. /**
  410. * @returns {Array.<Server>} An array of declared server configurations.
  411. */
  412. getServers(){
  413. return this.servers
  414. }
  415. /**
  416. * Get a server configuration by its ID. If it does not
  417. * exist, null will be returned.
  418. *
  419. * @param {string} id The ID of the server.
  420. *
  421. * @returns {Server} The server configuration with the given ID or null.
  422. */
  423. getServer(id){
  424. for(let serv of this.servers){
  425. if(serv.id === id){
  426. return serv
  427. }
  428. }
  429. return null
  430. }
  431. /**
  432. * Get the main server.
  433. *
  434. * @returns {Server} The main server.
  435. */
  436. getMainServer(){
  437. return this.mainServer != null ? this.getServer(this.mainServer) : null
  438. }
  439. }
  440. exports.DistroIndex
  441. exports.Types = {
  442. Library: 'Library',
  443. ForgeHosted: 'ForgeHosted',
  444. Forge: 'Forge', // Unimplemented
  445. LiteLoader: 'LiteLoader',
  446. ForgeMod: 'ForgeMod',
  447. LiteMod: 'LiteMod',
  448. File: 'File',
  449. VersionManifest: 'VersionManifest',
  450. VersionJar: 'VersionJar'
  451. }
  452. let DEV_MODE = false
  453. const DISTRO_PATH = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json')
  454. const DEV_PATH = path.join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json')
  455. let data = null
  456. /**
  457. * @returns {Promise.<DistroIndex>}
  458. */
  459. exports.pullRemote = function(){
  460. if(DEV_MODE){
  461. return exports.pullLocal()
  462. }
  463. return new Promise((resolve, reject) => {
  464. const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'
  465. //const distroURL = 'https://gist.githubusercontent.com/dscalzi/53b1ba7a11d26a5c353f9d5ae484b71b/raw/'
  466. const opts = {
  467. url: distroURL,
  468. timeout: 2500
  469. }
  470. const distroDest = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json')
  471. request(opts, (error, resp, body) => {
  472. if(!error){
  473. try {
  474. data = DistroIndex.fromJSON(JSON.parse(body))
  475. } catch (e) {
  476. reject(e)
  477. }
  478. fs.writeFile(distroDest, body, 'utf-8', (err) => {
  479. if(!err){
  480. resolve(data)
  481. } else {
  482. reject(err)
  483. }
  484. })
  485. } else {
  486. reject(error)
  487. }
  488. })
  489. })
  490. }
  491. /**
  492. * @returns {Promise.<DistroIndex>}
  493. */
  494. exports.pullLocal = function(){
  495. return new Promise((resolve, reject) => {
  496. fs.readFile(DEV_MODE ? DEV_PATH : DISTRO_PATH, 'utf-8', (err, d) => {
  497. if(!err){
  498. data = DistroIndex.fromJSON(JSON.parse(d))
  499. resolve(data)
  500. } else {
  501. reject(err)
  502. }
  503. })
  504. })
  505. }
  506. exports.setDevMode = function(value){
  507. if(value){
  508. logger.log('Developer mode enabled.')
  509. logger.log('If you don\'t know what that means, revert immediately.')
  510. } else {
  511. logger.log('Developer mode disabled.')
  512. }
  513. DEV_MODE = value
  514. }
  515. exports.isDevMode = function(){
  516. return DEV_MODE
  517. }
  518. /**
  519. * @returns {DistroIndex}
  520. */
  521. exports.getDistribution = function(){
  522. return data
  523. }