mirror of
https://github.com/dscalzi/HeliosLauncher.git
synced 2024-12-22 11:42:14 -08:00
5f3e229360
Implemented a new `dlAsync.js` file and updated the `en_US.toml` with new mod validation messages. This includes the logic for mod verification and error handling, ensuring the integrity of mods before launching the game. Ajout prochainement de l'utilitaire npm run athshield
306 lines
14 KiB
JavaScript
306 lines
14 KiB
JavaScript
/**
|
|
* @Name dlAsync Function
|
|
* @async
|
|
* @param {boolean} login
|
|
* @returns {Promise<void>}
|
|
*
|
|
* @author Sandro642
|
|
* @Cheating Athena's Shield
|
|
* @Ajout Liste blanche des mods
|
|
*/
|
|
|
|
/**
|
|
* @Révision le XX.XX.2024 périme le 01.01.2025
|
|
* @Bug découvert : 0
|
|
* @Athena's Shield
|
|
* @Sandro642
|
|
*/
|
|
|
|
// ▄▄▄ ▄▄▄█████▓ ██░ ██ ▓█████ ███▄ █ ▄▄▄ ██████ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄
|
|
// ▒████▄ ▓ ██▒ ▓▒▓██░ ██▒▓█ ▀ ██ ▀█ █ ▒████▄ ▒██ ▒ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌
|
|
// ▒██ ▀█▄ ▒ ▓██░ ▒░▒██▀▀██░▒███ ▓██ ▀█ ██▒▒██ ▀█▄ ░ ▓██▄ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌
|
|
// ░██▄▄▄▄██░ ▓██▓ ░ ░▓█ ░██ ▒▓█ ▄ ▓██▒ ▐▌██▒░██▄▄▄▄██ ▒ ██▒ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌
|
|
// ▓█ ▓██▒ ▒██▒ ░ ░▓█▒░██▓░▒████▒▒██░ ▓██░ ▓█ ▓██▒▒██████▒▒ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓
|
|
// ▒▒ ▓▒█░ ▒ ░░ ▒ ░░▒░▒░░ ▒░ ░░ ▒░ ▒ ▒ ▒▒ ▓▒█░▒ ▒▓▒ ▒ ░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒
|
|
// ▒ ▒▒ ░ ░ ▒ ░▒░ ░ ░ ░ ░░ ░░ ░ ▒░ ▒ ▒▒ ░░ ░▒ ░ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒
|
|
// ░ ▒ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░
|
|
// ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
|
|
// ░
|
|
|
|
// Keep reference to Minecraft Process
|
|
let proc
|
|
// Is DiscordRPC enabled
|
|
let hasRPC = false
|
|
// Joined server regex
|
|
const GAME_JOINED_REGEX = /\[.+\]: Sound engine started/
|
|
const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+|Loading Minecraft .+ with Fabric Loader .+)$/
|
|
const MIN_LINGER = 5000
|
|
|
|
// List of mods to exclude from validation
|
|
const EXCLUDED_MODS = [
|
|
]
|
|
|
|
async function dlAsync(login = true) {
|
|
const loggerLaunchSuite = LoggerUtil.getLogger('LaunchSuite')
|
|
const loggerLanding = LoggerUtil.getLogger('Landing')
|
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.loadingServerInfo'))
|
|
|
|
let distro
|
|
|
|
try {
|
|
distro = await DistroAPI.refreshDistributionOrFallback()
|
|
onDistroRefresh(distro)
|
|
} catch (err) {
|
|
loggerLanding.error(Lang.queryJS('landing.dlAsync.unableToLoadDistributionIndex'))
|
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.fatalError'), Lang.queryJS('landing.dlAsync.unableToLoadDistributionIndex'))
|
|
return
|
|
}
|
|
|
|
const serv = distro.getServerById(ConfigManager.getSelectedServer())
|
|
|
|
if (login) {
|
|
if (ConfigManager.getSelectedAccount() == null) {
|
|
loggerLanding.error(Lang.queryJS('landing.dlAsync.accountLoginNeeded'))
|
|
return
|
|
}
|
|
}
|
|
|
|
// --------- Mod Verification Logic ---------
|
|
const modsDir = path.join(ConfigManager.getDataDirectory(), 'instances', serv.rawServer.id, 'mods')
|
|
|
|
// Check if mods directory exists, if not, create it
|
|
if (!fs.existsSync(modsDir)) {
|
|
fs.mkdirSync(modsDir, { recursive: true })
|
|
}
|
|
|
|
const distroMods = {}
|
|
const mdls = serv.modules
|
|
|
|
// Populate expected mod identities and log them
|
|
mdls.forEach(mdl => {
|
|
if (mdl.rawModule.name.endsWith('.jar')) {
|
|
const modPath = path.join(modsDir, mdl.rawModule.name)
|
|
const modIdentity = mdl.rawModule.identity || mdl.rawModule.MD5
|
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.distributionIdentityError', {'moduleName': mdl.rawModule.name, 'moduleIdentity': modIdentity}))
|
|
distroMods[modPath] = modIdentity
|
|
}
|
|
})
|
|
|
|
// Function to extract mod identity from the jar file
|
|
const extractModIdentity = (filePath) => {
|
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modIdentityExtraction', {'filePath': filePath}))
|
|
const zip = new AdmZip(filePath)
|
|
const manifestEntry = zip.getEntry('META-INF/MANIFEST.MF')
|
|
|
|
if (manifestEntry) {
|
|
const manifestContent = manifestEntry.getData().toString('utf8')
|
|
const lines = manifestContent.split('\n')
|
|
const identityLine = lines.find(line => line.startsWith('Mod-Id:') || line.startsWith('Implementation-Title:'))
|
|
if (identityLine) {
|
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.manifestIdentityFound', {'filePath': filePath, 'identityLine': identityLine}))
|
|
return identityLine.split(':')[1].trim()
|
|
}
|
|
}
|
|
|
|
// Fall back to a hash if no identity is found
|
|
const fileBuffer = fs.readFileSync(filePath)
|
|
const hashSum = crypto.createHash('md5') // Use MD5 to match the distribution configuration
|
|
hashSum.update(fileBuffer)
|
|
const hash = hashSum.digest('hex')
|
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.identityNotFoundInManifest', {'filePath': filePath, 'hash': hash}))
|
|
return hash
|
|
}
|
|
|
|
// Validate mods function
|
|
const validateMods = () => {
|
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.startingModValidation'))
|
|
const installedMods = fs.readdirSync(modsDir)
|
|
let valid = true
|
|
|
|
for (let mod of installedMods) {
|
|
const modPath = path.join(modsDir, mod)
|
|
|
|
// Skip validation for mods in the excluded list
|
|
if (EXCLUDED_MODS.includes(mod)) {
|
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modValidationBypassed', {'mod': mod}))
|
|
continue
|
|
}
|
|
|
|
const expectedIdentity = distroMods[modPath]
|
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.validatingMod', {'mod': mod}))
|
|
|
|
if (expectedIdentity) {
|
|
const modIdentity = extractModIdentity(modPath)
|
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.expectedAndCalculatedIdentity', {'expectedIdentity': expectedIdentity, 'mod': mod, 'modIdentity': modIdentity}))
|
|
|
|
if (modIdentity !== expectedIdentity) {
|
|
loggerLanding.error(Lang.queryJS('landing.dlAsync.AthShield.modIdentityMismatchError', {'mod': mod, 'expectedIdentity': expectedIdentity, 'modIdentity': modIdentity}))
|
|
valid = false
|
|
break
|
|
}
|
|
} else {
|
|
loggerLanding.warn(Lang.queryJS('landing.dlAsync.AthShield.expectedIdentityNotFound', {'mod': mod}))
|
|
valid = false
|
|
break
|
|
}
|
|
}
|
|
|
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modValidationCompleted'))
|
|
return valid
|
|
}
|
|
|
|
// Perform mod validation before proceeding
|
|
if (!validateMods()) {
|
|
const errorMessage = Lang.queryJS('landing.dlAsync.AthShield.invalidModsDetectedMessage', {'folder': dataPath})
|
|
loggerLanding.error(errorMessage)
|
|
showLaunchFailure(errorMessage, null)
|
|
return
|
|
}
|
|
// --------- End of Mod Verification Logic ---------
|
|
|
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.pleaseWait'))
|
|
toggleLaunchArea(true)
|
|
setLaunchPercentage(0, 100)
|
|
|
|
const fullRepairModule = new FullRepair(
|
|
ConfigManager.getCommonDirectory(),
|
|
ConfigManager.getInstanceDirectory(),
|
|
ConfigManager.getLauncherDirectory(),
|
|
ConfigManager.getSelectedServer(),
|
|
DistroAPI.isDevMode()
|
|
)
|
|
|
|
fullRepairModule.spawnReceiver()
|
|
|
|
fullRepairModule.childProcess.on('error', (err) => {
|
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.errorDuringLaunchText') + err)
|
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), err.message || Lang.queryJS('landing.dlAsync.errorDuringLaunchText'))
|
|
})
|
|
fullRepairModule.childProcess.on('close', (code, _signal) => {
|
|
if(code !== 0){
|
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.fullRepairMode', {'code': code}))
|
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
|
}
|
|
})
|
|
|
|
loggerLaunchSuite.info(Lang.queryJS('landing.dlAsync.validatingFileIntegrity'))
|
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.validatingFileIntegrity'))
|
|
let invalidFileCount = 0
|
|
try {
|
|
invalidFileCount = await fullRepairModule.verifyFiles(percent => {
|
|
setLaunchPercentage(percent)
|
|
})
|
|
setLaunchPercentage(100)
|
|
} catch (err) {
|
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.errFileVerification'))
|
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringFileVerificationTitle'), err.displayable || Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
|
return
|
|
}
|
|
|
|
if(invalidFileCount > 0) {
|
|
loggerLaunchSuite.info('Downloading files.')
|
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.downloadingFiles'))
|
|
setLaunchPercentage(0)
|
|
try {
|
|
await fullRepairModule.download(percent => {
|
|
setDownloadPercentage(percent)
|
|
})
|
|
setDownloadPercentage(100)
|
|
} catch(err) {
|
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.errorDuringFileDownloadTitle'))
|
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringFileDownloadTitle'), err.displayable || Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
|
return
|
|
}
|
|
} else {
|
|
loggerLaunchSuite.info(Lang.queryJS('landing.dlAsync.AthShield.downloadingFiles'))
|
|
}
|
|
|
|
// Remove download bar.
|
|
remote.getCurrentWindow().setProgressBar(-1)
|
|
|
|
fullRepairModule.destroyReceiver()
|
|
|
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.preparingToLaunch'))
|
|
|
|
const mojangIndexProcessor = new MojangIndexProcessor(
|
|
ConfigManager.getCommonDirectory(),
|
|
serv.rawServer.minecraftVersion)
|
|
const distributionIndexProcessor = new DistributionIndexProcessor(
|
|
ConfigManager.getCommonDirectory(),
|
|
distro,
|
|
serv.rawServer.id
|
|
)
|
|
|
|
const modLoaderData = await distributionIndexProcessor.loadModLoaderVersionJson(serv)
|
|
const versionData = await mojangIndexProcessor.getVersionJson()
|
|
|
|
if(login) {
|
|
const authUser = ConfigManager.getSelectedAccount()
|
|
loggerLaunchSuite.info(Lang.queryJS('landing.dlAsync.accountToProcessBuilder', {'userDisplayName': authUser.displayName}))
|
|
let pb = new ProcessBuilder(serv, versionData, modLoaderData, authUser, remote.app.getVersion())
|
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.launchingGame'))
|
|
|
|
const SERVER_JOINED_REGEX = new RegExp(`\\[.+\\]: \\[CHAT\\] ${authUser.displayName} joined the game`)
|
|
|
|
const onLoadComplete = () => {
|
|
toggleLaunchArea(false)
|
|
|
|
proc.stdout.removeListener('data', tempListener)
|
|
proc.stderr.removeListener('data', gameErrorListener)
|
|
}
|
|
const start = Date.now()
|
|
|
|
// Attach a temporary listener to the client output.
|
|
const tempListener = function(data){
|
|
if(GAME_LAUNCH_REGEX.test(data.trim())){
|
|
const diff = Date.now()-start
|
|
if(diff < MIN_LINGER) {
|
|
setTimeout(onLoadComplete, MIN_LINGER-diff)
|
|
} else {
|
|
onLoadComplete()
|
|
}
|
|
}
|
|
}
|
|
|
|
const gameErrorListener = function(data){
|
|
if(data.trim().toLowerCase().includes('error')){
|
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.gameError', {'data': data}))
|
|
}
|
|
}
|
|
|
|
proc = pb.build()
|
|
|
|
proc.stdout.on('data', tempListener)
|
|
proc.stderr.on('data', gameErrorListener)
|
|
|
|
proc.stdout.on('data', function(data){
|
|
if(SERVER_JOINED_REGEX.test(data.trim())){
|
|
DiscordWrapper.updateDetails('Exploring the World')
|
|
} else if(GAME_JOINED_REGEX.test(data.trim())) {
|
|
DiscordWrapper.updateDetails('Main Menu')
|
|
}
|
|
})
|
|
|
|
proc.on('close', (code, _signal) => {
|
|
if (hasRPC) {
|
|
DiscordWrapper.shutdownRPC()
|
|
hasRPC = false
|
|
}
|
|
loggerLaunchSuite.info(Lang.queryJS('landing.dlAsync.gameExited', {'code': code}))
|
|
if(code !== 0){
|
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.gameExitedAbnormal'), Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
|
}
|
|
proc = null
|
|
})
|
|
|
|
proc.on('error', (err) => {
|
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.gameErrorDuringLaunch', {'error': err}))
|
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), err.message || Lang.queryJS('landing.dlAsync.errorDuringLaunchText'))
|
|
proc = null
|
|
})
|
|
|
|
setTimeout(() => {
|
|
loggerLaunchSuite.info(Lang.queryJS('landing.dlAsync.launchingGame'))
|
|
}, MIN_LINGER)
|
|
}
|
|
} |