diff --git a/package-lock.json b/package-lock.json index a90f6fc..8a5952d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -514,6 +514,11 @@ "has-flag": "^3.0.0" } }, + "toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", diff --git a/package.json b/package.json index 0b42869..fe71c95 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "axios": "^0.19.1", "dotenv": "^8.2.0", "fs-extra": "^8.1.0", + "toml": "^3.0.0", "yargs": "^15.1.0" } } diff --git a/src/index.ts b/src/index.ts index 387b525..11a621a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { inspect } from 'util' import yargs from 'yargs' import { DistributionStructure } from './model/struct/model/distribution.struct' import { ServerStructure } from './model/struct/model/server.struct' -import { ResolverRegistry } from './resolver/ResolverRegistry' +import { VersionSegmentedRegistry } from './util/VersionSegmentedRegistry' dotenv.config() @@ -217,7 +217,7 @@ const testCommand: yargs.CommandModule = { handler: async (argv) => { console.debug(`Invoked test with mcVer ${argv.mcVer} forgeVer ${argv.forgeVer}`) console.log(process.cwd()) - const resolver = ResolverRegistry.getForgeResolver(argv.mcVer as string, + const resolver = VersionSegmentedRegistry.getForgeResolver(argv.mcVer as string, argv.forgeVer as string, getRoot(), '', getBaseURL()) if (resolver != null) { const mdl = await resolver.getModule() diff --git a/src/model/forge/modstoml.ts b/src/model/forge/modstoml.ts new file mode 100644 index 0000000..4601726 --- /dev/null +++ b/src/model/forge/modstoml.ts @@ -0,0 +1,29 @@ +// https://github.com/MinecraftForge/MinecraftForge/blob/1.15.x/mdk/src/main/resources/META-INF/mods.toml + +export interface ModsToml { + + modLoader: string, + loaderVersion: string + issueTrackerURL?: string + + mods: Array<{ + modId: string + version: string + displayName: string + updateJSONURL?: string + displayURL?: string + logoFile?: string + credits?: string + authors?: string + description: string + }> + + dependencies?: {[modId: string]: { + modId: string, + mandatory: boolean, + versionRange: string, + ordering?: 'NONE' | 'BEFORE' | 'AFTER' + side: 'BOTH' | 'CLIENT' | 'SERVER' + }} + +} diff --git a/src/model/struct/model/module/forgemod.struct.ts b/src/model/struct/model/module/forgemod.struct.ts index 2383e7c..ced5fb1 100644 --- a/src/model/struct/model/module/forgemod.struct.ts +++ b/src/model/struct/model/module/forgemod.struct.ts @@ -1,16 +1,11 @@ -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 { VersionSegmented } from '../../../../util/VersionSegmented' import { Type } from '../../../spec/type' import { ModuleStructure } from './module.struct' -export class ForgeModStructure extends ModuleStructure { - - private forgeModMetadata: {[property: string]: McModInfo | undefined} = {} +export abstract class BaseForgeModStructure extends ModuleStructure implements VersionSegmented { constructor( absoluteRoot: string, @@ -20,13 +15,8 @@ export class ForgeModStructure extends ModuleStructure { super(absoluteRoot, relativeRoot, 'forgemods', baseUrl, Type.ForgeMod) } - protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise { - 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 capitalize((this.getForgeModMetadata(buf, name)).name) - } + public abstract isForVersion(version: string): boolean + protected async getModuleUrl(name: string, path: string, stats: Stats): Promise { return resolve(this.baseUrl, join(this.relativeRoot, name)) } @@ -34,75 +24,4 @@ export class ForgeModStructure extends ModuleStructure { 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 - } - } - - let createDefault = false - - if (raw) { - // Assuming the main mod will be the first entry in this file. - try { - 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] - } - } catch (err) { - console.error(`ForgeMod ${name} contains an invalid mcmod.info file.`) - createDefault = true - } - } else { - console.error(`ForgeMod ${name} does not contain mcmod.info file.`) - createDefault = true - } - - if (createDefault) { - 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/model/module/forgemod/forgemod113.struct.ts b/src/model/struct/model/module/forgemod/forgemod113.struct.ts new file mode 100644 index 0000000..796a8f3 --- /dev/null +++ b/src/model/struct/model/module/forgemod/forgemod113.struct.ts @@ -0,0 +1,104 @@ +import AdmZip from 'adm-zip' +import { Stats } from 'fs-extra' +import toml from 'toml' +import { capitalize } from '../../../../../util/stringutils' +import { VersionUtil } from '../../../../../util/versionutil' +import { ModsToml } from '../../../../forge/modstoml' +import { BaseForgeModStructure } from '../forgemod.struct' + +export class ForgeModStructure113 extends BaseForgeModStructure { + + public static isForVersion(version: string) { + return VersionUtil.isVersionAcceptable(version, [13, 14, 15]) + } + + private forgeModMetadata: {[property: string]: ModsToml | undefined} = {} + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string + ) { + super(absoluteRoot, relativeRoot, baseUrl) + } + + public isForVersion(version: string): boolean { + return ForgeModStructure113.isForVersion(version) + } + + protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise { + const fmData = this.getForgeModMetadata(buf, name) + return this.generateMavenIdentifier(fmData.mods[0].modId, fmData.mods[0].version) + } + protected async getModuleName(name: string, path: string, stats: Stats, buf: Buffer): Promise { + return capitalize((this.getForgeModMetadata(buf, name)).mods[0].displayName) + } + + private getForgeModMetadata(buf: Buffer, name: string): ModsToml { + + 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 ModsToml + return this.forgeModMetadata[name] as ModsToml + } + + const raw = zip.readAsText('META-INF/mods.toml') + + let createDefault = false + + if (raw) { + // Assuming the main mod will be the first entry in this file. + try { + this.forgeModMetadata[name] = toml.parse(raw) as ModsToml + } catch (err) { + console.error(`ForgeMod ${name} contains an invalid mods.toml file.`) + createDefault = true + } + } else { + console.error(`ForgeMod ${name} does not contain mods.toml file.`) + createDefault = true + } + + if (createDefault) { + this.forgeModMetadata[name] = ({ + modLoader: 'javafml', + loaderVersion: '', + mods: [{ + modId: name.substring(0, name.lastIndexOf('.')).toLowerCase(), + version: '0.0.0', + displayName: name, + description: '' + }] + }) + } + } + + return this.forgeModMetadata[name] as ModsToml + + } + +} diff --git a/src/model/struct/model/module/forgemod/forgemod17.struct.ts b/src/model/struct/model/module/forgemod/forgemod17.struct.ts new file mode 100644 index 0000000..f678195 --- /dev/null +++ b/src/model/struct/model/module/forgemod/forgemod17.struct.ts @@ -0,0 +1,108 @@ +import AdmZip from 'adm-zip' +import { Stats } from 'fs-extra' +import { capitalize } from '../../../../../util/stringutils' +import { VersionUtil } from '../../../../../util/versionutil' +import { McModInfo } from '../../../../forge/mcmodinfo' +import { McModInfoList } from '../../../../forge/mcmodinfolist' +import { BaseForgeModStructure } from '../forgemod.struct' + +export class ForgeModStructure17 extends BaseForgeModStructure { + + public static isForVersion(version: string) { + return VersionUtil.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12]) + } + + private forgeModMetadata: {[property: string]: McModInfo | undefined} = {} + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string + ) { + super(absoluteRoot, relativeRoot, baseUrl) + } + + public isForVersion(version: string): boolean { + return ForgeModStructure17.isForVersion(version) + } + + protected async getModuleId(name: string, path: string, stats: Stats, buf: Buffer): Promise { + 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 capitalize((this.getForgeModMetadata(buf, name)).name) + } + + 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 + } + } + + let createDefault = false + + if (raw) { + // Assuming the main mod will be the first entry in this file. + try { + 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] + } + } catch (err) { + console.error(`ForgeMod ${name} contains an invalid mcmod.info file.`) + createDefault = true + } + } else { + console.error(`ForgeMod ${name} does not contain mcmod.info file.`) + createDefault = true + } + + if (createDefault) { + 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/model/server.struct.ts b/src/model/struct/model/server.struct.ts index b767f3f..e4a881c 100644 --- a/src/model/struct/model/server.struct.ts +++ b/src/model/struct/model/server.struct.ts @@ -1,12 +1,11 @@ import { lstat, mkdirs, pathExists, readdir, readFile, writeFile } from 'fs-extra' import { dirname, join, resolve as resolvePath } from 'path' import { resolve as resolveUrl } from 'url' -import { ResolverRegistry } from '../../../resolver/ResolverRegistry' +import { VersionSegmentedRegistry } from '../../../util/VersionSegmentedRegistry' import { ServerMeta } from '../../nebula/servermeta' import { Server } from '../../spec/server' import { BaseModelStructure } from './basemodel.struct' import { MiscFileStructure } from './module/file.struct' -import { ForgeModStructure } from './module/forgemod.struct' import { LiteModStructure } from './module/litemod.struct' export class ServerStructure extends BaseModelStructure { @@ -47,7 +46,12 @@ export class ServerStructure extends BaseModelStructure { await mkdirs(absoluteServerRoot) if (options.forgeVersion != null) { - const fms = new ForgeModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl) + const fms = VersionSegmentedRegistry.getForgeModStruct( + minecraftVersion, + absoluteServerRoot, + relativeServerRoot, + this.baseUrl + ) await fms.init() const serverMeta: ServerMeta = { forgeVersion: options.forgeVersion @@ -99,24 +103,25 @@ export class ServerStructure extends BaseModelStructure { // Read server meta const serverMeta: ServerMeta = JSON.parse(await readFile(resolvePath(absoluteServerRoot, 'servermeta.json'), 'utf-8')) + const minecraftVersion = match[2] - const forgeResolver = ResolverRegistry.getForgeResolver( - match[2], + const forgeResolver = VersionSegmentedRegistry.getForgeResolver( + minecraftVersion, serverMeta.forgeVersion, dirname(this.containerDirectory), '', this.baseUrl ) - if (forgeResolver == null) { - console.error(`No forge resolver found for Minecraft ${match[2]}, aborting.`) - throw new Error(`No forge resolver found for Minecraft ${match[2]}!`) - } - // Resolve forge const forgeItselfModule = await forgeResolver.getModule() - const forgeModStruct = new ForgeModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl) + const forgeModStruct = VersionSegmentedRegistry.getForgeModStruct( + minecraftVersion, + absoluteServerRoot, + relativeServerRoot, + this.baseUrl + ) const forgeModModules = await forgeModStruct.getSpecModel() const liteModStruct = new LiteModStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl) diff --git a/src/resolver/ResolverRegistry.ts b/src/resolver/ResolverRegistry.ts deleted file mode 100644 index 1550d40..0000000 --- a/src/resolver/ResolverRegistry.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Forge113Adapter } from './forge/adapter/forge113.resolver' -import { Forge17Adapter } from './forge/adapter/forge17.resolver' -import { ForgeResolver } from './forge/forge.resolver' - -export class ResolverRegistry { - - public static readonly FORGE_ADAPTER_IMPL = [ - Forge17Adapter, - Forge113Adapter - ] - - public static getForgeResolver( - minecraftVersion: string, - forgeVersion: string, - absoluteRoot: string, - relativeRoot: string, - baseURL: string): ForgeResolver | undefined { - for (const impl of ResolverRegistry.FORGE_ADAPTER_IMPL) { - if (impl.isForVersion(minecraftVersion)) { - return new impl(absoluteRoot, relativeRoot, baseURL, minecraftVersion, forgeVersion) - } - } - } - -} diff --git a/src/resolver/baseresolver.ts b/src/resolver/baseresolver.ts index bb5af4d..996381d 100644 --- a/src/resolver/baseresolver.ts +++ b/src/resolver/baseresolver.ts @@ -1,16 +1,8 @@ import { Module } from '../model/spec/module' -import { VersionUtil } from '../util/versionutil' +import { VersionSegmented } from '../util/VersionSegmented' import { Resolver } from './resolver' -export abstract class BaseResolver implements Resolver { - - protected static isVersionAcceptable(version: string, acceptable: number[]): boolean { - const versionComponents = VersionUtil.getMinecraftVersionComponents(version) - if (versionComponents != null && versionComponents.major === 1) { - return acceptable.find((element) => versionComponents.minor === element) != null - } - return false - } +export abstract class BaseResolver implements Resolver, VersionSegmented { constructor( protected absoluteRoot: string, diff --git a/src/resolver/forge/adapter/forge113.resolver.ts b/src/resolver/forge/adapter/forge113.resolver.ts index 9cf3336..2a574ca 100644 --- a/src/resolver/forge/adapter/forge113.resolver.ts +++ b/src/resolver/forge/adapter/forge113.resolver.ts @@ -8,12 +8,13 @@ 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 { VersionUtil } from '../../../util/versionutil' import { ForgeResolver } from '../forge.resolver' export class Forge113Adapter extends ForgeResolver { public static isForVersion(version: string) { - return Forge113Adapter.isVersionAcceptable(version, [13, 14, 15]) + return VersionUtil.isVersionAcceptable(version, [13, 14, 15]) } constructor( diff --git a/src/resolver/forge/adapter/forge17.resolver.ts b/src/resolver/forge/adapter/forge17.resolver.ts index 881fa45..8813fc3 100644 --- a/src/resolver/forge/adapter/forge17.resolver.ts +++ b/src/resolver/forge/adapter/forge17.resolver.ts @@ -8,12 +8,13 @@ import { Type } from '../../../model/spec/type' import { ForgeRepoStructure } from '../../../model/struct/repo/forgerepo.struct' import { MavenUtil } from '../../../util/maven' import { PackXZExtractWrapper } from '../../../util/PackXZExtractWrapper' +import { VersionUtil } from '../../../util/versionutil' import { ForgeResolver } from '../forge.resolver' export class Forge17Adapter extends ForgeResolver { public static isForVersion(version: string) { - return Forge17Adapter.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12]) + return VersionUtil.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12]) } constructor( diff --git a/src/util/VersionSegmented.ts b/src/util/VersionSegmented.ts new file mode 100644 index 0000000..2fd3a0d --- /dev/null +++ b/src/util/VersionSegmented.ts @@ -0,0 +1,5 @@ +export interface VersionSegmented { + + isForVersion(version: string): boolean + +} diff --git a/src/util/VersionSegmentedRegistry.ts b/src/util/VersionSegmentedRegistry.ts new file mode 100644 index 0000000..baf18a0 --- /dev/null +++ b/src/util/VersionSegmentedRegistry.ts @@ -0,0 +1,46 @@ +import { ForgeModStructure113 } from '../model/struct/model/module/forgemod/forgemod113.struct' +import { ForgeModStructure17 } from '../model/struct/model/module/forgemod/forgemod17.struct' +import { Forge113Adapter } from '../resolver/forge/adapter/forge113.resolver' +import { Forge17Adapter } from '../resolver/forge/adapter/forge17.resolver' + +export class VersionSegmentedRegistry { + + public static readonly FORGE_ADAPTER_IMPL = [ + Forge17Adapter, + Forge113Adapter + ] + + public static readonly FORGEMOD_STRUCT_IML = [ + ForgeModStructure17, + ForgeModStructure113 + ] + + public static getForgeResolver( + minecraftVersion: string, + forgeVersion: string, + absoluteRoot: string, + relativeRoot: string, + baseURL: string) { + for (const impl of VersionSegmentedRegistry.FORGE_ADAPTER_IMPL) { + if (impl.isForVersion(minecraftVersion)) { + return new impl(absoluteRoot, relativeRoot, baseURL, minecraftVersion, forgeVersion) + } + } + throw new Error(`No forge resolver found for Minecraft ${minecraftVersion}!`) + } + + public static getForgeModStruct( + minecraftVersion: string, + absoluteRoot: string, + relativeRoot: string, + baseUrl: string + ) { + for (const impl of VersionSegmentedRegistry.FORGEMOD_STRUCT_IML) { + if (impl.isForVersion(minecraftVersion)) { + return new impl(absoluteRoot, relativeRoot, baseUrl) + } + } + throw new Error(`No forge mod structure found for Minecraft ${minecraftVersion}!`) + } + +} diff --git a/src/util/versionutil.ts b/src/util/versionutil.ts index 4cdcf5a..b2b0353 100644 --- a/src/util/versionutil.ts +++ b/src/util/versionutil.ts @@ -20,4 +20,12 @@ export class VersionUtil { throw new Error(`${version} is not a valid minecraft version!`) } + public static isVersionAcceptable(version: string, acceptable: number[]): boolean { + const versionComponents = VersionUtil.getMinecraftVersionComponents(version) + if (versionComponents != null && versionComponents.major === 1) { + return acceptable.find((element) => versionComponents.minor === element) != null + } + return false + } + }