Support for 1.13+ Forge Resolution.

Tested versions: 1.13.2
TODO: Test 1.14.4, 1.15.1
TODO: Read mod metadata from mods.toml in 1.13+
This commit is contained in:
Daniel Scalzi
2020-01-19 10:58:02 -05:00
parent e827dea7ff
commit 4d342b7b0a
10 changed files with 318 additions and 27 deletions

View File

@@ -0,0 +1,25 @@
export interface VersionManifest113 {
id: string
time: string
releaseTime: string
type: string
mainClass: string
inheritsFrom: string
logging: any
arguments: {
game: string[]
}
libraries: Array<{
name: string,
downloads: {
artifact: {
path: string,
url: string,
sha1: string,
size: number
}
}
}>
}

View File

@@ -1,4 +1,4 @@
export interface VersionManifest {
export interface VersionManifest17 {
id: string
time: string

View File

@@ -2,6 +2,9 @@ import { BaseMavenRepo } from './BaseMavenRepo'
export class LibRepoStructure extends BaseMavenRepo {
public static readonly MINECRAFT_GROUP = 'net.minecraft'
public static readonly MINECRAFT_CLIENT_ARTIFACT = 'client'
constructor(
absoluteRoot: string,
relativeRoot: string

View File

@@ -3,12 +3,14 @@ import { BaseFileStructure } from '../BaseFileStructure'
import { ForgeRepoStructure } from './forgerepo.struct'
import { LibRepoStructure } from './librepo.struct'
import { LiteLoaderRepoStructure } from './liteloaderrepo.struct'
import { VersionRepoStructure } from './versionrepo.struct'
export class RepoStructure extends BaseFileStructure {
private forgeRepoStruct: ForgeRepoStructure
private liteloaderRepoStruct: LiteLoaderRepoStructure
private libRepoStruct: LibRepoStructure
private versionRepoStruct: VersionRepoStructure
constructor(
absoluteRoot: string,
@@ -18,6 +20,7 @@ export class RepoStructure extends BaseFileStructure {
this.forgeRepoStruct = new ForgeRepoStructure(this.containerDirectory, this.relativeRoot)
this.liteloaderRepoStruct = new LiteLoaderRepoStructure(this.containerDirectory, this.relativeRoot)
this.libRepoStruct = new LibRepoStructure(this.containerDirectory, this.relativeRoot)
this.versionRepoStruct = new VersionRepoStructure(this.containerDirectory, this.relativeRoot)
}
public async init() {
@@ -25,6 +28,7 @@ export class RepoStructure extends BaseFileStructure {
await this.forgeRepoStruct.init()
await this.liteloaderRepoStruct.init()
await this.libRepoStruct.init()
await this.versionRepoStruct.init()
}
public getForgeRepoStruct() {
@@ -39,6 +43,10 @@ export class RepoStructure extends BaseFileStructure {
return this.libRepoStruct
}
public getVersionRepoStruct() {
return this.versionRepoStruct
}
public getTempDirectory() {
return join(this.absoluteRoot, 'temp')
}

View File

@@ -0,0 +1,28 @@
import { join } from 'path'
import { resolve as resolveURL } from 'url'
import { BaseFileStructure } from '../BaseFileStructure'
export class VersionRepoStructure extends BaseFileStructure {
constructor(
absoluteRoot: string,
relativeRoot: string
) {
super(absoluteRoot, relativeRoot, 'versions')
}
public getFileName(minecraftVersion: string, forgeVersion: string) {
return `${minecraftVersion}-forge-${forgeVersion}`
}
public getVersionManifest(minecraftVersion: string, forgeVersion: string) {
const fileName = this.getFileName(minecraftVersion, forgeVersion)
return join(this.containerDirectory, fileName, `${fileName}.json`)
}
public getVersionManifestURL(url: string, minecraftVersion: string, forgeVersion: string) {
const fileName = this.getFileName(minecraftVersion, forgeVersion)
return resolveURL(url, join(this.relativeRoot, fileName, `${fileName}.json`))
}
}

View File

@@ -1,11 +1,11 @@
import { Forge113Adapter } from './forge/adapter/forge113.resolver'
import { Forge18Adapter } from './forge/adapter/forge18.resolver'
import { Forge17Adapter } from './forge/adapter/forge17.resolver'
import { ForgeResolver } from './forge/forge.resolver'
export class ResolverRegistry {
public static readonly FORGE_ADAPTER_IMPL = [
Forge18Adapter,
Forge17Adapter,
Forge113Adapter
]

View File

@@ -1,9 +1,13 @@
import { spawn } from 'child_process'
import { copy, mkdirs, pathExists, remove, writeFile } from 'fs-extra'
import { basename, join } from 'path'
import { copy, lstat, mkdirs, move, pathExists, readFile, remove, writeFile } from 'fs-extra'
import { basename, dirname, join } from 'path'
import { VersionManifest113 } from '../../../model/forge/versionmanifest113'
import { Module } from '../../../model/spec/module'
import { Type } from '../../../model/spec/type'
import { ForgeRepoStructure } from '../../../model/struct/repo/forgerepo.struct'
import { LibRepoStructure } from '../../../model/struct/repo/librepo.struct'
import { JavaUtil } from '../../../util/javautil'
import { MavenUtil } from '../../../util/maven'
import { ForgeResolver } from '../forge.resolver'
export class Forge113Adapter extends ForgeResolver {
@@ -23,8 +27,7 @@ export class Forge113Adapter extends ForgeResolver {
}
public async getModule(): Promise<Module> {
await this.process()
return {} as unknown as Module
return this.process()
}
public isForVersion(version: string): boolean {
@@ -64,6 +67,226 @@ export class Forge113Adapter extends ForgeResolver {
console.debug(`Spawning forge installer`)
console.log('============== [ IMPORTANT ] ==============')
console.log('When the installer opens please set the client installation directory to:')
console.log(workDir)
console.log('===========================================')
await this.executeInstaller(workingInstaller)
console.debug('Installer finished, beginning processing..')
console.debug('Processing Version Manifest')
const versionManifestTuple = await this.processVersionManifest()
const versionManifest = versionManifestTuple[0] as VersionManifest113
console.debug('Processing generated forge files.')
const forgeModule = await this.processForgeModule()
// Attach version.json module.
forgeModule.subModules?.unshift(versionManifestTuple[1] as Module)
console.debug('Processing Libraries')
const libs = await this.processLibraries(versionManifest)
forgeModule.subModules = forgeModule.subModules?.concat(libs)
await remove(workDir)
return forgeModule
}
private async processLibraries(manifest: VersionManifest113) {
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 async processForgeModule() {
const libDir = join(this.repoStructure.getWorkDirectory(), 'libraries')
const generatedFiles = [
{
name: 'base jar',
group: ForgeRepoStructure.FORGE_GROUP,
artifact: ForgeRepoStructure.FORGE_ARTIFACT,
version: this.artifactVersion,
classifier: undefined
},
{
name: 'universal jar',
group: ForgeRepoStructure.FORGE_GROUP,
artifact: ForgeRepoStructure.FORGE_ARTIFACT,
version: this.artifactVersion,
classifier: 'universal'
},
{
name: 'client jar',
group: ForgeRepoStructure.FORGE_GROUP,
artifact: ForgeRepoStructure.FORGE_ARTIFACT,
version: this.artifactVersion,
classifier: 'client'
},
{
name: 'client slim',
group: LibRepoStructure.MINECRAFT_GROUP,
artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT,
version: this.minecraftVersion,
classifier: 'slim'
},
{
name: 'client data',
group: LibRepoStructure.MINECRAFT_GROUP,
artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT,
version: this.minecraftVersion,
classifier: 'data',
skipIfNotPresent: true
},
{
name: 'client extra',
group: LibRepoStructure.MINECRAFT_GROUP,
artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT,
version: this.minecraftVersion,
classifier: 'extra'
}
]
const mdls: Module[] = []
for (const entry of generatedFiles) {
const targetLocalPath = join(
libDir,
MavenUtil.mavenComponentsToPath(entry.group, entry.artifact, entry.version, entry.classifier)
)
const exists = await pathExists(targetLocalPath)
if (exists) {
mdls.push({
id: MavenUtil.mavenComponentsToIdentifier(
entry.group,
entry.artifact,
entry.version,
entry.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,
entry.classifier
)
),
subModules: []
})
const destination = this.repoStructure.getLibRepoStruct().getArtifactByComponents(
entry.group,
entry.artifact,
entry.version,
entry.classifier
)
await move(targetLocalPath, destination, {overwrite: true})
} else {
if (!entry.skipIfNotPresent) {
throw new Error(`Required file ${entry.name} not found at expected location ${targetLocalPath}!`)
}
}
}
const forgeModule = mdls.shift() as Module
forgeModule.type = Type.ForgeHosted
forgeModule.subModules = mdls
return forgeModule
}
private async processVersionManifest() {
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 VersionManifest113
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 = this.repoStructure.getVersionRepoStruct().getVersionManifest(
this.minecraftVersion,
this.forgeVersion
)
await move(versionManifestPath, destination, {overwrite: true})
return [versionManifest, versionManifestModule]
}
private executeInstaller(installerExec: string) {
@@ -71,9 +294,11 @@ export class Forge113Adapter extends ForgeResolver {
const child = spawn(JavaUtil.getJavaExecutable(), [
'-jar',
installerExec
])
child.stdout.on('data', (data) => console.log('[Forge Installer]', data.toString('utf8')))
child.stderr.on('data', (data) => console.error('[Forge Installer]', data.toString('utf8')))
], {
cwd: dirname(installerExec)
})
child.stdout.on('data', (data) => console.log('[Forge Installer]', data.toString('utf8').trim()))
child.stderr.on('data', (data) => console.error('[Forge Installer]', data.toString('utf8').trim()))
child.on('close', (code, signal) => {
console.log('[Forge Installer]', 'Exited with code', code)
resolve()

View File

@@ -1,9 +1,8 @@
import AdmZip from 'adm-zip'
import { createHash } from 'crypto'
import { copy, lstat, mkdirs, pathExists, readFile, remove, Stats } from 'fs-extra'
import { copy, lstat, mkdirs, pathExists, readFile, remove } from 'fs-extra'
import { basename, join } from 'path'
import { VersionManifest } from '../../../model/forge/versionmanifest'
import { Artifact } from '../../../model/spec/artifact'
import { VersionManifest17 } from '../../../model/forge/versionmanifest17'
import { Module } from '../../../model/spec/module'
import { Type } from '../../../model/spec/type'
import { ForgeRepoStructure } from '../../../model/struct/repo/forgerepo.struct'
@@ -11,10 +10,10 @@ import { MavenUtil } from '../../../util/maven'
import { PackXZExtractWrapper } from '../../../util/PackXZExtractWrapper'
import { ForgeResolver } from '../forge.resolver'
export class Forge18Adapter extends ForgeResolver {
export class Forge17Adapter extends ForgeResolver {
public static isForVersion(version: string) {
return Forge18Adapter.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12])
return Forge17Adapter.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12])
}
constructor(
@@ -32,7 +31,7 @@ export class Forge18Adapter extends ForgeResolver {
}
public isForVersion(version: string) {
return Forge18Adapter.isForVersion(version)
return Forge17Adapter.isForVersion(version)
}
public async getForgeByVersion() {
@@ -68,7 +67,7 @@ export class Forge18Adapter extends ForgeResolver {
throw new Error('Failed to find version.json in forge universal jar.')
}
versionManifest = JSON.parse(versionManifest) as VersionManifest
versionManifest = JSON.parse(versionManifest) as VersionManifest17
const forgeModule: Module = {
id: MavenUtil.mavenComponentsToIdentifier(
@@ -177,14 +176,6 @@ export class Forge18Adapter extends ForgeResolver {
return forgeModule
}
private generateArtifact(buf: Buffer, stats: Stats, url: string): Artifact {
return {
size: stats.size,
MD5: createHash('md5').update(buf).digest('hex'),
url
}
}
private determineExtension(checksums: string[] | undefined) {
return checksums != null && checksums.length > 1 ? 'jar.pack.xz' : 'jar'
}

View File

@@ -1,3 +1,6 @@
import { createHash } from 'crypto'
import { Stats } from 'fs-extra'
import { Artifact } from '../../model/spec/artifact'
import { RepoStructure } from '../../model/struct/repo/repo.struct'
import { BaseResolver } from '../baseresolver'
@@ -68,4 +71,12 @@ export abstract class ForgeResolver extends BaseResolver {
return version
}
protected generateArtifact(buf: Buffer, stats: Stats, url: string): Artifact {
return {
size: stats.size,
MD5: createHash('md5').update(buf).digest('hex'),
url
}
}
}

View File

@@ -28,8 +28,8 @@ export class PackXZExtractWrapper {
command,
paths.join(',')
])
child.stdout.on('data', (data) => console.log('[PackXZExtract]', data.toString('utf8')))
child.stderr.on('data', (data) => console.error('[PackXZExtract]', data.toString('utf8')))
child.stdout.on('data', (data) => console.log('[PackXZExtract]', data.toString('utf8').trim()))
child.stderr.on('data', (data) => console.error('[PackXZExtract]', data.toString('utf8').trim()))
child.on('close', (code, signal) => {
console.log('[PackXZExtract]', 'Exited with code', code)
resolve()