processbuilder.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. /**
  2. * The initial iteration of this file will not support optional submodules.
  3. * Support will be added down the line, only top-level modules will recieve optional support.
  4. *
  5. *
  6. * TODO why are logs not working??????
  7. */
  8. const AdmZip = require('adm-zip')
  9. const {AssetGuard, Library} = require('./assetguard.js')
  10. const child_process = require('child_process')
  11. const {DEFAULT_CONFIG} = require('./constants')
  12. const fs = require('fs')
  13. const mkpath = require('mkdirp')
  14. const path = require('path')
  15. const {URL} = require('url')
  16. class ProcessBuilder {
  17. constructor(gameDirectory, distroServer, versionData, forgeData, authUser){
  18. this.dir = gameDirectory
  19. this.server = distroServer
  20. this.versionData = versionData
  21. this.forgeData = forgeData
  22. this.authUser = authUser
  23. this.fmlDir = path.join(this.dir, 'versions', this.server.id + '.json')
  24. this.libPath = path.join(this.dir, 'libraries')
  25. }
  26. static shouldInclude(mdle){
  27. //If the module should be included by default
  28. return mdle.required == null || mdle.required.value == null || mdle.required.value === true || (mdle.required.value === false && (mdle.required.def == null || mdle.required.def === true))
  29. }
  30. /**
  31. * Convienence method to run the functions typically used to build a process.
  32. */
  33. build(){
  34. process.throwDeprecation = true
  35. const mods = this.resolveDefaultMods()
  36. this.constructFMLModList(mods, true)
  37. const args = this.constructJVMArguments(mods)
  38. console.log(args)
  39. const child = child_process.spawn(DEFAULT_CONFIG.getJavaExecutable(), args)
  40. child.stdout.on('data', (data) => {
  41. console.log('Minecraft:', data.toString('utf8'))
  42. })
  43. child.stderr.on('data', (data) => {
  44. console.log('Minecraft:', data.toString('utf8'))
  45. })
  46. child.on('close', (code, signal) => {
  47. console.log('Exited with code', code)
  48. })
  49. return child
  50. }
  51. resolveDefaultMods(options = {type: 'forgemod'}){
  52. //Returns array of default forge mods to load.
  53. const mods = []
  54. const mdles = this.server.modules
  55. for(let i=0; i<mdles.length; ++i){
  56. if(mdles[i].type != null && mdles[i].type === options.type){
  57. if(ProcessBuilder.shouldInclude(mdles[i])){
  58. mods.push(mdles[i])
  59. }
  60. }
  61. }
  62. return mods
  63. }
  64. constructFMLModList(mods, save = false){
  65. const modList = {}
  66. modList.repositoryRoot = path.join(this.dir, 'modstore')
  67. const ids = []
  68. for(let i=0; i<mods.length; ++i){
  69. ids.push(mods[i].id)
  70. }
  71. modList.modRef = ids
  72. if(save){
  73. const json = JSON.stringify(modList, null, 4)
  74. fs.writeFileSync(this.fmlDir, json, 'UTF-8')
  75. }
  76. return modList
  77. }
  78. /**
  79. * Construct the argument array that will be passed to the JVM process.
  80. *
  81. * @param {Array.<Object>} mods - An array of enabled mods which will be launched with this process.
  82. * @returns {Array.<String>} - An array containing the full JVM arguments for this process.
  83. */
  84. constructJVMArguments(mods){
  85. let args = ['-Xmx' + DEFAULT_CONFIG.getMaxRAM(),
  86. '-Xms' + DEFAULT_CONFIG.getMinRAM(),,
  87. '-Djava.library.path=' + path.join(this.dir, 'natives'),
  88. '-cp',
  89. this.classpathArg(mods).join(';'),
  90. this.forgeData.mainClass]
  91. // For some reason this will add an undefined value unless
  92. // the delete count is 1. I suspect this is unintended behavior
  93. // by the function.. need to keep an eye on this.
  94. args.splice(2, 1, ...DEFAULT_CONFIG.getJVMOptions())
  95. args = args.concat(this._resolveForgeArgs())
  96. return args
  97. }
  98. /**
  99. * Resolve the arguments required by forge.
  100. *
  101. * @returns {Array.<String>} - An array containing the arguments required by forge.
  102. */
  103. _resolveForgeArgs(){
  104. const mcArgs = this.forgeData.minecraftArguments.split(' ')
  105. const argDiscovery = /\${*(.*)}/
  106. // Replace the declared variables with their proper values.
  107. for(let i=0; i<mcArgs.length; ++i){
  108. if(argDiscovery.test(mcArgs[i])){
  109. const identifier = mcArgs[i].match(argDiscovery)[1]
  110. let val = null;
  111. switch(identifier){
  112. case 'auth_player_name':
  113. val = this.authUser.selectedProfile.name
  114. break
  115. case 'version_name':
  116. //val = versionData.id
  117. val = this.server.id
  118. break
  119. case 'game_directory':
  120. val = this.dir
  121. break
  122. case 'assets_root':
  123. val = path.join(this.dir, 'assets')
  124. break
  125. case 'assets_index_name':
  126. val = this.versionData.assets
  127. break
  128. case 'auth_uuid':
  129. val = this.authUser.selectedProfile.id
  130. break
  131. case 'auth_access_token':
  132. val = this.authUser.accessToken
  133. break
  134. case 'user_type':
  135. val = 'MOJANG'
  136. break
  137. case 'version_type':
  138. val = this.versionData.type
  139. break
  140. }
  141. if(val != null){
  142. mcArgs[i] = val;
  143. }
  144. }
  145. }
  146. mcArgs.push('--modListFile')
  147. mcArgs.push('absolute:' + this.fmlDir)
  148. // Prepare game resolution
  149. if(DEFAULT_CONFIG.isFullscreen()){
  150. mcArgs.unshift('--fullscreen')
  151. } else {
  152. mcArgs.unshift(DEFAULT_CONFIG.getGameWidth())
  153. mcArgs.unshift('--width')
  154. mcArgs.unshift(DEFAULT_CONFIG.getGameHeight())
  155. mcArgs.unshift('--height')
  156. }
  157. // Prepare autoconnect
  158. if(DEFAULT_CONFIG.isAutoConnect() && this.server.autoconnect){
  159. const serverURL = new URL('my://' + this.server.server_ip)
  160. mcArgs.unshift(serverURL.hostname)
  161. mcArgs.unshift('--server')
  162. if(serverURL.port){
  163. mcArgs.unshift(serverURL.port)
  164. mcArgs.unshift('--port')
  165. }
  166. }
  167. return mcArgs
  168. }
  169. /**
  170. * Resolve the full classpath argument list for this process. This method will resolve all Mojang-declared
  171. * libraries as well as the libraries declared by the server. Since mods are permitted to declare libraries,
  172. * this method requires all enabled mods as an input
  173. *
  174. * @param {Array.<Object>} mods - An array of enabled mods which will be launched with this process.
  175. * @returns {Array.<String>} - An array containing the paths of each library required by this process.
  176. */
  177. classpathArg(mods){
  178. let cpArgs = []
  179. // Add the version.jar to the classpath.
  180. const version = this.versionData.id
  181. cpArgs.push(path.join(this.dir, 'versions', version, version + '.jar'))
  182. // Resolve the Mojang declared libraries.
  183. const mojangLibs = this._resolveMojangLibraries()
  184. cpArgs = cpArgs.concat(mojangLibs)
  185. // Resolve the server declared libraries.
  186. const servLibs = this._resolveServerLibraries(mods)
  187. cpArgs = cpArgs.concat(servLibs)
  188. return cpArgs
  189. }
  190. /**
  191. * Resolve the libraries defined by Mojang's version data. This method will also extract
  192. * native libraries and point to the correct location for its classpath.
  193. *
  194. * TODO - clean up function
  195. *
  196. * @returns {Array.<String>} - An array containing the paths of each library mojang declares.
  197. */
  198. _resolveMojangLibraries(){
  199. const libs = []
  200. const libArr = this.versionData.libraries
  201. const nativePath = path.join(this.dir, 'natives')
  202. for(let i=0; i<libArr.length; i++){
  203. const lib = libArr[i]
  204. if(Library.validateRules(lib.rules)){
  205. if(lib.natives == null){
  206. const dlInfo = lib.downloads
  207. const artifact = dlInfo.artifact
  208. const to = path.join(this.libPath, artifact.path)
  209. libs.push(to)
  210. } else {
  211. // Extract the native library.
  212. const natives = lib.natives
  213. const extractInst = lib.extract
  214. const exclusionArr = extractInst.exclude
  215. const opSys = Library.mojangFriendlyOS()
  216. const indexId = natives[opSys]
  217. const dlInfo = lib.downloads
  218. const classifiers = dlInfo.classifiers
  219. const artifact = classifiers[indexId]
  220. // Location of native zip.
  221. const to = path.join(this.libPath, artifact.path)
  222. let zip = new AdmZip(to)
  223. let zipEntries = zip.getEntries()
  224. // Unzip the native zip.
  225. for(let i=0; i<zipEntries.length; i++){
  226. const fileName = zipEntries[i].entryName
  227. let shouldExclude = false
  228. // Exclude noted files.
  229. exclusionArr.forEach(function(exclusion){
  230. if(fileName.indexOf(exclusion) > -1){
  231. shouldExclude = true
  232. }
  233. })
  234. // Extract the file.
  235. if(!shouldExclude){
  236. mkpath.sync(path.join(nativePath, fileName, '..'))
  237. fs.writeFile(path.join(nativePath, fileName), zipEntries[i].getData(), (err) => {
  238. if(err){
  239. console.error('Error while extracting native library:', err)
  240. }
  241. })
  242. }
  243. }
  244. libs.push(to)
  245. }
  246. }
  247. }
  248. return libs
  249. }
  250. /**
  251. * Resolve the libraries declared by this server in order to add them to the classpath.
  252. * This method will also check each enabled mod for libraries, as mods are permitted to
  253. * declare libraries.
  254. *
  255. * @param {Array.<Object>} mods - An array of enabled mods which will be launched with this process.
  256. * @returns {Array.<String>} - An array containing the paths of each library this server requires.
  257. */
  258. _resolveServerLibraries(mods){
  259. const mdles = this.server.modules
  260. let libs = []
  261. // Locate Forge/Libraries
  262. for(let i=0; i<mdles.length; i++){
  263. if(mdles[i].type != null && (mdles[i].type === 'forge-hosted' || mdles[i].type === 'library')){
  264. let lib = mdles[i]
  265. libs.push(path.join(this.libPath, lib.artifact.path == null ? AssetGuard._resolvePath(lib.id, lib.artifact.extension) : lib.artifact.path))
  266. if(lib.sub_modules != null){
  267. const res = this._resolveModuleLibraries(lib)
  268. if(res.length > 0){
  269. libs = libs.concat(res)
  270. }
  271. }
  272. }
  273. }
  274. //Check for any libraries in our mod list.
  275. for(let i=0; i<mods.length; i++){
  276. if(mods.sub_modules != null){
  277. const res = this._resolveModuleLibraries(mods[i])
  278. if(res.length > 0){
  279. libs = libs.concat(res)
  280. }
  281. }
  282. }
  283. return libs
  284. }
  285. /**
  286. * Recursively resolve the path of each library required by this module.
  287. *
  288. * @param {Object} mdle - A module object from the server distro index.
  289. * @returns {Array.<String>} - An array containing the paths of each library this module requires.
  290. */
  291. _resolveModuleLibraries(mdle){
  292. if(mdle.sub_modules == null){
  293. return []
  294. }
  295. let libs = []
  296. for(let i=0; i<mdle.sub_modules.length; i++){
  297. const sm = mdle.sub_modules[i]
  298. if(sm.type != null && sm.type == 'library'){
  299. libs.push(path.join(this.libPath, sm.artifact.path == null ? AssetGuard._resolvePath(sm.id, sm.artifact.extension) : sm.artifact.path))
  300. }
  301. // If this module has submodules, we need to resolve the libraries for those.
  302. // To avoid unnecessary recursive calls, base case is checked here.
  303. if(mdle.sub_modules != null){
  304. const res = this._resolveModuleLibraries(sm)
  305. if(res.length > 0){
  306. libs = libs.concat(res)
  307. }
  308. }
  309. }
  310. return libs
  311. }
  312. }
  313. module.exports = ProcessBuilder