diff --git a/package-lock.json b/package-lock.json index 8fa8ad6..efe9431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,15 @@ "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": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.0.0.tgz", diff --git a/package.json b/package.json index 72705bf..4afcb54 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "homepage": "https://github.com/dscalzi/Nebula#readme", "devDependencies": { + "@types/adm-zip": "^0.4.32", "@types/fs-extra": "^8.0.0", "@types/node": "^12.7.2", "@types/yargs": "^13.0.2", diff --git a/src/model/forge/mcmodinfo.ts b/src/model/forge/mcmodinfo.ts new file mode 100644 index 0000000..665b7a8 --- /dev/null +++ b/src/model/forge/mcmodinfo.ts @@ -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. + +} diff --git a/src/model/forge/mcmodinfolist.ts b/src/model/forge/mcmodinfolist.ts new file mode 100644 index 0000000..348e2d5 --- /dev/null +++ b/src/model/forge/mcmodinfolist.ts @@ -0,0 +1,7 @@ +import { McModInfo } from './mcmodinfo' + +export interface McModInfoList { + + modListVersion: number + modList: McModInfo[] +} diff --git a/src/model/liteloader/litemod.ts b/src/model/liteloader/litemod.ts new file mode 100644 index 0000000..9cf99f4 --- /dev/null +++ b/src/model/liteloader/litemod.ts @@ -0,0 +1,10 @@ +export interface LiteMod { + + name: string + version: string + mcversion: string + revision: string + description: string + author?: string + +} diff --git a/src/model/struct/module/file.struct.ts b/src/model/struct/module/file.struct.ts index 5bf8b53..0d37f21 100644 --- a/src/model/struct/module/file.struct.ts +++ b/src/model/struct/module/file.struct.ts @@ -1,4 +1,6 @@ import { Stats } from 'fs' +import { join } from 'path' +import { resolve } from 'url' import { Type } from '../../spec/type' 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 { return name } - protected async getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise { - return 'TODO' + protected async getModuleUrl(name: string, path: string, stats: Stats): Promise { + return resolve(this.baseUrl, join(this.relativeRoot, name)) } - protected async getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise { - return 'TODO' + protected async getModulePath(name: string, path: string, stats: Stats): Promise { + return name } } diff --git a/src/model/struct/module/forgemod.struct.ts b/src/model/struct/module/forgemod.struct.ts index 2c34279..fac0eb3 100644 --- a/src/model/struct/module/forgemod.struct.ts +++ b/src/model/struct/module/forgemod.struct.ts @@ -1,9 +1,17 @@ +import AdmZip from 'adm-zip' 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 { ModuleStructure } from './module.struct' export class ForgeModStructure extends ModuleStructure { + private forgeModMetadata: {[property: string]: McModInfo | undefined} = {} + constructor( absoluteRoot: string, relativeRoot: string, @@ -13,16 +21,77 @@ export class ForgeModStructure extends ModuleStructure { } protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise { - 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 { - return name + return capitalize((this.getForgeModMetadata(buf, name)).name) } - protected async getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise { - return 'TODO' + protected async getModuleUrl(name: string, path: string, stats: Stats): Promise { + return resolve(this.baseUrl, join(this.relativeRoot, name)) } - protected async getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise { - return 'TODO' + protected async getModulePath(name: string, path: string, stats: Stats): Promise { + 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 } } diff --git a/src/model/struct/module/litemod.struct.ts b/src/model/struct/module/litemod.struct.ts index a273d64..8f7eb72 100644 --- a/src/model/struct/module/litemod.struct.ts +++ b/src/model/struct/module/litemod.struct.ts @@ -1,10 +1,16 @@ +import AdmZip from 'adm-zip' import { Stats } from 'fs-extra' import { join } from 'path' +import { resolve } from 'url' +import { capitalize } from '../../../util/stringutils' +import { LiteMod } from '../../liteloader/litemod' import { Type } from '../../spec/type' import { ModuleStructure } from './module.struct' export class LiteModStructure extends ModuleStructure { + private liteModMetadata: {[property: string]: LiteMod | undefined} = {} + constructor( absoluteRoot: string, relativeRoot: string, @@ -14,16 +20,40 @@ export class LiteModStructure extends ModuleStructure { } protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise { - 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 { - return name + return capitalize(this.getLiteModMetadata(buf, name).name) } - protected async getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise { - return 'TODO' + protected async getModuleUrl(name: string, path: string, stats: Stats): Promise { + return resolve(this.baseUrl, join(this.relativeRoot, name)) } - protected async getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise { - return 'TODO' + protected async getModulePath(name: string, path: string, stats: Stats): Promise { + 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 } } diff --git a/src/model/struct/module/module.struct.ts b/src/model/struct/module/module.struct.ts index 5de62c5..23964fc 100644 --- a/src/model/struct/module/module.struct.ts +++ b/src/model/struct/module/module.struct.ts @@ -2,7 +2,7 @@ import { createHash } from 'crypto' import { lstat, pathExists, readdir, readFile, Stats } from 'fs-extra' import { resolve } from 'path' import { Module } from '../../spec/module' -import { Type } from '../../spec/type' +import { Type, TypeMetadata } from '../../spec/type' import { BaseModelStructure } from '../basemodel.struct' export abstract class ModuleStructure extends BaseModelStructure { @@ -25,10 +25,14 @@ export abstract class ModuleStructure extends BaseModelStructure { 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 protected async abstract getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise - protected async abstract getModuleUrl(name: string, path: string, stats: Stats, buf: Buffer): Promise - protected async abstract getModulePath(name: string, path: string, stats: Stats, buf: Buffer): Promise + protected async abstract getModuleUrl(name: string, path: string, stats: Stats): Promise + protected async abstract getModulePath(name: string, path: string, stats: Stats): Promise private async _doModuleRetrieval(): Promise { @@ -41,7 +45,7 @@ export abstract class ModuleStructure extends BaseModelStructure { const stats = await lstat(filePath) const buf = await readFile(filePath) if (stats.isFile()) { - accumulator.push({ + const mdl: Module = { id: await this.getModuleId(file, filePath, stats, buf), name: await this.getModuleName(file, filePath, stats, buf), type: this.type, @@ -52,10 +56,14 @@ export abstract class ModuleStructure extends BaseModelStructure { artifact: { size: stats.size, MD5: createHash('md5').update(buf).digest('hex'), - url: await this.getModuleUrl(file, filePath, stats, buf), - path: await this.getModulePath(file, filePath, stats, buf) + url: await this.getModuleUrl(file, filePath, stats) } - }) + } + const pth = await this.getModulePath(file, filePath, stats) + if (pth) { + mdl.artifact.path = pth + } + accumulator.push(mdl) } } } diff --git a/src/util/stringutils.ts b/src/util/stringutils.ts new file mode 100644 index 0000000..607b5b9 --- /dev/null +++ b/src/util/stringutils.ts @@ -0,0 +1,6 @@ +export function capitalize(str: string) { + if (!str) { + return str + } + return str.charAt(0).toUpperCase() + str.slice(1) +}