Remove some old code, apply fix from master.

Reference to the old code will be made from the master branch.
This commit is contained in:
Daniel Scalzi 2020-05-24 15:50:23 -04:00
parent 9a67087766
commit dc7386f19d
No known key found for this signature in database
GPG Key ID: D18EA3FB4B142A57
16 changed files with 23 additions and 4366 deletions

18
package-lock.json generated
View File

@ -5290,9 +5290,9 @@
"dev": true "dev": true
}, },
"eslint": { "eslint": {
"version": "7.0.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.1.0.tgz",
"integrity": "sha512-qY1cwdOxMONHJfGqw52UOpZDeqXy8xmD0u8CT6jIstil72jkhURC704W8CFyTPDPllz4z4lu0Ql1+07PG/XdIg==", "integrity": "sha512-DfS3b8iHMK5z/YLSme8K5cge168I8j8o1uiVmFCgnnjxZQbCGyraF8bMl7Ju4yfBmCuxD7shOF7eqGkcuIHfsA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.0.0", "@babel/code-frame": "^7.0.0",
@ -6990,9 +6990,9 @@
} }
}, },
"glob-parent": { "glob-parent": {
"version": "5.1.0", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
@ -8862,9 +8862,9 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
}, },
"mocha": { "mocha": {
"version": "7.1.2", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz",
"integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==", "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-colors": "3.2.3", "ansi-colors": "3.2.3",

View File

@ -68,9 +68,9 @@
"electron-builder": "^22.6.1", "electron-builder": "^22.6.1",
"electron-webpack": "^2.8.2", "electron-webpack": "^2.8.2",
"electron-webpack-ts": "^4.0.1", "electron-webpack-ts": "^4.0.1",
"eslint": "^7.0.0", "eslint": "^7.1.0",
"helios-distribution-types": "1.0.0-pre.1", "helios-distribution-types": "1.0.0-pre.1",
"mocha": "^7.1.2", "mocha": "^7.2.0",
"nock": "^12.0.3", "nock": "^12.0.3",
"react": "^16.13.0", "react": "^16.13.0",
"react-dom": "^16.13.0", "react-dom": "^16.13.0",

View File

@ -1,4 +1,4 @@
import { Rule, Natives } from "../../main/asset/model/mojang/VersionJson" import { Rule, Natives } from "../asset/model/mojang/VersionJson"
export function getMojangOS(): string { export function getMojangOS(): string {
const opSys = process.platform const opSys = process.platform

View File

@ -230,16 +230,19 @@ function createMenu() {
} }
function getPlatformIcon(filename: string){ function getPlatformIcon(filename: string){
const opSys = process.platform let ext
if (opSys === 'darwin') { switch(process.platform) {
filename = filename + '.icns' case 'win32':
} else if (opSys === 'win32') { ext = 'ico'
filename = filename + '.ico' break
} else { case 'darwin':
filename = filename + '.png' case 'linux':
default:
ext = 'png'
break
} }
return join(__dirname, '..', 'assets', 'images', filename) return join(__dirname, '..', 'assets', 'images', `${filename}.${ext}`)
} }
app.on('ready', createWindow) app.on('ready', createWindow)

View File

@ -1,69 +0,0 @@
let target = require('./assetguard')[process.argv[2]]
if(target == null){
process.send!({context: 'error', data: null, error: 'Invalid class name'})
console.error('Invalid class name passed to argv[2], cannot continue.')
process.exit(1)
}
let tracker = new target(...(process.argv.splice(3)))
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
//const tracker = new AssetGuard(process.argv[2], process.argv[3])
console.log('AssetExec Started')
// Temporary for debug purposes.
process.on('unhandledRejection', r => console.log(r))
function assignListeners(){
tracker.on('validate', (data: any) => {
process.send!({context: 'validate', data})
})
tracker.on('progress', (data: any, acc: number, total: number) => {
process.send!({context: 'progress', data, value: acc, total, percent: parseInt(((acc/total)*100) as unknown as string)})
})
tracker.on('complete', (data: any, ...args: any[]) => {
process.send!({context: 'complete', data, args})
})
tracker.on('error', (data: any, error: any) => {
process.send!({context: 'error', data, error})
})
}
assignListeners()
process.on('message', (msg) => {
if(msg.task === 'execute'){
const func = msg.function
let nS = tracker[func] // Nonstatic context
let iS = target[func] // Static context
if(typeof nS === 'function' || typeof iS === 'function'){
const f = typeof nS === 'function' ? nS : iS
const res = f.apply(f === nS ? tracker : null, msg.argsArr)
if(res instanceof Promise){
res.then((v) => {
process.send!({result: v, context: func})
}).catch((err) => {
process.send!({result: err.message || err, context: func})
})
} else {
process.send!({result: res, context: func})
}
} else {
process.send!({context: 'error', data: null, error: `Function ${func} not found on ${process.argv[2]}`})
}
} else if(msg.task === 'changeContext'){
target = require('./assetguard')[msg.class]
if(target == null){
process.send!({context: 'error', data: null, error: `Invalid class ${msg.class}`})
} else {
tracker = new target(...(msg.args))
assignListeners()
}
}
})
process.on('disconnect', () => {
console.log('AssetExec Disconnected')
process.exit(0)
})

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +0,0 @@
import { LoggerUtil } from './loggerutil'
import { ConfigManager } from '../../common/config/configmanager'
import { Mojang } from '../../common/mojang/mojang'
import { SavedAccount } from '../../common/config/model/SavedAccount'
/**
* AuthManager
*
* This module aims to abstract login procedures. Results from Mojang's REST api
* are retrieved through our Mojang module. These results are processed and stored,
* if applicable, in the config using the ConfigManager. All login procedures should
* be made through this module.
*
* @module authmanager
*/
const logger = new LoggerUtil('%c[AuthManager]', 'color: #a02d2a; font-weight: bold')
const loggerSuccess = new LoggerUtil('%c[AuthManager]', 'color: #209b07; font-weight: bold')
// Functions
/**
* Add an account. This will authenticate the given credentials with Mojang's
* authserver. The resultant data will be stored as an auth account in the
* configuration database.
*
* @param {string} username The account username (email if migrated).
* @param {string} password The account password.
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
*/
exports.addAccount = async function(username: string, password: string){
try {
const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken())
if(session.selectedProfile != null){
const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
if(ConfigManager.getClientToken() == null){
ConfigManager.setClientToken(session.clientToken)
}
ConfigManager.save()
return ret
} else {
throw new Error('NotPaidAccount')
}
} catch (err){
return Promise.reject(err)
}
}
/**
* Remove an account. This will invalidate the access token associated
* with the account and then remove it from the database.
*
* @param {string} uuid The UUID of the account to be removed.
* @returns {Promise.<void>} Promise which resolves to void when the action is complete.
*/
exports.removeAccount = async function(uuid: string){
try {
const authAcc = ConfigManager.getAuthAccount(uuid)
await Mojang.invalidate(authAcc.accessToken, ConfigManager.getClientToken() as string)
ConfigManager.removeAuthAccount(uuid)
ConfigManager.save()
return Promise.resolve()
} catch (err){
return Promise.reject(err)
}
}
/**
* Validate the selected account with Mojang's authserver. If the account is not valid,
* we will attempt to refresh the access token and update that value. If that fails, a
* new login will be required.
*
* **Function is WIP**
*
* @returns {Promise.<boolean>} Promise which resolves to true if the access token is valid,
* otherwise false.
*/
exports.validateSelected = async function(){
const current = ConfigManager.getSelectedAccount() as SavedAccount
const isValid = await Mojang.validate(current.accessToken, ConfigManager.getClientToken() as string)
if(!isValid){
try {
const session = await Mojang.refresh(current.accessToken, ConfigManager.getClientToken() as string)
ConfigManager.updateAuthAccount(current.uuid, session.accessToken)
ConfigManager.save()
} catch(err) {
logger.debug('Error while validating selected profile:', err)
if(err && err.error === 'ForbiddenOperationException'){
// What do we do?
}
logger.log('Account access token is invalid.')
return false
}
loggerSuccess.log('Account access token validated.')
return true
} else {
loggerSuccess.log('Account access token validated.')
return true
}
}

View File

@ -1,50 +0,0 @@
import { LoggerUtil } from './loggerutil'
import { Client, Presence } from 'discord-rpc'
// Work in progress
const logger = new LoggerUtil('%c[DiscordWrapper]', 'color: #7289da; font-weight: bold')
let client: Client
let activity: Presence
// TODO types for these settings
export function initRPC(genSettings: any, servSettings: any, initialDetails = 'Waiting for Client..'){
client = new Client({ transport: 'ipc' })
activity = {
details: initialDetails,
state: 'Server: ' + servSettings.shortId,
largeImageKey: servSettings.largeImageKey,
largeImageText: servSettings.largeImageText,
smallImageKey: genSettings.smallImageKey,
smallImageText: genSettings.smallImageText,
startTimestamp: new Date().getTime(),
instance: false
}
client.on('ready', () => {
logger.log('Discord RPC Connected')
client.setActivity(activity)
})
client.login({clientId: genSettings.clientId}).catch(error => {
if(error.message.includes('ENOENT')) {
logger.log('Unable to initialize Discord Rich Presence, no client detected.')
} else {
logger.log('Unable to initialize Discord Rich Presence: ' + error.message, error)
}
})
}
export function updateDetails(details: string){
activity.details = details
client.setActivity(activity)
}
export function shutdownRPC(){
if(!client) return
client.clearActivity()
client.destroy()
client = null as unknown as Client // TODO cleanup
activity = null as unknown as Presence // TODO cleanup
}

View File

@ -1,296 +0,0 @@
import request from 'request'
import { Distribution, Module, Type, TypeMetadata, Server } from 'helios-distribution-types'
import { readJson, writeJson } from 'fs-extra'
import { join } from 'path'
import { LoggerUtil } from './loggerutil'
import { ConfigManager } from '../../common/config/configmanager'
const logger = new LoggerUtil('%c[DistroManager]', 'color: #a02d2a; font-weight: bold')
interface ArtifactMeta {
group: string
artifact: string
version: string
classifier?: string
extension: string
}
export class ModuleWrapper {
private artifactMeta: ArtifactMeta
private subModules: ModuleWrapper[] = []
constructor(public module: Module, private serverId: string) {
this.artifactMeta = this.resolveMetaData()
this.resolveArtifactPath()
this.resolveRequired()
if (this.module.subModules != null) {
this.subModules = this.module.subModules.map(mdl => new ModuleWrapper(mdl, serverId))
}
}
private resolveMetaData(): ArtifactMeta {
try {
const m0 = this.module.id.split('@')
const m1 = m0[0].split(':')
return {
group: m1[0] || '???',
artifact: m1[1] || '???',
version: m1[2] || '???',
classifier: m1[3] || undefined,
extension: m0[1] || TypeMetadata[this.module.type].defaultExtension || 'undefined'
}
} catch (err) {
logger.error('Improper ID for module', this.module.id, err)
return {
group: '???',
artifact: '???',
version: '???',
classifier: undefined,
extension: '???'
}
}
}
private resolveArtifactPath(): void {
const relativePath = this.module.artifact.path == null ? join(
...this.artifactMeta.group.split('.'),
this.artifactMeta.artifact,
this.artifactMeta.version,
`${this.artifactMeta.artifact}-${this.artifactMeta.version}${this.artifactMeta.classifier != undefined ? `-${this.artifactMeta.classifier}` : ''}.${this.artifactMeta.extension}`
) : this.module.artifact.path
switch (this.module.type){
case Type.Library:
case Type.ForgeHosted:
case Type.LiteLoader:
this.module.artifact.path = join(ConfigManager.getCommonDirectory(), 'libraries', relativePath)
break
case Type.ForgeMod:
case Type.LiteMod:
this.module.artifact.path = join(ConfigManager.getCommonDirectory(), 'modstore', relativePath)
break
case Type.VersionManifest:
this.module.artifact.path = join(ConfigManager.getCommonDirectory(), 'versions', this.module.id, `${this.module.id}.json`)
break
case Type.File:
default:
this.module.artifact.path = join(ConfigManager.getInstanceDirectory(), this.serverId, relativePath)
break
}
}
private resolveRequired(): void {
if (this.module.required == null) {
this.module.required = {
value: true,
def: true
}
} else {
if (this.module.required.value == null) {
this.module.required.value = true
}
if (this.module.required.def == null) {
this.module.required.def = true
}
}
}
/**
* @returns {string} The maven identifier of this module's artifact.
*/
public getArtifact(): string {
return this.artifactMeta.artifact
}
/**
* @returns {string} The maven group of this module's artifact.
*/
public getGroup(): string {
return this.artifactMeta.group
}
/**
* @returns {string} The version of this module's artifact.
*/
public getVersion(): string {
return this.artifactMeta.version
}
/**
* @returns {string | undefined} The classifier of this module's artifact
*/
public getClassifier(): string | undefined {
return this.artifactMeta.classifier
}
/**
* @returns {string} The extension of this module's artifact.
*/
public getExtension(): string {
return this.artifactMeta.extension
}
/**
* @returns {string} The identifier without he version or extension.
*/
public getVersionlessID(): string {
return this.artifactMeta.group + ':' + this.artifactMeta.artifact
}
/**
* @returns {string} The identifier without the extension.
*/
public getExtensionlessID(): string {
return this.module.id.split('@')[0]
}
/**
* @returns {boolean} Whether or not this module has sub modules.
*/
public hasSubModules(): boolean {
return this.module.subModules != null
}
public getWrappedSubmodules(): ModuleWrapper[] {
return this.subModules
}
}
export class ServerWrapper {
private modules: ModuleWrapper[] = []
constructor(public server: Server) {
this.server.modules.map(mdl => new ModuleWrapper(mdl, server.id))
}
public getWrappedModules() {
return this.modules
}
}
export class DistributionWrapper {
private mainServer: ServerWrapper | null = null
private servers: ServerWrapper[]
constructor(public distro: Distribution) {
this.servers = this.distro.servers.map(serv => new ServerWrapper(serv))
this.resolveMainServer()
}
private resolveMainServer(): void {
for(const serv of this.servers){
if(serv.server.mainServer){
this.mainServer = serv
return
}
}
// If no server declares default_selected, default to the first one declared.
this.mainServer = (this.servers.length > 0) ? this.servers[0] : null
}
public getServer(id: string): ServerWrapper | null {
for(const serv of this.servers){
if(serv.server.id === id){
return serv
}
}
return null
}
public getMainServer(): ServerWrapper | null {
return this.mainServer
}
}
export class DistroManager {
private static readonly DISTRO_PATH = join(ConfigManager.getLauncherDirectory(), 'distribution.json')
private static readonly DEV_PATH = join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json')
private static readonly DISTRIBUTION_URL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'
// private static readonly DISTRIBUTION_URL = 'https://gist.githubusercontent.com/dscalzi/53b1ba7a11d26a5c353f9d5ae484b71b/raw/'
private static DEV_MODE = false
private static distro: DistributionWrapper
public static isDevMode() {
return DistroManager.DEV_MODE
}
public static setDevMode(value: boolean) {
if(value){
logger.log('Developer mode enabled.')
logger.log('If you don\'t know what that means, revert immediately.')
} else {
logger.log('Developer mode disabled.')
}
DistroManager.DEV_MODE = value
}
public static pullRemote(): Promise<DistributionWrapper> {
if(DistroManager.DEV_MODE){
return DistroManager.pullLocal()
}
return new Promise((resolve, reject) => {
const opts = {
url: DistroManager.DISTRIBUTION_URL,
timeout: 2500
}
const distroDest = join(ConfigManager.getLauncherDirectory(), 'distribution.json')
request(opts, async (error, resp, body) => {
if(!error){
let data: Distribution
try {
data = JSON.parse(body) as Distribution
DistroManager.distro = new DistributionWrapper(data)
} catch (e) {
reject(e)
return;
}
try {
await writeJson(distroDest, DistroManager.distro)
resolve(DistroManager.distro)
} catch (err) {
reject(err)
}
} else {
reject(error)
}
})
})
}
public static async pullLocal(): Promise<DistributionWrapper> {
const data = await readJson(DistroManager.DEV_MODE ? DistroManager.DEV_PATH : DistroManager.DISTRO_PATH) as Distribution
DistroManager.distro = new DistributionWrapper(data)
return DistroManager.distro
}
public static getDistribution(): DistributionWrapper {
return DistroManager.distro
}
}

View File

@ -1,232 +0,0 @@
import { ensureDirSync, pathExistsSync, readdirSync, moveSync, readFileSync, writeFileSync, rename } from 'fs-extra'
import { join } from 'path'
import { shell } from 'electron'
// Group #1: File Name (without .disabled, if any)
// Group #2: File Extension (jar, zip, or litemod)
// Group #3: If it is disabled (if string 'disabled' is present)
const MOD_REGEX = /^(.+(jar|zip|litemod))(?:\.(disabled))?$/
const DISABLED_EXT = '.disabled'
const SHADER_REGEX = /^(.+)\.zip$/
const SHADER_OPTION = /shaderPack=(.+)/
const SHADER_DIR = 'shaderpacks'
const SHADER_CONFIG = 'optionsshaders.txt'
/**
* Validate that the given directory exists. If not, it is
* created.
*
* @param {string} modsDir The path to the mods directory.
*/
export function validateDir(dir: string) {
ensureDirSync(dir)
}
/**
* Scan for drop-in mods in both the mods folder and version
* safe mods folder.
*
* @param {string} modsDir The path to the mods directory.
* @param {string} version The minecraft version of the server configuration.
*
* @returns {{fullName: string, name: string, ext: string, disabled: boolean}[]}
* An array of objects storing metadata about each discovered mod.
*/
export function scanForDropinMods(modsDir: string, version: string) {
const modsDiscovered = []
if(pathExistsSync(modsDir)){
let modCandidates = readdirSync(modsDir)
let verCandidates: string[] = []
const versionDir = join(modsDir, version)
if(pathExistsSync(versionDir)){
verCandidates = readdirSync(versionDir)
}
for(let file of modCandidates){
const match = MOD_REGEX.exec(file)
if(match != null){
modsDiscovered.push({
fullName: match[0],
name: match[1],
ext: match[2],
disabled: match[3] != null
})
}
}
for(let file of verCandidates){
const match = MOD_REGEX.exec(file)
if(match != null){
modsDiscovered.push({
fullName: join(version, match[0]),
name: match[1],
ext: match[2],
disabled: match[3] != null
})
}
}
}
return modsDiscovered
}
/**
* Add dropin mods.
*
* @param {FileList} files The files to add.
* @param {string} modsDir The path to the mods directory.
*/
export function addDropinMods(files: any, modsdir: string) {
exports.validateDir(modsdir)
for(let f of files) {
if(MOD_REGEX.exec(f.name) != null) {
moveSync(f.path, join(modsdir, f.name))
}
}
}
/**
* Delete a drop-in mod from the file system.
*
* @param {string} modsDir The path to the mods directory.
* @param {string} fullName The fullName of the discovered mod to delete.
*
* @returns {boolean} True if the mod was deleted, otherwise false.
*/
export function deleteDropinMod(modsDir: string, fullName: string){
const res = shell.moveItemToTrash(join(modsDir, fullName))
if(!res){
shell.beep()
}
return res
}
/**
* Toggle a discovered mod on or off. This is achieved by either
* adding or disabling the .disabled extension to the local file.
*
* @param {string} modsDir The path to the mods directory.
* @param {string} fullName The fullName of the discovered mod to toggle.
* @param {boolean} enable Whether to toggle on or off the mod.
*
* @returns {Promise.<void>} A promise which resolves when the mod has
* been toggled. If an IO error occurs the promise will be rejected.
*/
export function toggleDropinMod(modsDir: string, fullName: string, enable: boolean){
return new Promise((resolve, reject) => {
const oldPath = join(modsDir, fullName)
const newPath = join(modsDir, enable ? fullName.substring(0, fullName.indexOf(DISABLED_EXT)) : fullName + DISABLED_EXT)
rename(oldPath, newPath, (err) => {
if(err){
reject(err)
} else {
resolve()
}
})
})
}
/**
* Check if a drop-in mod is enabled.
*
* @param {string} fullName The fullName of the discovered mod to toggle.
* @returns {boolean} True if the mod is enabled, otherwise false.
*/
export function isDropinModEnabled(fullName: string){
return !fullName.endsWith(DISABLED_EXT)
}
/**
* Scan for shaderpacks inside the shaderpacks folder.
*
* @param {string} instanceDir The path to the server instance directory.
*
* @returns {{fullName: string, name: string}[]}
* An array of objects storing metadata about each discovered shaderpack.
*/
export function scanForShaderpacks(instanceDir: string){
const shaderDir = join(instanceDir, SHADER_DIR)
const packsDiscovered = [{
fullName: 'OFF',
name: 'Off (Default)'
}]
if(pathExistsSync(shaderDir)){
let modCandidates = readdirSync(shaderDir)
for(let file of modCandidates){
const match = SHADER_REGEX.exec(file)
if(match != null){
packsDiscovered.push({
fullName: match[0],
name: match[1]
})
}
}
}
return packsDiscovered
}
/**
* Read the optionsshaders.txt file to locate the current
* enabled pack. If the file does not exist, OFF is returned.
*
* @param {string} instanceDir The path to the server instance directory.
*
* @returns {string} The file name of the enabled shaderpack.
*/
export function getEnabledShaderpack(instanceDir: string){
validateDir(instanceDir)
const optionsShaders = join(instanceDir, SHADER_CONFIG)
if(pathExistsSync(optionsShaders)){
const buf = readFileSync(optionsShaders, {encoding: 'utf-8'})
const match = SHADER_OPTION.exec(buf)
if(match != null){
return match[1]
} else {
console.warn('WARNING: Shaderpack regex failed.')
}
}
return 'OFF'
}
/**
* Set the enabled shaderpack.
*
* @param {string} instanceDir The path to the server instance directory.
* @param {string} pack the file name of the shaderpack.
*/
export function setEnabledShaderpack(instanceDir: string, pack: string){
validateDir(instanceDir)
const optionsShaders = join(instanceDir, SHADER_CONFIG)
let buf
if(pathExistsSync(optionsShaders)){
buf = readFileSync(optionsShaders, {encoding: 'utf-8'})
buf = buf.replace(SHADER_OPTION, `shaderPack=${pack}`)
} else {
buf = `shaderPack=${pack}`
}
writeFileSync(optionsShaders, buf, {encoding: 'utf-8'})
}
/**
* Add shaderpacks.
*
* @param {FileList} files The files to add.
* @param {string} instanceDir The path to the server instance directory.
*/
export function addShaderpacks(files: any, instanceDir: string) {
const p = join(instanceDir, SHADER_DIR)
exports.validateDir(p)
for(let f of files) {
if(SHADER_REGEX.exec(f.name) != null) {
moveSync(f.path, join(p, f.name))
}
}
}

View File

@ -1,23 +0,0 @@
import { readJSONSync } from 'fs-extra'
import { join } from 'path'
// TODO revisit
let lang: any
export function loadLanguage(id: string){
lang = readJSONSync(join(__dirname, '..', 'assets', 'lang', `${id}.json`)) || {}
}
export function query(id: string){
let query = id.split('.')
let res = lang
for(let q of query){
res = res[q]
}
return res === lang ? {} : res
}
export function queryJS(id: string){
return exports.query(`js.${id}`)
}

View File

@ -1,28 +0,0 @@
export class LoggerUtil {
constructor(
protected prefix: string,
protected style: string
){}
public log(...args: any[]){
console.log(this.prefix, this.style, ...args)
}
public info(...args: any[]){
console.info(this.prefix, this.style, ...args)
}
public warn(...args: any[]){
console.warn(this.prefix, this.style, ...args)
}
public debug(...args: any[]){
console.debug(this.prefix, this.style, ...args)
}
public error(...args: any[]){
console.error(this.prefix, this.style, ...args)
}
}

View File

@ -1,71 +0,0 @@
import { ConfigManager } from '../../common/config/configmanager'
import { DistroManager, DistributionWrapper } from './distromanager'
import { join } from 'path'
import { remove } from 'fs-extra'
import { loadLanguage } from './langloader'
import { LoggerUtil } from './loggerutil'
import { tmpdir } from 'os'
import { ipcRenderer } from 'electron'
const logger = new LoggerUtil('%c[Preloader]', 'color: #a02d2a; font-weight: bold')
logger.log('Loading..')
// Load ConfigManager
ConfigManager.load()
// Load Strings
loadLanguage('en_US')
function onDistroLoad(data: DistributionWrapper | null){
if(data != null){
// Resolve the selected server if its value has yet to be set.
if(ConfigManager.getSelectedServer() == null || data.getServer(ConfigManager.getSelectedServer()!) == null){
logger.log('Determining default selected server..')
// TODO what if undefined
ConfigManager.setSelectedServer(data.getMainServer()!.server.id as string)
ConfigManager.save()
}
}
ipcRenderer.send('distributionIndexDone', data != null)
}
// Ensure Distribution is downloaded and cached.
DistroManager.pullRemote().then((data) => {
logger.log('Loaded distribution index.')
onDistroLoad(data)
}).catch((err) => {
logger.log('Failed to load distribution index.')
logger.error(err)
logger.log('Attempting to load an older version of the distribution index.')
// Try getting a local copy, better than nothing.
DistroManager.pullLocal().then((data) => {
logger.log('Successfully loaded an older version of the distribution index.')
onDistroLoad(data)
}).catch((err) => {
logger.log('Failed to load an older version of the distribution index.')
logger.log('Application cannot run.')
logger.error(err)
onDistroLoad(null)
})
})
// Clean up temp dir incase previous launches ended unexpectedly.
remove(join(tmpdir(), ConfigManager.getTempNativeFolder()), (err) => {
if(err){
logger.warn('Error while cleaning natives directory', err)
} else {
logger.log('Cleaned natives directory.')
}
})

View File

@ -1,755 +0,0 @@
import AdmZip from 'adm-zip'
import { pathExistsSync, writeFile, ensureDirSync, writeFileSync, remove } from 'fs-extra'
import { join, basename } from 'path'
import { ModuleWrapper, ServerWrapper } from './distromanager'
import { Type, Required } from 'helios-distribution-types'
import { LoggerUtil } from './loggerutil'
import { ConfigManager } from '../../common/config/configmanager'
import { spawn } from 'child_process'
import { SavedAccount } from '../../common/config/model/SavedAccount'
import { tmpdir, release } from 'os'
import { SubModConfig } from '../../common/config/model/ModConfig'
import { pseudoRandomBytes } from 'crypto'
import { Util, LibraryInternal } from './assetguard'
import { VersionJson, Rule } from '../asset/model/mojang/VersionJson'
import { URL } from 'url'
const logger = new LoggerUtil('%c[ProcessBuilder]', 'color: #003996; font-weight: bold')
export class ProcessBuilder {
private gameDir: string
private commonDir: string
private fmlDir: string
private llDir: string
private libPath: string
private usingLiteLoader: boolean
private llPath: string | null
constructor(
private wrappedServer: ServerWrapper,
private versionData: VersionJson,
private forgeData: any, // TODO type
private authUser: SavedAccount,
private launcherVersion: string
){
this.gameDir = join(ConfigManager.getInstanceDirectory(), wrappedServer.server.id)
this.commonDir = ConfigManager.getCommonDirectory()
this.authUser = authUser
this.launcherVersion = launcherVersion
this.fmlDir = join(this.gameDir, 'forgeModList.json')
this.llDir = join(this.gameDir, 'liteloaderModList.json')
this.libPath = join(this.commonDir, 'libraries')
this.usingLiteLoader = false
this.llPath = null
}
/**
* Convienence method to run the functions typically used to build a process.
*/
build(){
ensureDirSync(this.gameDir)
const tempNativePath = join(tmpdir(), ConfigManager.getTempNativeFolder(), pseudoRandomBytes(16).toString('hex'))
process.throwDeprecation = true
this.setupLiteLoader()
logger.log('Using liteloader:', this.usingLiteLoader)
const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.wrappedServer.server.id)!.mods, this.wrappedServer.getWrappedModules())
// Mod list below 1.13
if(!Util.mcVersionAtLeast('1.13', this.wrappedServer.server.minecraftVersion)){
this.constructModList('forge', modObj.fMods, true)
if(this.usingLiteLoader){
this.constructModList('liteloader', modObj.lMods, true)
}
}
const uberModArr = modObj.fMods.concat(modObj.lMods)
let args = this.constructJVMArguments(uberModArr, tempNativePath)
if(Util.mcVersionAtLeast('1.13', this.wrappedServer.server.minecraftVersion)){
args = args.concat(this.constructModArguments(modObj.fMods))
}
logger.log('Launch Arguments:', args)
const child = spawn(ConfigManager.getJavaExecutable()!, args, {
cwd: this.gameDir,
detached: ConfigManager.getLaunchDetached()
})
if(ConfigManager.getLaunchDetached()){
child.unref()
}
child.stdout.setEncoding('utf8')
child.stderr.setEncoding('utf8')
const loggerMCstdout = new LoggerUtil('%c[Minecraft]', 'color: #36b030; font-weight: bold')
const loggerMCstderr = new LoggerUtil('%c[Minecraft]', 'color: #b03030; font-weight: bold')
child.stdout.on('data', (data) => {
loggerMCstdout.log(data)
})
child.stderr.on('data', (data) => {
loggerMCstderr.log(data)
})
child.on('close', (code, signal) => {
logger.log('Exited with code', code)
remove(tempNativePath, (err) => {
if(err){
logger.warn('Error while deleting temp dir', err)
} else {
logger.log('Temp dir deleted successfully.')
}
})
})
return child
}
/**
* Determine if an optional mod is enabled from its configuration value. If the
* configuration value is null, the required object will be used to
* determine if it is enabled.
*
* A mod is enabled if:
* * The configuration is not null and one of the following:
* * The configuration is a boolean and true.
* * The configuration is an object and its 'value' property is true.
* * The configuration is null and one of the following:
* * The required object is null.
* * The required object's 'def' property is null or true.
*
* @param {SubModConfig | boolean} modCfg The mod configuration object.
* @param {Required | undefined} required Optional. The required object from the mod's distro declaration.
* @returns {boolean} True if the mod is enabled, false otherwise.
*/
public static isModEnabled(modCfg: SubModConfig | boolean, required?: Required){
return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && (typeof modCfg.value !== 'undefined' ? modCfg.value : true))) : required != null ? required.def : true
}
/**
* Function which performs a preliminary scan of the top level
* mods. If liteloader is present here, we setup the special liteloader
* launch options. Note that liteloader is only allowed as a top level
* mod. It must not be declared as a submodule.
*/
private setupLiteLoader(): void {
for(const ll of this.wrappedServer.getWrappedModules()){
if(ll.module.type === Type.LiteLoader){
if(!ll.module.required!.value!){
const modCfg = ConfigManager.getModConfiguration(this.wrappedServer.server.id)!.mods
if(ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessID()], ll.module.required)){
if(pathExistsSync(ll.module.artifact.path!)){
this.usingLiteLoader = true
this.llPath = ll.module.artifact.path!
}
}
} else {
if(pathExistsSync(ll.module.artifact.path!)){
this.usingLiteLoader = true
this.llPath = ll.module.artifact.path!
}
}
}
}
}
/**
* Resolve an array of all enabled mods. These mods will be constructed into
* a mod list format and enabled at launch.
*
* @param {{[id: string]: boolean | SubModConfig}} modCfg The mod configuration object.
* @param {Array.<ModuleWrapper>} mdls An array of modules to parse.
* @returns {{fMods: Array.<ModuleWrapper>, lMods: Array.<ModuleWrapper>}} An object which contains
* a list of enabled forge mods and litemods.
*/
resolveModConfiguration(modCfg: {[id: string]: boolean | SubModConfig}, mdls: ModuleWrapper[]): {fMods: ModuleWrapper[], lMods: ModuleWrapper[]}{
let fMods: ModuleWrapper[] = []
let lMods: ModuleWrapper[] = []
for(const mdl of mdls){
const type = mdl.module.type
if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader){
const o = !mdl.module.required!.value!
const e = ProcessBuilder.isModEnabled(modCfg[mdl.getVersionlessID()], mdl.module.required)
if(!o || (o && e)){
if(mdl.hasSubModules()){
const v = this.resolveModConfiguration((modCfg[mdl.getVersionlessID()] as SubModConfig).mods, mdl.getWrappedSubmodules())
fMods = fMods.concat(v.fMods)
lMods = lMods.concat(v.lMods)
if(mdl.module.type === Type.LiteLoader){
continue
}
}
if(mdl.module.type === Type.ForgeMod){
fMods.push(mdl)
} else {
lMods.push(mdl)
}
}
}
}
return {
fMods,
lMods
}
}
_isBelowOneDotSeven() {
return Number(this.forgeData.id.split('-')[0].split('.')[1]) <= 7
}
/**
* Test to see if this version of forge requires the absolute: prefix
* on the modListFile repository field.
*/
_requiresAbsolute(){
try {
if(this._isBelowOneDotSeven()) {
return false
}
const ver = this.forgeData.id.split('-')[2]
const pts = ver.split('.')
const min = [14, 23, 3, 2655]
for(let i=0; i<pts.length; i++){
const parsed = Number.parseInt(pts[i])
if(parsed < min[i]){
return false
} else if(parsed > min[i]){
return true
}
}
} catch (err) {
// We know old forge versions follow this format.
// Error must be caused by newer version.
}
// Equal or errored
return true
}
/**
* Construct a mod list json object.
*
* @param {'forge' | 'liteloader'} type The mod list type to construct.
* @param {Array.<ModuleWrapper>} mods An array of mods to add to the mod list.
* @param {boolean} save Optional. Whether or not we should save the mod list file.
*/
constructModList(type: 'forge' | 'liteloader', mods: ModuleWrapper[], save = false){
const modList = {
repositoryRoot: ((type === 'forge' && this._requiresAbsolute()) ? 'absolute:' : '') + join(this.commonDir, 'modstore'),
modRef: [] as string[]
}
const ids = []
if(type === 'forge'){
for(let mod of mods){
ids.push(mod.getExtensionlessID())
}
} else {
for(let mod of mods){
ids.push(mod.getExtensionlessID() + '@' + mod.getExtension())
}
}
modList.modRef = ids
if(save){
const json = JSON.stringify(modList, null, 4)
writeFileSync(type === 'forge' ? this.fmlDir : this.llDir, json, 'UTF-8')
}
return modList
}
/**
* Construct the mod argument list for forge 1.13
*
* @param {Array.<ModuleWrapper>} mods An array of mods to add to the mod list.
*/
constructModArguments(mods: ModuleWrapper[]){
const argStr = mods.map(mod => {
return mod.getExtensionlessID()
}).join(',')
if(argStr){
return [
'--fml.mavenRoots',
join('..', '..', 'common', 'modstore'),
'--fml.mods',
argStr
]
} else {
return []
}
}
/**
* Construct the argument array that will be passed to the JVM process.
*
* @param {Array.<ModuleWrapper>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string| number>} An array containing the full JVM arguments for this process.
*/
constructJVMArguments(mods: ModuleWrapper[], tempNativePath: string): string[] {
if(Util.mcVersionAtLeast('1.13', this.wrappedServer.server.minecraftVersion)){
return this._constructJVMArguments113(mods, tempNativePath)
} else {
return this._constructJVMArguments112(mods, tempNativePath)
}
}
/**
* Construct the argument array that will be passed to the JVM process.
* This function is for 1.12 and below.
*
* @param {Array.<ModuleWrapper>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the full JVM arguments for this process.
*/
_constructJVMArguments112(mods: ModuleWrapper[], tempNativePath: string): string[] {
let args = []
// Classpath Argument
args.push('-cp')
args.push(this.classpathArg(mods, tempNativePath).join(process.platform === 'win32' ? ';' : ':'))
// Java Arguments
if(process.platform === 'darwin'){
args.push('-Xdock:name=HeliosLauncher')
args.push('-Xdock:icon=' + join(__dirname, '..', 'images', 'minecraft.icns'))
}
args.push('-Xmx' + ConfigManager.getMaxRAM())
args.push('-Xms' + ConfigManager.getMinRAM())
args = args.concat(ConfigManager.getJVMOptions())
args.push('-Djava.library.path=' + tempNativePath)
// Main Java Class
args.push(this.forgeData.mainClass)
// Forge Arguments
args = args.concat(this._resolveForgeArgs())
return args
}
/**
* Construct the argument array that will be passed to the JVM process.
* This function is for 1.13+
*
* Note: Required Libs https://github.com/MinecraftForge/MinecraftForge/blob/af98088d04186452cb364280340124dfd4766a5c/src/fmllauncher/java/net/minecraftforge/fml/loading/LibraryFinder.java#L82
*
* @param {Array.<ModuleWrapper>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the full JVM arguments for this process.
*/
_constructJVMArguments113(mods: ModuleWrapper[], tempNativePath: string): string[] {
const argDiscovery = /\${*(.*)}/
// JVM Arguments First
let args: (string | { rules: Rule[], value: string[] })[] = this.versionData.arguments.jvm
//args.push('-Dlog4j.configurationFile=D:\\WesterosCraft\\game\\common\\assets\\log_configs\\client-1.12.xml')
// Java Arguments
if(process.platform === 'darwin'){
args.push('-Xdock:name=HeliosLauncher')
args.push('-Xdock:icon=' + join(__dirname, '..', 'images', 'minecraft.icns'))
}
args.push('-Xmx' + ConfigManager.getMaxRAM())
args.push('-Xms' + ConfigManager.getMinRAM())
args = args.concat(ConfigManager.getJVMOptions())
// Main Java Class
args.push(this.forgeData.mainClass)
// Vanilla Arguments
args = args.concat(this.versionData.arguments.game)
for(let i=0; i<args.length; i++){
if(typeof args[i] === 'object' && (args[i] as any).rules != null){
const arg = args[i] as { rules: Rule[], value: string[] }
let checksum = 0
for(let rule of arg.rules){
if(rule.os != null){
if(rule.os.name === LibraryInternal.mojangFriendlyOS()
&& (rule.os.version == null || new RegExp(rule.os.version).test(release()))){
if(rule.action === 'allow'){
checksum++
}
} else {
if(rule.action === 'disallow'){
checksum++
}
}
} else if(rule.features != null){
// We don't have many 'features' in the index at the moment.
// This should be fine for a while.
if(rule.features.has_custom_resolution != null && rule.features.has_custom_resolution === true){
if(ConfigManager.getFullscreen()){
arg.value = [
'--fullscreen',
'true'
]
}
checksum++
}
}
}
// TODO splice not push
if(checksum === arg.rules.length){
if(typeof arg.value === 'string'){
args[i] = arg.value
} else if(typeof arg.value === 'object'){
//args = args.concat(args[i].value)
args.splice(i, 1, ...arg.value)
}
// Decrement i to reprocess the resolved value
i--
} else {
args[i] = null! // TODO lol
}
} else if(typeof args[i] === 'string'){
const arg = args[i] as string
if(argDiscovery.test(arg)){
const identifier = arg.match(argDiscovery)![1]
let val = null
switch(identifier){
case 'auth_player_name':
val = this.authUser.displayName.trim()
break
case 'version_name':
//val = versionData.id
val = this.wrappedServer.server.id
break
case 'game_directory':
val = this.gameDir
break
case 'assets_root':
val = join(this.commonDir, 'assets')
break
case 'assets_index_name':
val = this.versionData.assets
break
case 'auth_uuid':
val = this.authUser.uuid.trim()
break
case 'auth_access_token':
val = this.authUser.accessToken
break
case 'user_type':
val = 'mojang'
break
case 'version_type':
val = this.versionData.type
break
case 'resolution_width':
val = ConfigManager.getGameWidth()
break
case 'resolution_height':
val = ConfigManager.getGameHeight()
break
case 'natives_directory':
val = arg.replace(argDiscovery, tempNativePath)
break
case 'launcher_name':
val = arg.replace(argDiscovery, 'Helios-Launcher')
break
case 'launcher_version':
val = arg.replace(argDiscovery, this.launcherVersion)
break
case 'classpath':
val = this.classpathArg(mods, tempNativePath).join(process.platform === 'win32' ? ';' : ':')
break
}
if(val != null){
args[i] = val.toString()
}
}
}
}
// Forge Specific Arguments
args = args.concat(this.forgeData.arguments.game)
// Filter null values
args = args.filter(arg => {
return arg != null
})
return args as string[]
}
/**
* Resolve the arguments required by forge.
*
* @returns {Array.<string>} An array containing the arguments required by forge.
*/
_resolveForgeArgs(): string[] {
const mcArgs: string[] = this.forgeData.minecraftArguments.split(' ')
const argDiscovery = /\${*(.*)}/
// Replace the declared variables with their proper values.
for(let i=0; i<mcArgs.length; ++i){
if(argDiscovery.test(mcArgs[i])){
const identifier = mcArgs[i].match(argDiscovery)![1]
let val = null
switch(identifier){
case 'auth_player_name':
val = this.authUser.displayName.trim()
break
case 'version_name':
//val = versionData.id
val = this.wrappedServer.server.id
break
case 'game_directory':
val = this.gameDir
break
case 'assets_root':
val = join(this.commonDir, 'assets')
break
case 'assets_index_name':
val = this.versionData.assets
break
case 'auth_uuid':
val = this.authUser.uuid.trim()
break
case 'auth_access_token':
val = this.authUser.accessToken
break
case 'user_type':
val = 'mojang'
break
case 'user_properties': // 1.8.9 and below.
val = '{}'
break
case 'version_type':
val = this.versionData.type
break
}
if(val != null){
mcArgs[i] = val
}
}
}
// Autoconnect to the selected server.
if(ConfigManager.getAutoConnect() && this.wrappedServer.server.autoconnect){
const serverURL = new URL('my://' + this.wrappedServer.server.address)
mcArgs.push('--server')
mcArgs.push(serverURL.hostname)
if(serverURL.port){
mcArgs.push('--port')
mcArgs.push(serverURL.port)
}
}
// Prepare game resolution
if(ConfigManager.getFullscreen()){
mcArgs.push('--fullscreen')
mcArgs.push('true')
} else {
mcArgs.push('--width')
mcArgs.push(ConfigManager.getGameWidth().toString())
mcArgs.push('--height')
mcArgs.push(ConfigManager.getGameHeight().toString())
}
// Mod List File Argument
mcArgs.push('--modListFile')
if(this._isBelowOneDotSeven()) {
mcArgs.push(basename(this.fmlDir))
} else {
mcArgs.push('absolute:' + this.fmlDir)
}
// LiteLoader
if(this.usingLiteLoader){
mcArgs.push('--modRepo')
mcArgs.push(this.llDir)
// Set first arg to liteloader tweak class
mcArgs.unshift('com.mumfrey.liteloader.launch.LiteLoaderTweaker')
mcArgs.unshift('--tweakClass')
}
return mcArgs
}
/**
* Resolve the full classpath argument list for this process. This method will resolve all Mojang-declared
* libraries as well as the libraries declared by the server. Since mods are permitted to declare libraries,
* this method requires all enabled mods as an input
*
* @param {Array.<ModuleWrapper>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the paths of each library required by this process.
*/
classpathArg(mods: ModuleWrapper[], tempNativePath: string): string[] {
let cpArgs: string[] = []
// Add the version.jar to the classpath.
const version = this.versionData.id
cpArgs.push(join(this.commonDir, 'versions', version, version + '.jar'))
if(this.usingLiteLoader){
cpArgs.push(this.llPath!)
}
// Resolve the Mojang declared libraries.
const mojangLibs = this._resolveMojangLibraries(tempNativePath)
// Resolve the server declared libraries.
const servLibs = this._resolveServerLibraries(mods)
// Merge libraries, server libs with the same
// maven identifier will override the mojang ones.
// Ex. 1.7.10 forge overrides mojang's guava with newer version.
const finalLibs = {...mojangLibs, ...servLibs}
cpArgs = cpArgs.concat(Object.values(finalLibs))
return cpArgs
}
/**
* Resolve the libraries defined by Mojang's version data. This method will also extract
* native libraries and point to the correct location for its classpath.
*
* TODO - clean up function
*
* @param {string} tempNativePath The path to store the native libraries.
* @returns {{[id: string]: string}} An object containing the paths of each library mojang declares.
*/
_resolveMojangLibraries(tempNativePath: string): {[id: string]: string} {
const libs: {[id: string]: string} = {}
const libArr = this.versionData.libraries
ensureDirSync(tempNativePath)
for(let i=0; i<libArr.length; i++){
const lib = libArr[i]
if(LibraryInternal.validateRules(lib.rules, lib.natives)){
if(lib.natives == null){
const dlInfo = lib.downloads
const artifact = dlInfo.artifact
const to = join(this.libPath, artifact.path)
const versionIndependentId: string = lib.name.substring(0, lib.name.lastIndexOf(':'))
libs[versionIndependentId] = to
} else {
// Extract the native library.
const exclusionArr: string[] = lib.extract != null ? lib.extract.exclude : ['META-INF/']
// @ts-ignore
const artifact = lib.downloads.classifiers[lib.natives[LibraryInternal.mojangFriendlyOS()].replace('${arch}', process.arch.replace('x', ''))]
// Location of native zip.
const to = join(this.libPath, artifact.path)
let zip = new AdmZip(to)
let zipEntries = zip.getEntries()
// Unzip the native zip.
for(let i=0; i<zipEntries.length; i++){
const fileName = zipEntries[i].entryName
let shouldExclude = false
// Exclude noted files.
exclusionArr.forEach((exclusion: string) => {
if(fileName.indexOf(exclusion) > -1){
shouldExclude = true
}
})
// Extract the file.
if(!shouldExclude){
writeFile(join(tempNativePath, fileName), zipEntries[i].getData(), (err) => {
if(err){
logger.error('Error while extracting native library:', err)
}
})
}
}
}
}
}
return libs
}
/**
* Resolve the libraries declared by this server in order to add them to the classpath.
* This method will also check each enabled mod for libraries, as mods are permitted to
* declare libraries.
*
* @param {Array.<ModuleWrapper>} mods An array of enabled mods which will be launched with this process.
* @returns {{[id: string]: string}} An object containing the paths of each library this server requires.
*/
_resolveServerLibraries(mods: ModuleWrapper[]): {[id: string]: string} {
const mdls: ModuleWrapper[] = this.wrappedServer.getWrappedModules()
let libs: {[id: string]: string} = {}
// Locate Forge/Libraries
for(let mdl of mdls){
const type = mdl.module.type
if(type === Type.ForgeHosted || type === Type.Library){
libs[mdl.getVersionlessID()] = mdl.module.artifact.path as string
if(mdl.hasSubModules()){
const res = this._resolveModuleLibraries(mdl)
if(Object.keys(res).length > 0){
libs = {...libs, ...res}
}
}
}
}
//Check for any libraries in our mod list.
for(let i=0; i<mods.length; i++){
if(mods[i].hasSubModules()){
const res = this._resolveModuleLibraries(mods[i])
if(Object.keys(res).length > 0){
libs = {...libs, ...res}
}
}
}
return libs
}
/**
* Recursively resolve the path of each library required by this module.
*
* @param {ModuleWrapper} mdl A module object from the server distro index.
* @returns {Array.<string>} An array containing the paths of each library this module requires.
*/
_resolveModuleLibraries(mdl: ModuleWrapper): {[id: string]: string} {
if(!mdl.hasSubModules()){
return {}
}
let libs: {[id: string]: string} = {}
for(const sm of mdl.getWrappedSubmodules()){
if(sm.module.type === Type.Library){
libs[sm.getVersionlessID()] = sm.module.artifact.path as string
}
// If this module has submodules, we need to resolve the libraries for those.
// To avoid unnecessary recursive calls, base case is checked here.
if(mdl.hasSubModules()){
const res = this._resolveModuleLibraries(sm)
if(Object.keys(res).length > 0){
libs = {...libs, ...res}
}
}
}
return libs
}
}

View File

@ -1,738 +0,0 @@
const AdmZip = require('adm-zip')
const child_process = require('child_process')
const crypto = require('crypto')
const fs = require('fs-extra')
const os = require('os')
const path = require('path')
const { URL } = require('url')
const { Util, Library: LibraryInternal } = require('./assetguard')
const ConfigManager = require('./configmanager')
const DistroManager = require('./distromanager')
const LoggerUtil = require('./loggerutil')
const logger = LoggerUtil('%c[ProcessBuilder]', 'color: #003996; font-weight: bold')
class ProcessBuilder {
constructor(distroServer, versionData, forgeData, authUser, launcherVersion){
this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.getID())
this.commonDir = ConfigManager.getCommonDirectory()
this.server = distroServer
this.versionData = versionData
this.forgeData = forgeData
this.authUser = authUser
this.launcherVersion = launcherVersion
this.fmlDir = path.join(this.gameDir, 'forgeModList.json')
this.llDir = path.join(this.gameDir, 'liteloaderModList.json')
this.libPath = path.join(this.commonDir, 'libraries')
this.usingLiteLoader = false
this.llPath = null
}
/**
* Convienence method to run the functions typically used to build a process.
*/
build(){
fs.ensureDirSync(this.gameDir)
const tempNativePath = path.join(os.tmpdir(), ConfigManager.getTempNativeFolder(), crypto.pseudoRandomBytes(16).toString('hex'))
process.throwDeprecation = true
this.setupLiteLoader()
logger.log('Using liteloader:', this.usingLiteLoader)
const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.getID()).mods, this.server.getModules())
// Mod list below 1.13
if(!Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
this.constructModList('forge', modObj.fMods, true)
if(this.usingLiteLoader){
this.constructModList('liteloader', modObj.lMods, true)
}
}
const uberModArr = modObj.fMods.concat(modObj.lMods)
let args = this.constructJVMArguments(uberModArr, tempNativePath)
if(Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
args = args.concat(this.constructModArguments(modObj.fMods))
}
logger.log('Launch Arguments:', args)
const child = child_process.spawn(ConfigManager.getJavaExecutable(), args, {
cwd: this.gameDir,
detached: ConfigManager.getLaunchDetached()
})
if(ConfigManager.getLaunchDetached()){
child.unref()
}
child.stdout.setEncoding('utf8')
child.stderr.setEncoding('utf8')
const loggerMCstdout = LoggerUtil('%c[Minecraft]', 'color: #36b030; font-weight: bold')
const loggerMCstderr = LoggerUtil('%c[Minecraft]', 'color: #b03030; font-weight: bold')
child.stdout.on('data', (data) => {
loggerMCstdout.log(data)
})
child.stderr.on('data', (data) => {
loggerMCstderr.log(data)
})
child.on('close', (code, signal) => {
logger.log('Exited with code', code)
fs.remove(tempNativePath, (err) => {
if(err){
logger.warn('Error while deleting temp dir', err)
} else {
logger.log('Temp dir deleted successfully.')
}
})
})
return child
}
/**
* Determine if an optional mod is enabled from its configuration value. If the
* configuration value is null, the required object will be used to
* determine if it is enabled.
*
* A mod is enabled if:
* * The configuration is not null and one of the following:
* * The configuration is a boolean and true.
* * The configuration is an object and its 'value' property is true.
* * The configuration is null and one of the following:
* * The required object is null.
* * The required object's 'def' property is null or true.
*
* @param {Object | boolean} modCfg The mod configuration object.
* @param {Object} required Optional. The required object from the mod's distro declaration.
* @returns {boolean} True if the mod is enabled, false otherwise.
*/
static isModEnabled(modCfg, required = null){
return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && (typeof modCfg.value !== 'undefined' ? modCfg.value : true))) : required != null ? required.isDefault() : true
}
/**
* Function which performs a preliminary scan of the top level
* mods. If liteloader is present here, we setup the special liteloader
* launch options. Note that liteloader is only allowed as a top level
* mod. It must not be declared as a submodule.
*/
setupLiteLoader(){
for(let ll of this.server.getModules()){
if(ll.getType() === DistroManager.Types.LiteLoader){
if(!ll.getRequired().isRequired()){
const modCfg = ConfigManager.getModConfiguration(this.server.getID()).mods
if(ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessID()], ll.getRequired())){
if(fs.existsSync(ll.getArtifact().getPath())){
this.usingLiteLoader = true
this.llPath = ll.getArtifact().getPath()
}
}
} else {
if(fs.existsSync(ll.getArtifact().getPath())){
this.usingLiteLoader = true
this.llPath = ll.getArtifact().getPath()
}
}
}
}
}
/**
* Resolve an array of all enabled mods. These mods will be constructed into
* a mod list format and enabled at launch.
*
* @param {Object} modCfg The mod configuration object.
* @param {Array.<Object>} mdls An array of modules to parse.
* @returns {{fMods: Array.<Object>, lMods: Array.<Object>}} An object which contains
* a list of enabled forge mods and litemods.
*/
resolveModConfiguration(modCfg, mdls){
let fMods = []
let lMods = []
for(let mdl of mdls){
const type = mdl.getType()
if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){
const o = !mdl.getRequired().isRequired()
const e = ProcessBuilder.isModEnabled(modCfg[mdl.getVersionlessID()], mdl.getRequired())
if(!o || (o && e)){
if(mdl.hasSubModules()){
const v = this.resolveModConfiguration(modCfg[mdl.getVersionlessID()].mods, mdl.getSubModules())
fMods = fMods.concat(v.fMods)
lMods = lMods.concat(v.lMods)
if(mdl.type === DistroManager.Types.LiteLoader){
continue
}
}
if(mdl.type === DistroManager.Types.ForgeMod){
fMods.push(mdl)
} else {
lMods.push(mdl)
}
}
}
}
return {
fMods,
lMods
}
}
_isBelowOneDotSeven() {
return Number(this.forgeData.id.split('-')[0].split('.')[1]) <= 7
}
/**
* Test to see if this version of forge requires the absolute: prefix
* on the modListFile repository field.
*/
_requiresAbsolute(){
try {
if(this._isBelowOneDotSeven()) {
return false
}
const ver = this.forgeData.id.split('-')[2]
const pts = ver.split('.')
const min = [14, 23, 3, 2655]
for(let i=0; i<pts.length; i++){
const parsed = Number.parseInt(pts[i])
if(parsed < min[i]){
return false
} else if(parsed > min[i]){
return true
}
}
} catch (err) {
// We know old forge versions follow this format.
// Error must be caused by newer version.
}
// Equal or errored
return true
}
/**
* Construct a mod list json object.
*
* @param {'forge' | 'liteloader'} type The mod list type to construct.
* @param {Array.<Object>} mods An array of mods to add to the mod list.
* @param {boolean} save Optional. Whether or not we should save the mod list file.
*/
constructModList(type, mods, save = false){
const modList = {
repositoryRoot: ((type === 'forge' && this._requiresAbsolute()) ? 'absolute:' : '') + path.join(this.commonDir, 'modstore')
}
const ids = []
if(type === 'forge'){
for(let mod of mods){
ids.push(mod.getExtensionlessID())
}
} else {
for(let mod of mods){
ids.push(mod.getExtensionlessID() + '@' + mod.getExtension())
}
}
modList.modRef = ids
if(save){
const json = JSON.stringify(modList, null, 4)
fs.writeFileSync(type === 'forge' ? this.fmlDir : this.llDir, json, 'UTF-8')
}
return modList
}
/**
* Construct the mod argument list for forge 1.13
*
* @param {Array.<Object>} mods An array of mods to add to the mod list.
*/
constructModArguments(mods){
const argStr = mods.map(mod => {
return mod.getExtensionlessID()
}).join(',')
if(argStr){
return [
'--fml.mavenRoots',
path.join('..', '..', 'common', 'modstore'),
'--fml.mods',
argStr
]
} else {
return []
}
}
/**
* Construct the argument array that will be passed to the JVM process.
*
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the full JVM arguments for this process.
*/
constructJVMArguments(mods, tempNativePath){
if(Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
return this._constructJVMArguments113(mods, tempNativePath)
} else {
return this._constructJVMArguments112(mods, tempNativePath)
}
}
/**
* Construct the argument array that will be passed to the JVM process.
* This function is for 1.12 and below.
*
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the full JVM arguments for this process.
*/
_constructJVMArguments112(mods, tempNativePath){
let args = []
// Classpath Argument
args.push('-cp')
args.push(this.classpathArg(mods, tempNativePath).join(process.platform === 'win32' ? ';' : ':'))
// Java Arguments
if(process.platform === 'darwin'){
args.push('-Xdock:name=HeliosLauncher')
args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns'))
}
args.push('-Xmx' + ConfigManager.getMaxRAM())
args.push('-Xms' + ConfigManager.getMinRAM())
args = args.concat(ConfigManager.getJVMOptions())
args.push('-Djava.library.path=' + tempNativePath)
// Main Java Class
args.push(this.forgeData.mainClass)
// Forge Arguments
args = args.concat(this._resolveForgeArgs())
return args
}
/**
* Construct the argument array that will be passed to the JVM process.
* This function is for 1.13+
*
* Note: Required Libs https://github.com/MinecraftForge/MinecraftForge/blob/af98088d04186452cb364280340124dfd4766a5c/src/fmllauncher/java/net/minecraftforge/fml/loading/LibraryFinder.java#L82
*
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the full JVM arguments for this process.
*/
_constructJVMArguments113(mods, tempNativePath){
const argDiscovery = /\${*(.*)}/
// JVM Arguments First
let args = this.versionData.arguments.jvm
//args.push('-Dlog4j.configurationFile=D:\\WesterosCraft\\game\\common\\assets\\log_configs\\client-1.12.xml')
// Java Arguments
if(process.platform === 'darwin'){
args.push('-Xdock:name=HeliosLauncher')
args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns'))
}
args.push('-Xmx' + ConfigManager.getMaxRAM())
args.push('-Xms' + ConfigManager.getMinRAM())
args = args.concat(ConfigManager.getJVMOptions())
// Main Java Class
args.push(this.forgeData.mainClass)
// Vanilla Arguments
args = args.concat(this.versionData.arguments.game)
for(let i=0; i<args.length; i++){
if(typeof args[i] === 'object' && args[i].rules != null){
let checksum = 0
for(let rule of args[i].rules){
if(rule.os != null){
if(rule.os.name === LibraryInternal.mojangFriendlyOS()
&& (rule.os.version == null || new RegExp(rule.os.version).test(os.release))){
if(rule.action === 'allow'){
checksum++
}
} else {
if(rule.action === 'disallow'){
checksum++
}
}
} else if(rule.features != null){
// We don't have many 'features' in the index at the moment.
// This should be fine for a while.
if(rule.features.has_custom_resolution != null && rule.features.has_custom_resolution === true){
if(ConfigManager.getFullscreen()){
rule.values = [
'--fullscreen',
'true'
]
}
checksum++
}
}
}
// TODO splice not push
if(checksum === args[i].rules.length){
if(typeof args[i].value === 'string'){
args[i] = args[i].value
} else if(typeof args[i].value === 'object'){
//args = args.concat(args[i].value)
args.splice(i, 1, ...args[i].value)
}
// Decrement i to reprocess the resolved value
i--
} else {
args[i] = null
}
} else if(typeof args[i] === 'string'){
if(argDiscovery.test(args[i])){
const identifier = args[i].match(argDiscovery)[1]
let val = null
switch(identifier){
case 'auth_player_name':
val = this.authUser.displayName.trim()
break
case 'version_name':
//val = versionData.id
val = this.server.getID()
break
case 'game_directory':
val = this.gameDir
break
case 'assets_root':
val = path.join(this.commonDir, 'assets')
break
case 'assets_index_name':
val = this.versionData.assets
break
case 'auth_uuid':
val = this.authUser.uuid.trim()
break
case 'auth_access_token':
val = this.authUser.accessToken
break
case 'user_type':
val = 'mojang'
break
case 'version_type':
val = this.versionData.type
break
case 'resolution_width':
val = ConfigManager.getGameWidth()
break
case 'resolution_height':
val = ConfigManager.getGameHeight()
break
case 'natives_directory':
val = args[i].replace(argDiscovery, tempNativePath)
break
case 'launcher_name':
val = args[i].replace(argDiscovery, 'Helios-Launcher')
break
case 'launcher_version':
val = args[i].replace(argDiscovery, this.launcherVersion)
break
case 'classpath':
val = this.classpathArg(mods, tempNativePath).join(process.platform === 'win32' ? ';' : ':')
break
}
if(val != null){
args[i] = val
}
}
}
}
// Forge Specific Arguments
args = args.concat(this.forgeData.arguments.game)
// Filter null values
args = args.filter(arg => {
return arg != null
})
return args
}
/**
* Resolve the arguments required by forge.
*
* @returns {Array.<string>} An array containing the arguments required by forge.
*/
_resolveForgeArgs(){
const mcArgs = this.forgeData.minecraftArguments.split(' ')
const argDiscovery = /\${*(.*)}/
// Replace the declared variables with their proper values.
for(let i=0; i<mcArgs.length; ++i){
if(argDiscovery.test(mcArgs[i])){
const identifier = mcArgs[i].match(argDiscovery)[1]
let val = null
switch(identifier){
case 'auth_player_name':
val = this.authUser.displayName.trim()
break
case 'version_name':
//val = versionData.id
val = this.server.getID()
break
case 'game_directory':
val = this.gameDir
break
case 'assets_root':
val = path.join(this.commonDir, 'assets')
break
case 'assets_index_name':
val = this.versionData.assets
break
case 'auth_uuid':
val = this.authUser.uuid.trim()
break
case 'auth_access_token':
val = this.authUser.accessToken
break
case 'user_type':
val = 'mojang'
break
case 'user_properties': // 1.8.9 and below.
val = '{}'
break
case 'version_type':
val = this.versionData.type
break
}
if(val != null){
mcArgs[i] = val
}
}
}
// Autoconnect to the selected server.
if(ConfigManager.getAutoConnect() && this.server.isAutoConnect()){
const serverURL = new URL('my://' + this.server.getAddress())
mcArgs.push('--server')
mcArgs.push(serverURL.hostname)
if(serverURL.port){
mcArgs.push('--port')
mcArgs.push(serverURL.port)
}
}
// Prepare game resolution
if(ConfigManager.getFullscreen()){
mcArgs.push('--fullscreen')
mcArgs.push(true)
} else {
mcArgs.push('--width')
mcArgs.push(ConfigManager.getGameWidth())
mcArgs.push('--height')
mcArgs.push(ConfigManager.getGameHeight())
}
// Mod List File Argument
mcArgs.push('--modListFile')
if(this._isBelowOneDotSeven()) {
mcArgs.push(path.basename(this.fmlDir))
} else {
mcArgs.push('absolute:' + this.fmlDir)
}
// LiteLoader
if(this.usingLiteLoader){
mcArgs.push('--modRepo')
mcArgs.push(this.llDir)
// Set first arg to liteloader tweak class
mcArgs.unshift('com.mumfrey.liteloader.launch.LiteLoaderTweaker')
mcArgs.unshift('--tweakClass')
}
return mcArgs
}
/**
* Resolve the full classpath argument list for this process. This method will resolve all Mojang-declared
* libraries as well as the libraries declared by the server. Since mods are permitted to declare libraries,
* this method requires all enabled mods as an input
*
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
* @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the paths of each library required by this process.
*/
classpathArg(mods, tempNativePath){
let cpArgs = []
// Add the version.jar to the classpath.
const version = this.versionData.id
cpArgs.push(path.join(this.commonDir, 'versions', version, version + '.jar'))
if(this.usingLiteLoader){
cpArgs.push(this.llPath)
}
// Resolve the Mojang declared libraries.
const mojangLibs = this._resolveMojangLibraries(tempNativePath)
// Resolve the server declared libraries.
const servLibs = this._resolveServerLibraries(mods)
// Merge libraries, server libs with the same
// maven identifier will override the mojang ones.
// Ex. 1.7.10 forge overrides mojang's guava with newer version.
const finalLibs = {...mojangLibs, ...servLibs}
cpArgs = cpArgs.concat(Object.values(finalLibs))
return cpArgs
}
/**
* Resolve the libraries defined by Mojang's version data. This method will also extract
* native libraries and point to the correct location for its classpath.
*
* TODO - clean up function
*
* @param {string} tempNativePath The path to store the native libraries.
* @returns {{[id: string]: string}} An object containing the paths of each library mojang declares.
*/
_resolveMojangLibraries(tempNativePath){
const libs = {}
const libArr = this.versionData.libraries
fs.ensureDirSync(tempNativePath)
for(let i=0; i<libArr.length; i++){
const lib = libArr[i]
if(LibraryInternal.validateRules(lib.rules, lib.natives)){
if(lib.natives == null){
const dlInfo = lib.downloads
const artifact = dlInfo.artifact
const to = path.join(this.libPath, artifact.path)
const versionIndependentId = lib.name.substring(0, lib.name.lastIndexOf(':'))
libs[versionIndependentId] = to
} else {
// Extract the native library.
const exclusionArr = lib.extract != null ? lib.extract.exclude : ['META-INF/']
const artifact = lib.downloads.classifiers[lib.natives[LibraryInternal.mojangFriendlyOS()].replace('${arch}', process.arch.replace('x', ''))]
// Location of native zip.
const to = path.join(this.libPath, artifact.path)
let zip = new AdmZip(to)
let zipEntries = zip.getEntries()
// Unzip the native zip.
for(let i=0; i<zipEntries.length; i++){
const fileName = zipEntries[i].entryName
let shouldExclude = false
// Exclude noted files.
exclusionArr.forEach(function(exclusion){
if(fileName.indexOf(exclusion) > -1){
shouldExclude = true
}
})
// Extract the file.
if(!shouldExclude){
fs.writeFile(path.join(tempNativePath, fileName), zipEntries[i].getData(), (err) => {
if(err){
logger.error('Error while extracting native library:', err)
}
})
}
}
}
}
}
return libs
}
/**
* Resolve the libraries declared by this server in order to add them to the classpath.
* This method will also check each enabled mod for libraries, as mods are permitted to
* declare libraries.
*
* @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
* @returns {{[id: string]: string}} An object containing the paths of each library this server requires.
*/
_resolveServerLibraries(mods){
const mdls = this.server.getModules()
let libs = {}
// Locate Forge/Libraries
for(let mdl of mdls){
const type = mdl.getType()
if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Library){
libs[mdl.getVersionlessID()] = mdl.getArtifact().getPath()
if(mdl.hasSubModules()){
const res = this._resolveModuleLibraries(mdl)
if(res.length > 0){
libs = {...libs, ...res}
}
}
}
}
//Check for any libraries in our mod list.
for(let i=0; i<mods.length; i++){
if(mods.sub_modules != null){
const res = this._resolveModuleLibraries(mods[i])
if(res.length > 0){
libs = {...libs, ...res}
}
}
}
return libs
}
/**
* Recursively resolve the path of each library required by this module.
*
* @param {Object} mdl A module object from the server distro index.
* @returns {Array.<string>} An array containing the paths of each library this module requires.
*/
_resolveModuleLibraries(mdl){
if(!mdl.hasSubModules()){
return []
}
let libs = []
for(let sm of mdl.getSubModules()){
if(sm.getType() === DistroManager.Types.Library){
libs.push(sm.getArtifact().getPath())
}
// If this module has submodules, we need to resolve the libraries for those.
// To avoid unnecessary recursive calls, base case is checked here.
if(mdl.hasSubModules()){
const res = this._resolveModuleLibraries(sm)
if(res.length > 0){
libs = libs.concat(res)
}
}
}
return libs
}
}
module.exports = ProcessBuilder

View File

@ -1,67 +0,0 @@
import { connect } from 'net'
/**
* Retrieves the status of a minecraft server.
*
* @param {string} address The server address.
* @param {number} port Optional. The port of the server. Defaults to 25565.
* @returns {Promise.<Object>} A promise which resolves to an object containing
* status information.
*/
export function getStatus(address: string, port: number | string = 25565){
let sanitizedPort: number
if(port == null || port == ''){
sanitizedPort = 25565
}
if(typeof port === 'string'){
sanitizedPort = parseInt(port)
}
return new Promise((resolve, reject) => {
const socket = connect(sanitizedPort, address, () => {
let buff = Buffer.from([0xFE, 0x01])
socket.write(buff)
})
socket.setTimeout(2500, () => {
socket.end()
reject({
code: 'ETIMEDOUT',
errno: 'ETIMEDOUT',
address,
sanitizedPort
})
})
socket.on('data', (data) => {
if(data != null){
let server_info = data.toString().split('\x00\x00\x00')
const NUM_FIELDS = 6
if(server_info != null && server_info.length >= NUM_FIELDS){
resolve({
online: true,
version: server_info[2].replace(/\u0000/g, ''),
motd: server_info[3].replace(/\u0000/g, ''),
onlinePlayers: server_info[4].replace(/\u0000/g, ''),
maxPlayers: server_info[5].replace(/\u0000/g,'')
})
} else {
resolve({
online: false
})
}
}
socket.end()
})
socket.on('error', (err) => {
socket.destroy()
reject(err)
// ENOTFOUND = Unable to resolve.
// ECONNREFUSED = Unable to connect to port.
})
})
}