import AdmZip from 'adm-zip' import { ForgeResolver } from '../forge.resolver' import { MinecraftVersion } from '../../../util/MinecraftVersion' import { LoggerUtil } from '../../../util/LoggerUtil' import { VersionUtil } from '../../../util/versionutil' import { Module, Type } from 'helios-distribution-types' import { LibRepoStructure } from '../../../model/struct/repo/librepo.struct' import { pathExists, remove, mkdirs, copy, writeFile, readFile, lstat, move, writeJson } from 'fs-extra' import { join, basename, dirname } from 'path' import { spawn } from 'child_process' import { JavaUtil } from '../../../util/javautil' import { VersionManifestFG3 } from '../../../model/forge/VersionManifestFG3' import { MavenUtil } from '../../../util/maven' import { createHash } from 'crypto' interface GeneratedFile { name: string group: string artifact: string version: string classifiers: string[] | [undefined] skipIfNotPresent?: boolean } export class ForgeGradle3Adapter extends ForgeResolver { private static readonly logger = LoggerUtil.getLogger('FG3 Adapter') private static readonly WILDCARD_MCP_VERSION = '${mcpVersion}' public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { if(version.getMinor() === 12 && VersionUtil.isOneDotTwelveFG2(libraryVersion)) { return false } return VersionUtil.isVersionAcceptable(version, [12, 13, 14, 15]) } private generatedFiles: GeneratedFile[] | undefined private wildcardsInUse: string[] | undefined constructor( absoluteRoot: string, relativeRoot: string, baseUrl: string, minecraftVersion: MinecraftVersion, forgeVersion: string ) { super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, forgeVersion) this.configure() } private configure(): void { // Configure for 13, 14, 15 if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [13, 14, 15])) { this.generatedFiles = [ { name: 'base jar', group: LibRepoStructure.FORGE_GROUP, artifact: LibRepoStructure.FORGE_ARTIFACT, version: this.artifactVersion, classifiers: [undefined] }, { name: 'universal jar', group: LibRepoStructure.FORGE_GROUP, artifact: LibRepoStructure.FORGE_ARTIFACT, version: this.artifactVersion, classifiers: ['universal'] }, { name: 'client jar', group: LibRepoStructure.FORGE_GROUP, artifact: LibRepoStructure.FORGE_ARTIFACT, version: this.artifactVersion, classifiers: ['client'] }, { name: 'client slim', group: LibRepoStructure.MINECRAFT_GROUP, artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT, version: this.minecraftVersion.toString(), classifiers: [ 'slim', 'slim-stable' ] }, { name: 'client data', group: LibRepoStructure.MINECRAFT_GROUP, artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT, version: this.minecraftVersion.toString(), classifiers: ['data'], skipIfNotPresent: true }, { name: 'client extra', group: LibRepoStructure.MINECRAFT_GROUP, artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT, version: this.minecraftVersion.toString(), classifiers: [ 'extra', 'extra-stable' ] }, { name: 'client srg', group: LibRepoStructure.MINECRAFT_GROUP, artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT, version: `${this.minecraftVersion}-${ForgeGradle3Adapter.WILDCARD_MCP_VERSION}`, classifiers: ['srg'] } ] this.wildcardsInUse = [ ForgeGradle3Adapter.WILDCARD_MCP_VERSION ] return } // Configure for 12 if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [12])) { // NOTHING TO CONFIGURE return } } public async getModule(): Promise { return this.process() } public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { return ForgeGradle3Adapter.isForVersion(version, libraryVersion) } private async process(): Promise { const libRepo = this.repoStructure.getLibRepoStruct() // Get Installer const installerPath = libRepo.getLocalForge(this.artifactVersion, 'installer') ForgeGradle3Adapter.logger.debug(`Checking for forge installer at ${installerPath}..`) if (!await libRepo.artifactExists(installerPath)) { ForgeGradle3Adapter.logger.debug('Forge installer not found locally, initializing download..') await libRepo.downloadArtifactByComponents( this.REMOTE_REPOSITORY, LibRepoStructure.FORGE_GROUP, LibRepoStructure.FORGE_ARTIFACT, this.artifactVersion, 'installer', 'jar' ) } else { ForgeGradle3Adapter.logger.debug('Using locally discovered forge installer.') } ForgeGradle3Adapter.logger.debug(`Beginning processing of Forge v${this.forgeVersion} (Minecraft ${this.minecraftVersion})`) if(this.generatedFiles != null && this.generatedFiles.length > 0) { // Run installer return this.processWithInstaller(installerPath) } else { // Installer not required return this.processWithoutInstaller(installerPath) } } private async processWithInstaller(installerPath: string): Promise { const workDir = this.repoStructure.getWorkDirectory() if (await pathExists(workDir)) { await remove(workDir) } await mkdirs(workDir) const workingInstaller = join(workDir, basename(installerPath)) await copy(installerPath, workingInstaller) // Required for the installer to function. await writeFile(join(workDir, 'launcher_profiles.json'), JSON.stringify({})) ForgeGradle3Adapter.logger.debug('Spawning forge installer') ForgeGradle3Adapter.logger.info('============== [ IMPORTANT ] ==============') ForgeGradle3Adapter.logger.info('When the installer opens please set the client installation directory to:') ForgeGradle3Adapter.logger.info(workDir) ForgeGradle3Adapter.logger.info('===========================================') await this.executeInstaller(workingInstaller) ForgeGradle3Adapter.logger.debug('Installer finished, beginning processing..') ForgeGradle3Adapter.logger.debug('Processing Version Manifest') const versionManifestTuple = await this.processVersionManifest() const versionManifest = versionManifestTuple[0] as VersionManifestFG3 ForgeGradle3Adapter.logger.debug('Processing generated forge files.') const forgeModule = await this.processForgeModule(versionManifest) // Attach version.json module. forgeModule.subModules?.unshift(versionManifestTuple[1] as Module) ForgeGradle3Adapter.logger.debug('Processing Libraries') const libs = await this.processLibraries(versionManifest) forgeModule.subModules = forgeModule.subModules?.concat(libs) await remove(workDir) return forgeModule } private async processVersionManifest(): Promise<[VersionManifestFG3, Module]> { const workDir = this.repoStructure.getWorkDirectory() const versionRepo = this.repoStructure.getVersionRepoStruct() const versionName = versionRepo.getFileName(this.minecraftVersion, this.forgeVersion) const versionManifestPath = join(workDir, 'versions', versionName, `${versionName}.json`) const versionManifestBuf = await readFile(versionManifestPath) const versionManifest = JSON.parse(versionManifestBuf.toString()) as VersionManifestFG3 const versionManifestModule: Module = { id: this.artifactVersion, name: 'Minecraft Forge (version.json)', type: Type.VersionManifest, artifact: this.generateArtifact( versionManifestBuf, await lstat(versionManifestPath), versionRepo.getVersionManifestURL(this.baseUrl, this.minecraftVersion, this.forgeVersion) ) } const destination = versionRepo.getVersionManifest( this.minecraftVersion, this.forgeVersion ) await move(versionManifestPath, destination, {overwrite: true}) return [versionManifest, versionManifestModule] } private async processForgeModule(versionManifest: VersionManifestFG3): Promise { const libDir = join(this.repoStructure.getWorkDirectory(), 'libraries') if(this.wildcardsInUse) { if(this.wildcardsInUse.indexOf(ForgeGradle3Adapter.WILDCARD_MCP_VERSION) > -1) { const mcpVersion = this.getMCPVersion(versionManifest.arguments.game) if(mcpVersion == null) { throw new Error('MCP Version not found.. did forge change their format?') } this.generatedFiles = this.generatedFiles!.map(f => { if(f.version.indexOf(ForgeGradle3Adapter.WILDCARD_MCP_VERSION) > -1) { return { ...f, version: f.version.replace(ForgeGradle3Adapter.WILDCARD_MCP_VERSION, mcpVersion) } } return f }) } } const mdls: Module[] = [] for (const entry of this.generatedFiles!) { const targetLocations: string[] = [] let located = false classifierLoop: for (const _classifier of entry.classifiers) { const targetLocalPath = join( libDir, MavenUtil.mavenComponentsToPath(entry.group, entry.artifact, entry.version, _classifier) ) targetLocations.push(targetLocalPath) const exists = await pathExists(targetLocalPath) if (exists) { mdls.push({ id: MavenUtil.mavenComponentsToIdentifier( entry.group, entry.artifact, entry.version, _classifier ), name: `Minecraft Forge (${entry.name})`, type: Type.Library, artifact: this.generateArtifact( await readFile(targetLocalPath), await lstat(targetLocalPath), this.repoStructure.getLibRepoStruct().getArtifactUrlByComponents( this.baseUrl, entry.group, entry.artifact, entry.version, _classifier ) ), subModules: [] }) const destination = this.repoStructure.getLibRepoStruct().getArtifactByComponents( entry.group, entry.artifact, entry.version, _classifier ) await move(targetLocalPath, destination, {overwrite: true}) located = true break classifierLoop } } if (!entry.skipIfNotPresent && !located) { throw new Error(`Required file ${entry.name} not found at any expected location:\n\t${targetLocations.join('\n\t')}`) } } const forgeModule = mdls.shift() as Module forgeModule.type = Type.ForgeHosted forgeModule.subModules = mdls return forgeModule } private async processLibraries(manifest: VersionManifestFG3): Promise { const libDir = join(this.repoStructure.getWorkDirectory(), 'libraries') const libRepo = this.repoStructure.getLibRepoStruct() const mdls: Module[] = [] for (const entry of manifest.libraries) { const artifact = entry.downloads.artifact if (artifact.url) { const targetLocalPath = join(libDir, artifact.path) if (!await pathExists(targetLocalPath)) { throw new Error(`Expected library ${entry.name} not found!`) } const components = MavenUtil.getMavenComponents(entry.name) mdls.push({ id: entry.name, name: `Minecraft Forge (${components.artifact})`, type: Type.Library, artifact: this.generateArtifact( await readFile(targetLocalPath), await lstat(targetLocalPath), libRepo.getArtifactUrlByComponents( this.baseUrl, components.group, components.artifact, components.version, components.classifier, components.extension ) ) }) const destination = libRepo.getArtifactByComponents( components.group, components.artifact, components.version, components.classifier, components.extension ) await move(targetLocalPath, destination, {overwrite: true}) } } return mdls } private executeInstaller(installerExec: string): Promise { return new Promise(resolve => { const fiLogger = LoggerUtil.getLogger('Forge Installer') const child = spawn(JavaUtil.getJavaExecutable(), [ '-jar', installerExec ], { cwd: dirname(installerExec) }) child.stdout.on('data', (data) => fiLogger.info(data.toString('utf8').trim())) child.stderr.on('data', (data) => fiLogger.error(data.toString('utf8').trim())) child.on('close', code => { if(code === 0) { fiLogger.info('Exited with code', code) } else { fiLogger.error('Exited with code', code) } resolve() }) }) } private getMCPVersion(args: string[]): string | null { for (let i = 0; i < args.length; i++) { if (args[i] === '--fml.mcpVersion') { return args[i + 1] } } return null } private async processWithoutInstaller(installerPath: string): Promise { // Extract version.json from installer. const forgeInstallerBuffer = await readFile(installerPath) const zip = new AdmZip(forgeInstallerBuffer) const zipEntries = zip.getEntries() let versionManifest for (const entry of zipEntries) { if (entry.entryName === 'version.json') { versionManifest = zip.readAsText(entry) break } } if (!versionManifest) { throw new Error('Failed to find version.json in forge installer jar.') } versionManifest = JSON.parse(versionManifest) as VersionManifestFG3 // Save Version Manifest const versionManifestDest = this.repoStructure.getVersionRepoStruct().getVersionManifest( this.minecraftVersion, this.forgeVersion ) await mkdirs(dirname(versionManifestDest)) await writeJson(versionManifestDest, versionManifest, { spaces: 4 }) const libRepo = this.repoStructure.getLibRepoStruct() const universalLocalPath = libRepo.getLocalForge(this.artifactVersion, 'universal') ForgeGradle3Adapter.logger.debug(`Checking for Forge Universal jar at ${universalLocalPath}..`) const forgeMdl = versionManifest.libraries.find(val => val.name.startsWith('net.minecraftforge:forge:')) if(forgeMdl == null) { throw new Error('Forge entry not found in version.json!') } let forgeUniversalBuffer // Check for local universal jar. if (await libRepo.artifactExists(universalLocalPath)) { const localUniBuf = await readFile(universalLocalPath) const sha1 = createHash('sha1').update(localUniBuf).digest('hex') if(sha1 !== forgeMdl.downloads.artifact.sha1) { ForgeGradle3Adapter.logger.debug('SHA-1 of local universal jar does not match version.json entry.') ForgeGradle3Adapter.logger.debug('Redownloading Forge Universal jar..') } else { ForgeGradle3Adapter.logger.debug('Using locally discovered forge.') forgeUniversalBuffer = localUniBuf } } else { ForgeGradle3Adapter.logger.debug('Forge Universal jar not found locally, initializing download..') } // Download if local is missing or corrupt if(!forgeUniversalBuffer) { await libRepo.downloadArtifactByComponents( this.REMOTE_REPOSITORY, LibRepoStructure.FORGE_GROUP, LibRepoStructure.FORGE_ARTIFACT, this.artifactVersion, 'universal', 'jar') forgeUniversalBuffer = await readFile(universalLocalPath) } ForgeGradle3Adapter.logger.debug(`Beginning processing of Forge v${this.forgeVersion} (Minecraft ${this.minecraftVersion})`) const forgeModule: Module = { id: MavenUtil.mavenComponentsToIdentifier( LibRepoStructure.FORGE_GROUP, LibRepoStructure.FORGE_ARTIFACT, this.artifactVersion, 'universal' ), name: 'Minecraft Forge', type: Type.ForgeHosted, artifact: this.generateArtifact( forgeUniversalBuffer, await lstat(universalLocalPath), libRepo.getArtifactUrlByComponents( this.baseUrl, LibRepoStructure.FORGE_GROUP, LibRepoStructure.FORGE_ARTIFACT, this.artifactVersion, 'universal' ) ), subModules: [] } // Attach Version Manifest module. forgeModule.subModules?.push({ id: this.artifactVersion, name: 'Minecraft Forge (version.json)', type: Type.VersionManifest, artifact: this.generateArtifact( await readFile(versionManifestDest), await lstat(versionManifestDest), this.repoStructure.getVersionRepoStruct().getVersionManifestURL( this.baseUrl, this.minecraftVersion, this.forgeVersion) ) }) for(const lib of versionManifest.libraries) { if (lib.name.startsWith('net.minecraftforge:forge:')) { // We've already processed forge. continue } ForgeGradle3Adapter.logger.debug(`Processing ${lib.name}..`) const extension = 'jar' const localPath = libRepo.getArtifactById(lib.name, extension) let queueDownload = !await libRepo.artifactExists(localPath) let libBuf if (!queueDownload) { libBuf = await readFile(localPath) const sha1 = createHash('sha1').update(libBuf).digest('hex') if (sha1 !== lib.downloads.artifact.sha1) { ForgeGradle3Adapter.logger.debug('Hashes do not match, redownloading..') queueDownload = true } } else { ForgeGradle3Adapter.logger.debug('Not found locally, downloading..') queueDownload = true } if (queueDownload) { await libRepo.downloadArtifactDirect(lib.downloads.artifact.url, lib.downloads.artifact.path) libBuf = await readFile(localPath) } else { ForgeGradle3Adapter.logger.debug('Using local copy.') } const stats = await lstat(localPath) const mavenComponents = MavenUtil.getMavenComponents(lib.name) const properId = MavenUtil.mavenComponentsToIdentifier( mavenComponents.group, mavenComponents.artifact, mavenComponents.version, mavenComponents.classifier, extension ) forgeModule.subModules?.push({ id: properId, name: `Minecraft Forge (${mavenComponents?.artifact})`, type: Type.Library, artifact: this.generateArtifact( libBuf as Buffer, stats, libRepo.getArtifactUrlByComponents( this.baseUrl, mavenComponents.group, mavenComponents.artifact, mavenComponents.version, mavenComponents.classifier, extension ) ) }) } return forgeModule } }