Added metadata readers for litemods and forgemods.

This commit is contained in:
Daniel Scalzi
2019-08-22 00:10:15 -04:00
parent 9e6d057462
commit 79ab8abad2
10 changed files with 187 additions and 23 deletions

9
package-lock.json generated
View File

@@ -24,6 +24,15 @@
"js-tokens": "^4.0.0" "js-tokens": "^4.0.0"
} }
}, },
"@types/adm-zip": {
"version": "0.4.32",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.32.tgz",
"integrity": "sha512-hv1O7ySn+XvP5OeDQcJFWwVb2v+GFGO1A9aMTQ5B/bzxb7WW21O8iRhVdsKKr8QwuiagzGmPP+gsUAYZ6bRddQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/fs-extra": { "@types/fs-extra": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.0.0.tgz",

View File

@@ -26,6 +26,7 @@
}, },
"homepage": "https://github.com/dscalzi/Nebula#readme", "homepage": "https://github.com/dscalzi/Nebula#readme",
"devDependencies": { "devDependencies": {
"@types/adm-zip": "^0.4.32",
"@types/fs-extra": "^8.0.0", "@types/fs-extra": "^8.0.0",
"@types/node": "^12.7.2", "@types/node": "^12.7.2",
"@types/yargs": "^13.0.2", "@types/yargs": "^13.0.2",

View File

@@ -0,0 +1,22 @@
// https://mcforge.readthedocs.io/en/latest/gettingstarted/structuring/#the-mcmodinfo-file
export interface McModInfo {
modid: string
name: string
description: string
version: string
mcversion: string
url: string
updateUrl?: string
updateJSON: string
authorList: string[]
credits: string
logoFile: string
screenshots: string[]
parent: string
useDependencyInformation: boolean
requiredMods: string[]
dependencies: string[]
dependants: string[] // Spelled as dependants on forge's wiki.
}

View File

@@ -0,0 +1,7 @@
import { McModInfo } from './mcmodinfo'
export interface McModInfoList {
modListVersion: number
modList: McModInfo[]
}

View File

@@ -0,0 +1,10 @@
export interface LiteMod {
name: string
version: string
mcversion: string
revision: string
description: string
author?: string
}

View File

@@ -1,4 +1,6 @@
import { Stats } from 'fs' import { Stats } from 'fs'
import { join } from 'path'
import { resolve } from 'url'
import { Type } from '../../spec/type' import { Type } from '../../spec/type'
import { ModuleStructure } from './module.struct' import { ModuleStructure } from './module.struct'
@@ -18,11 +20,11 @@ export class FileStructure extends ModuleStructure {
protected async getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name return name
} }
protected async getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModuleUrl(name: string, path: string, stats: Stats): Promise<string> {
return 'TODO' return resolve(this.baseUrl, join(this.relativeRoot, name))
} }
protected async getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModulePath(name: string, path: string, stats: Stats): Promise<string | null> {
return 'TODO' return name
} }
} }

View File

@@ -1,9 +1,17 @@
import AdmZip from 'adm-zip'
import { Stats } from 'fs-extra' import { Stats } from 'fs-extra'
import { join } from 'path'
import { resolve } from 'url'
import { capitalize } from '../../../util/stringutils'
import { McModInfo } from '../../forge/mcmodinfo'
import { McModInfoList } from '../../forge/mcmodinfolist'
import { Type } from '../../spec/type' import { Type } from '../../spec/type'
import { ModuleStructure } from './module.struct' import { ModuleStructure } from './module.struct'
export class ForgeModStructure extends ModuleStructure { export class ForgeModStructure extends ModuleStructure {
private forgeModMetadata: {[property: string]: McModInfo | undefined} = {}
constructor( constructor(
absoluteRoot: string, absoluteRoot: string,
relativeRoot: string, relativeRoot: string,
@@ -13,16 +21,77 @@ export class ForgeModStructure extends ModuleStructure {
} }
protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name const fmData = this.getForgeModMetadata(buf, name)
return this.generateMavenIdentifier(fmData.modid, fmData.version)
} }
protected async getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name return capitalize((this.getForgeModMetadata(buf, name)).name)
} }
protected async getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModuleUrl(name: string, path: string, stats: Stats): Promise<string> {
return 'TODO' return resolve(this.baseUrl, join(this.relativeRoot, name))
} }
protected async getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModulePath(name: string, path: string, stats: Stats): Promise<string | null> {
return 'TODO' return null
}
private getForgeModMetadata(buf: Buffer, name: string): McModInfo {
if (!this.forgeModMetadata.hasOwnProperty(name)) {
const zip = new AdmZip(buf)
const zipEntries = zip.getEntries()
// Optifine is a tweak that can be loaded as a forge mod. It does not
// appear to contain a mcmod.info class. This a special case we will
// account for.
if (name.toLowerCase().indexOf('optifine') > -1) {
// Read zip for changelog.txt
let rawChangelog
for (const entry of zipEntries) {
if (entry.entryName === 'changelog.txt') {
rawChangelog = zip.readAsText(entry)
break
}
}
if (!rawChangelog) {
throw new Error('Failed to read OptiFine changelog.')
}
const info = rawChangelog.split('\n')[0].trim()
const version = info.split(' ')[1]
this.forgeModMetadata[name] = ({
modid: 'optifine',
name: info,
version,
mcversion: version.substring(0, version.indexOf('_'))
}) as unknown as McModInfo
return this.forgeModMetadata[name] as McModInfo
}
let raw
for (const entry of zipEntries) {
if (entry.entryName === 'mcmod.info') {
raw = zip.readAsText(entry)
break
}
}
if (raw) {
// Assuming the main mod will be the first entry in this file.
const resolved = JSON.parse(raw) as object
if (resolved.hasOwnProperty('modListVersion')) {
this.forgeModMetadata[name] = (resolved as McModInfoList).modList[0]
} else {
this.forgeModMetadata[name] = (resolved as McModInfo[])[0]
}
} else {
console.error(`ForgeMod ${name} does not contain mcmod.info file.`)
this.forgeModMetadata[name] = ({
modid: name.substring(0, name.lastIndexOf('.')).toLowerCase(),
name,
version: '0.0.0'
}) as unknown as McModInfo
}
}
return this.forgeModMetadata[name] as McModInfo
} }
} }

View File

@@ -1,10 +1,16 @@
import AdmZip from 'adm-zip'
import { Stats } from 'fs-extra' import { Stats } from 'fs-extra'
import { join } from 'path' import { join } from 'path'
import { resolve } from 'url'
import { capitalize } from '../../../util/stringutils'
import { LiteMod } from '../../liteloader/litemod'
import { Type } from '../../spec/type' import { Type } from '../../spec/type'
import { ModuleStructure } from './module.struct' import { ModuleStructure } from './module.struct'
export class LiteModStructure extends ModuleStructure { export class LiteModStructure extends ModuleStructure {
private liteModMetadata: {[property: string]: LiteMod | undefined} = {}
constructor( constructor(
absoluteRoot: string, absoluteRoot: string,
relativeRoot: string, relativeRoot: string,
@@ -14,16 +20,40 @@ export class LiteModStructure extends ModuleStructure {
} }
protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name const liteModData = this.getLiteModMetadata(buf, name)
return this.generateMavenIdentifier(liteModData.name, `${liteModData.version}-${liteModData.mcversion}`)
} }
protected async getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> {
return name return capitalize(this.getLiteModMetadata(buf, name).name)
} }
protected async getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModuleUrl(name: string, path: string, stats: Stats): Promise<string> {
return 'TODO' return resolve(this.baseUrl, join(this.relativeRoot, name))
} }
protected async getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> { protected async getModulePath(name: string, path: string, stats: Stats): Promise<string | null> {
return 'TODO' return null
}
private getLiteModMetadata(buf: Buffer, name: string): LiteMod {
if (!this.liteModMetadata.hasOwnProperty(name)) {
const zip = new AdmZip(buf)
const zipEntries = zip.getEntries()
let raw
for (const entry of zipEntries) {
if (entry.entryName === 'litemod.json') {
raw = zip.readAsText(entry)
break
}
}
if (raw) {
this.liteModMetadata[name] = JSON.parse(raw) as LiteMod
} else {
throw new Error(`Litemod ${name} does not contain litemod.json file.`)
}
}
return this.liteModMetadata[name] as LiteMod
} }
} }

View File

@@ -2,7 +2,7 @@ import { createHash } from 'crypto'
import { lstat, pathExists, readdir, readFile, Stats } from 'fs-extra' import { lstat, pathExists, readdir, readFile, Stats } from 'fs-extra'
import { resolve } from 'path' import { resolve } from 'path'
import { Module } from '../../spec/module' import { Module } from '../../spec/module'
import { Type } from '../../spec/type' import { Type, TypeMetadata } from '../../spec/type'
import { BaseModelStructure } from '../basemodel.struct' import { BaseModelStructure } from '../basemodel.struct'
export abstract class ModuleStructure extends BaseModelStructure<Module> { export abstract class ModuleStructure extends BaseModelStructure<Module> {
@@ -25,10 +25,14 @@ export abstract class ModuleStructure extends BaseModelStructure<Module> {
return this.resolvedModels return this.resolvedModels
} }
protected generateMavenIdentifier(name: string, version: string) {
return `generated.${this.type.toLowerCase()}:${name}-${version}@${TypeMetadata[this.type].defaultExtension}`
}
protected async abstract getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> protected async abstract getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise<string>
protected async abstract getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> protected async abstract getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise<string>
protected async abstract getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> protected async abstract getModuleUrl(name: string, path: string, stats: Stats): Promise<string>
protected async abstract getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise<string> protected async abstract getModulePath(name: string, path: string, stats: Stats): Promise<string | null>
private async _doModuleRetrieval(): Promise<Module[]> { private async _doModuleRetrieval(): Promise<Module[]> {
@@ -41,7 +45,7 @@ export abstract class ModuleStructure extends BaseModelStructure<Module> {
const stats = await lstat(filePath) const stats = await lstat(filePath)
const buf = await readFile(filePath) const buf = await readFile(filePath)
if (stats.isFile()) { if (stats.isFile()) {
accumulator.push({ const mdl: Module = {
id: await this.getModuleId(file, filePath, stats, buf), id: await this.getModuleId(file, filePath, stats, buf),
name: await this.getModuleName(file, filePath, stats, buf), name: await this.getModuleName(file, filePath, stats, buf),
type: this.type, type: this.type,
@@ -52,10 +56,14 @@ export abstract class ModuleStructure extends BaseModelStructure<Module> {
artifact: { artifact: {
size: stats.size, size: stats.size,
MD5: createHash('md5').update(buf).digest('hex'), MD5: createHash('md5').update(buf).digest('hex'),
url: await this.getModuleUrl(file, filePath, stats, buf), url: await this.getModuleUrl(file, filePath, stats)
path: await this.getModulePath(file, filePath, stats, buf)
} }
}) }
const pth = await this.getModulePath(file, filePath, stats)
if (pth) {
mdl.artifact.path = pth
}
accumulator.push(mdl)
} }
} }
} }

6
src/util/stringutils.ts Normal file
View File

@@ -0,0 +1,6 @@
export function capitalize(str: string) {
if (!str) {
return str
}
return str.charAt(0).toUpperCase() + str.slice(1)
}