feat: support Fabric (#66)
* Initial scaffolding for Fabric. * refactor: extract common ModStructure * feat: add FabricModStructure * refactor: add name field to VersionRepoStructure * feat: FabricResolver * feat: support Fabric * docs: update README * Small changes. * Add additional note. * Upgrade helios-distribution-types. --------- Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>
This commit is contained in:
@@ -113,6 +113,13 @@ Options:
|
||||
* OPTIONAL (default: null)
|
||||
* If not provided forge will not be enabled.
|
||||
* You can provide either `latest` or `recommended` to use the latest/recommended version of forge.
|
||||
* `--fabric <string>` Specify fabric loader version
|
||||
* OPTIONAL (default: null)
|
||||
* If not provided fabric will not be enabled.
|
||||
* You can provide either `latest` or `recommended` to use the latest/recommended version of fabric.
|
||||
|
||||
> [!NOTE]
|
||||
> Forge and fabric cannot be used together on the same server. This command will fail if both are provided.
|
||||
|
||||
>
|
||||
> Example Usage
|
||||
@@ -227,6 +234,7 @@ Ex.
|
||||
* `files` All modules of type `File`.
|
||||
* `libraries` All modules of type `Library`
|
||||
* `forgemods` All modules of type `ForgeMod`.
|
||||
* `fabricmods` All modules of type `FabricMod`.
|
||||
* This is a directory of toggleable modules. See the note below.
|
||||
* `TestServer-1.12.2.png` Server icon file.
|
||||
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"got": "^13.0.0",
|
||||
"helios-distribution-types": "^1.2.0",
|
||||
"helios-distribution-types": "^1.3.0",
|
||||
"luxon": "^3.4.3",
|
||||
"minimatch": "^9.0.3",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
@@ -1505,9 +1505,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/helios-distribution-types": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/helios-distribution-types/-/helios-distribution-types-1.2.0.tgz",
|
||||
"integrity": "sha512-C8mRJGK0zAc7rRnA06Sj0LYwVqhY445UYNTmXU876AmfBirRR2F+A3LsD3osdgTxRMzrgkxBXvYZ0QbYW6j+6Q=="
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/helios-distribution-types/-/helios-distribution-types-1.3.0.tgz",
|
||||
"integrity": "sha512-MP66JRHvmuE9yDoZoKeFDh3stsHger0w/cRcJAlV7UYw5ztR3m/uLbWdbfFV68B1Yc0+hDIiuFsuJT/Ve9xuiw=="
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.1.1",
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"got": "^13.0.0",
|
||||
"helios-distribution-types": "^1.2.0",
|
||||
"helios-distribution-types": "^1.3.0",
|
||||
"luxon": "^3.4.3",
|
||||
"minimatch": "^9.0.3",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
|
||||
25
src/index.ts
25
src/index.ts
@@ -174,16 +174,22 @@ const generateServerCommand: CommandModule = {
|
||||
})
|
||||
.option('forge', {
|
||||
describe: 'Forge version.',
|
||||
type: 'string',
|
||||
default: null
|
||||
type: 'string'
|
||||
})
|
||||
.option('fabric', {
|
||||
describe: 'Fabric version.',
|
||||
type: 'string'
|
||||
})
|
||||
.conflicts('forge', 'fabric')
|
||||
},
|
||||
handler: async (argv) => {
|
||||
argv.root = getRoot()
|
||||
|
||||
logger.debug(`Root set to ${argv.root}`)
|
||||
logger.debug(`Generating server ${argv.id} for Minecraft ${argv.version}.`,
|
||||
`\n\t└ Forge version: ${argv.forge}`)
|
||||
`\n\t└ Forge version: ${argv.forge}`,
|
||||
`\n\t└ Fabric version: ${argv.fabric}`
|
||||
)
|
||||
|
||||
const minecraftVersion = new MinecraftVersion(argv.version as string)
|
||||
|
||||
@@ -196,12 +202,22 @@ const generateServerCommand: CommandModule = {
|
||||
}
|
||||
}
|
||||
|
||||
if(argv.fabric != null) {
|
||||
if (VersionUtil.isPromotionVersion(argv.fabric as string)) {
|
||||
logger.debug(`Resolving ${argv.fabric as string} Fabric Version..`)
|
||||
const version = await VersionUtil.getPromotedFabricVersion(argv.fabric as string)
|
||||
logger.debug(`Fabric version set to ${version}`)
|
||||
argv.fabric = version
|
||||
}
|
||||
}
|
||||
|
||||
const serverStruct = new ServerStructure(argv.root as string, getBaseURL(), false, false)
|
||||
await serverStruct.createServer(
|
||||
argv.id as string,
|
||||
minecraftVersion,
|
||||
{
|
||||
forgeVersion: argv.forge as string
|
||||
forgeVersion: argv.forge as string,
|
||||
fabricVersion: argv.fabric as string
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -234,6 +250,7 @@ const generateServerCurseForgeCommand: CommandModule = {
|
||||
const minecraftVersion = new MinecraftVersion(modpackManifest.minecraft.version)
|
||||
|
||||
// Extract forge version
|
||||
// TODO Support fabric
|
||||
const forgeModLoader = modpackManifest.minecraft.modLoaders.find(({ id }) => id.toLowerCase().startsWith('forge-'))
|
||||
const forgeVersion = forgeModLoader != null ? forgeModLoader.id.substring('forge-'.length) : undefined
|
||||
logger.debug(`Forge version set to ${forgeVersion}`)
|
||||
|
||||
49
src/model/fabric/FabricMeta.ts
Normal file
49
src/model/fabric/FabricMeta.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export interface FabricVersionMeta {
|
||||
version: string
|
||||
stable: boolean
|
||||
}
|
||||
|
||||
export interface FabricLoaderMeta extends FabricVersionMeta {
|
||||
separator: string
|
||||
build: number
|
||||
maven: string
|
||||
}
|
||||
|
||||
export interface FabricInstallerMeta extends FabricVersionMeta {
|
||||
url: string
|
||||
maven: string
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
action: string
|
||||
os?: {
|
||||
name: string
|
||||
version?: string
|
||||
}
|
||||
features?: {
|
||||
[key: string]: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface RuleBasedArgument {
|
||||
rules: Rule[]
|
||||
value: string | string[]
|
||||
}
|
||||
|
||||
// This is really a mojang format, but it's currently only used here for Fabric.
|
||||
export interface FabricProfileJson {
|
||||
id: string
|
||||
inheritsFrom: string
|
||||
releaseTime: string
|
||||
time: string
|
||||
type: string
|
||||
mainClass: string
|
||||
arguments: {
|
||||
game: (string | RuleBasedArgument)[]
|
||||
jvm: (string | RuleBasedArgument)[]
|
||||
}
|
||||
libraries: {
|
||||
name: string // Maven identifier
|
||||
url: string
|
||||
}[]
|
||||
}
|
||||
13
src/model/fabric/FabricModJson.ts
Normal file
13
src/model/fabric/FabricModJson.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// https://fabricmc.net/wiki/documentation:fabric_mod_json_spec
|
||||
// https://github.com/FabricMC/fabric-loader/blob/master/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java
|
||||
|
||||
type FabricEntryPoint = string | { value: string }
|
||||
|
||||
export interface FabricModJson {
|
||||
|
||||
id: string
|
||||
version: string
|
||||
name?: string
|
||||
entrypoints?: { [key: string]: FabricEntryPoint[] }
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export interface UntrackedFilesOption {
|
||||
export interface ServerMetaOptions {
|
||||
version?: string
|
||||
forgeVersion?: string
|
||||
fabricVersion?: string
|
||||
}
|
||||
|
||||
export function getDefaultServerMeta(id: string, version: string, options?: ServerMetaOptions): ServerMeta {
|
||||
@@ -43,6 +44,13 @@ export function getDefaultServerMeta(id: string, version: string, options?: Serv
|
||||
}
|
||||
}
|
||||
|
||||
if(options?.fabricVersion) {
|
||||
servMeta.meta.description = `${servMeta.meta.description} (Fabric v${options.fabricVersion})`
|
||||
servMeta.fabric = {
|
||||
version: options.fabricVersion
|
||||
}
|
||||
}
|
||||
|
||||
// Add empty untracked files.
|
||||
servMeta.untrackedFiles = []
|
||||
|
||||
@@ -77,6 +85,17 @@ export interface ServerMeta {
|
||||
version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties related to Fabric.
|
||||
*/
|
||||
fabric?: {
|
||||
/**
|
||||
* The fabric loader version. This does NOT include the minecraft version.
|
||||
* Ex. 0.14.18
|
||||
*/
|
||||
version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of option objects defining patterns for untracked files.
|
||||
*/
|
||||
|
||||
@@ -100,9 +100,9 @@ export class CurseForgeParser {
|
||||
await zip.close()
|
||||
}
|
||||
|
||||
if(createServerResult.forgeModContainer) {
|
||||
const requiredPath = resolve(createServerResult.forgeModContainer, ToggleableNamespace.REQUIRED)
|
||||
const optionalPath = resolve(createServerResult.forgeModContainer, ToggleableNamespace.OPTIONAL_ON)
|
||||
if(createServerResult.modContainer) {
|
||||
const requiredPath = resolve(createServerResult.modContainer, ToggleableNamespace.REQUIRED)
|
||||
const optionalPath = resolve(createServerResult.modContainer, ToggleableNamespace.OPTIONAL_ON)
|
||||
|
||||
const disallowedFiles: { name: string, fileName: string, url: string }[] = []
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Module } from 'helios-distribution-types'
|
||||
import { Artifact, Module } from 'helios-distribution-types'
|
||||
import { VersionSegmented } from '../util/VersionSegmented.js'
|
||||
import { Resolver } from './Resolver.js'
|
||||
import { MinecraftVersion } from '../util/MinecraftVersion.js'
|
||||
import { Stats } from 'fs'
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
export abstract class BaseResolver implements Resolver, VersionSegmented {
|
||||
|
||||
@@ -14,4 +16,12 @@ export abstract class BaseResolver implements Resolver, VersionSegmented {
|
||||
public abstract getModule(): Promise<Module>
|
||||
public abstract isForVersion(version: MinecraftVersion, libraryVersion: string): boolean
|
||||
|
||||
protected generateArtifact(buf: Buffer, stats: Stats, url: string): Artifact {
|
||||
return {
|
||||
size: stats.size,
|
||||
MD5: createHash('md5').update(buf).digest('hex'),
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
123
src/resolver/fabric/Fabric.resolver.ts
Normal file
123
src/resolver/fabric/Fabric.resolver.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { mkdirs, pathExists } from 'fs-extra/esm'
|
||||
import { lstat, readFile, writeFile } from 'fs/promises'
|
||||
import { Module, Type } from 'helios-distribution-types'
|
||||
import { dirname } from 'path'
|
||||
import { FabricProfileJson } from '../../model/fabric/FabricMeta.js'
|
||||
import { RepoStructure } from '../../structure/repo/Repo.struct.js'
|
||||
import { LoggerUtil } from '../../util/LoggerUtil.js'
|
||||
import { MavenUtil } from '../../util/MavenUtil.js'
|
||||
import { MinecraftVersion } from '../../util/MinecraftVersion.js'
|
||||
import { VersionUtil } from '../../util/VersionUtil.js'
|
||||
import { BaseResolver } from '../BaseResolver.js'
|
||||
|
||||
export class FabricResolver extends BaseResolver {
|
||||
|
||||
private static readonly log = LoggerUtil.getLogger('FabricResolver')
|
||||
|
||||
protected repoStructure: RepoStructure
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public static isForVersion(_version: MinecraftVersion, _libraryVersion: string): boolean {
|
||||
// --fabric.addMods support was added in https://github.com/FabricMC/fabric-loader/commit/ce8405c22166ef850ae73c09ab513c17d121df5a
|
||||
return VersionUtil.versionGte(_libraryVersion, '0.12.3')
|
||||
}
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
protected loaderVersion: string,
|
||||
protected minecraftVersion: MinecraftVersion
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, baseUrl)
|
||||
this.repoStructure = new RepoStructure(absoluteRoot, relativeRoot, 'fabric')
|
||||
}
|
||||
|
||||
public async getModule(): Promise<Module> {
|
||||
return this.getFabricModule()
|
||||
}
|
||||
|
||||
public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
|
||||
return FabricResolver.isForVersion(version, libraryVersion)
|
||||
}
|
||||
|
||||
public async getFabricModule(): Promise<Module> {
|
||||
|
||||
const versionRepo = this.repoStructure.getVersionRepoStruct()
|
||||
const versionManifest = versionRepo.getVersionManifest(this.minecraftVersion, this.loaderVersion)
|
||||
|
||||
FabricResolver.log.debug(`Checking for fabric profile json at ${versionManifest}..`)
|
||||
if(!await pathExists(versionManifest)) {
|
||||
FabricResolver.log.debug('Fabric profile not found locally, initializing download..')
|
||||
await mkdirs(dirname(versionManifest))
|
||||
const manifest = await VersionUtil.getFabricProfileJson(this.minecraftVersion.toString(), this.loaderVersion)
|
||||
await writeFile(versionManifest, JSON.stringify(manifest))
|
||||
}
|
||||
const profileJsonBuf = await readFile(versionManifest)
|
||||
const profileJson = JSON.parse(profileJsonBuf.toString()) as FabricProfileJson
|
||||
|
||||
const libRepo = this.repoStructure.getLibRepoStruct()
|
||||
|
||||
const modules: Module[] = [{
|
||||
id: versionRepo.getFileName(this.minecraftVersion, this.loaderVersion),
|
||||
name: 'Fabric (version.json)',
|
||||
type: Type.VersionManifest,
|
||||
artifact: this.generateArtifact(
|
||||
profileJsonBuf,
|
||||
await lstat(versionManifest),
|
||||
versionRepo.getVersionManifestURL(this.baseUrl, this.minecraftVersion, this.loaderVersion)
|
||||
)
|
||||
}]
|
||||
for (const lib of profileJson.libraries) {
|
||||
FabricResolver.log.debug(`Processing ${lib.name}..`)
|
||||
|
||||
const localPath = libRepo.getArtifactById(lib.name)
|
||||
|
||||
if (!await libRepo.artifactExists(localPath)) {
|
||||
FabricResolver.log.debug('Not found locally, downloading..')
|
||||
await libRepo.downloadArtifactById(lib.url, lib.name)
|
||||
} else {
|
||||
FabricResolver.log.debug('Using local copy.')
|
||||
}
|
||||
|
||||
const libBuf = await readFile(localPath)
|
||||
const stats = await lstat(localPath)
|
||||
|
||||
const mavenComponents = MavenUtil.getMavenComponents(lib.name)
|
||||
|
||||
modules.push({
|
||||
id: lib.name,
|
||||
name: `Fabric (${mavenComponents.artifact})`,
|
||||
type: Type.Library,
|
||||
artifact: this.generateArtifact(
|
||||
libBuf,
|
||||
stats,
|
||||
libRepo.getArtifactUrlByComponents(
|
||||
this.baseUrl,
|
||||
mavenComponents.group, mavenComponents.artifact,
|
||||
mavenComponents.version, mavenComponents.classifier
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO Rework this
|
||||
let index = -1
|
||||
for(let i=0; i<modules.length; i++) {
|
||||
if(modules[i].id.startsWith('net.fabricmc:fabric-loader')) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const fabricModule = modules[index]
|
||||
fabricModule.type = Type.Fabric
|
||||
modules.splice(index)
|
||||
|
||||
fabricModule.subModules = modules
|
||||
|
||||
return fabricModule
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
import StreamZip from 'node-stream-zip'
|
||||
import { createHash } from 'crypto'
|
||||
import { Stats } from 'fs'
|
||||
import { Artifact } from 'helios-distribution-types'
|
||||
import { RepoStructure } from '../../structure/repo/Repo.struct.js'
|
||||
import { BaseResolver } from '../BaseResolver.js'
|
||||
import { MinecraftVersion } from '../../util/MinecraftVersion.js'
|
||||
@@ -26,7 +23,7 @@ export abstract class ForgeResolver extends BaseResolver {
|
||||
protected invalidateCache: boolean
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, baseUrl)
|
||||
this.repoStructure = new RepoStructure(absoluteRoot, relativeRoot)
|
||||
this.repoStructure = new RepoStructure(absoluteRoot, relativeRoot, 'forge')
|
||||
this.artifactVersion = this.inferArtifactVersion()
|
||||
this.checkSecurity()
|
||||
}
|
||||
@@ -133,14 +130,6 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
protected async getVersionManifestFromJar(jarPath: string): Promise<Buffer>{
|
||||
return new Promise((resolve, reject) => {
|
||||
const zip = new StreamZip({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface FileStructure {
|
||||
|
||||
init(): void
|
||||
init(): Promise<void>
|
||||
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ export class RepoStructure extends BaseFileStructure {
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string
|
||||
relativeRoot: string,
|
||||
name: string
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, 'repo')
|
||||
this.libRepoStruct = new LibRepoStructure(this.containerDirectory, this.relativeRoot)
|
||||
this.versionRepoStruct = new VersionRepoStructure(this.containerDirectory, this.relativeRoot)
|
||||
this.versionRepoStruct = new VersionRepoStructure(this.containerDirectory, this.relativeRoot, name)
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
|
||||
@@ -5,28 +5,32 @@ import { MinecraftVersion } from '../../util/MinecraftVersion.js'
|
||||
|
||||
export class VersionRepoStructure extends BaseFileStructure {
|
||||
|
||||
private name: string
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string
|
||||
relativeRoot: string,
|
||||
name: string
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, 'versions')
|
||||
this.name = name
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
return 'VersionRepoStructure'
|
||||
}
|
||||
|
||||
public getFileName(minecraftVersion: MinecraftVersion, forgeVersion: string): string {
|
||||
return `${minecraftVersion}-forge-${forgeVersion}`
|
||||
public getFileName(minecraftVersion: MinecraftVersion, loaderVersion: string): string {
|
||||
return `${minecraftVersion}-${this.name}-${loaderVersion}`
|
||||
}
|
||||
|
||||
public getVersionManifest(minecraftVersion: MinecraftVersion, forgeVersion: string): string {
|
||||
const fileName = this.getFileName(minecraftVersion, forgeVersion)
|
||||
public getVersionManifest(minecraftVersion: MinecraftVersion, loaderVersion: string): string {
|
||||
const fileName = this.getFileName(minecraftVersion, loaderVersion)
|
||||
return join(this.containerDirectory, fileName, `${fileName}.json`)
|
||||
}
|
||||
|
||||
public getVersionManifestURL(url: string, minecraftVersion: MinecraftVersion, forgeVersion: string): string {
|
||||
const fileName = this.getFileName(minecraftVersion, forgeVersion)
|
||||
public getVersionManifestURL(url: string, minecraftVersion: MinecraftVersion, loaderVersion: string): string {
|
||||
const fileName = this.getFileName(minecraftVersion, loaderVersion)
|
||||
return new URL(join(this.relativeRoot, fileName, `${fileName}.json`), url).toString()
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,16 @@ import { URL } from 'url'
|
||||
import { VersionSegmentedRegistry } from '../../util/VersionSegmentedRegistry.js'
|
||||
import { ServerMeta, getDefaultServerMeta, ServerMetaOptions, UntrackedFilesOption } from '../../model/nebula/ServerMeta.js'
|
||||
import { BaseModelStructure } from './BaseModel.struct.js'
|
||||
import { FabricModStructure } from './module/FabricMod.struct.js'
|
||||
import { MiscFileStructure } from './module/File.struct.js'
|
||||
import { LibraryStructure } from './module/Library.struct.js'
|
||||
import { MinecraftVersion } from '../../util/MinecraftVersion.js'
|
||||
import { addSchemaToObject, SchemaTypes } from '../../util/SchemaUtil.js'
|
||||
import { isValidUrl } from '../../util/StringUtils.js'
|
||||
import { FabricResolver } from '../../resolver/fabric/Fabric.resolver.js'
|
||||
|
||||
export interface CreateServerResult {
|
||||
forgeModContainer?: string
|
||||
modContainer?: string
|
||||
libraryContainer: string
|
||||
miscFileContainer: string
|
||||
}
|
||||
@@ -53,6 +55,7 @@ export class ServerStructure extends BaseModelStructure<Server> {
|
||||
options: {
|
||||
version?: string
|
||||
forgeVersion?: string
|
||||
fabricVersion?: string
|
||||
}
|
||||
): Promise<CreateServerResult | null> {
|
||||
const effectiveId = ServerStructure.getEffectiveId(id, minecraftVersion)
|
||||
@@ -69,7 +72,7 @@ export class ServerStructure extends BaseModelStructure<Server> {
|
||||
const serverMetaOpts: ServerMetaOptions = {
|
||||
version: options.version
|
||||
}
|
||||
let forgeModContainer: string | undefined = undefined
|
||||
let modContainer: string | undefined = undefined
|
||||
|
||||
if (options.forgeVersion != null) {
|
||||
const fms = VersionSegmentedRegistry.getForgeModStruct(
|
||||
@@ -81,10 +84,23 @@ export class ServerStructure extends BaseModelStructure<Server> {
|
||||
[]
|
||||
)
|
||||
await fms.init()
|
||||
forgeModContainer = fms.getContainerDirectory()
|
||||
modContainer = fms.getContainerDirectory()
|
||||
serverMetaOpts.forgeVersion = options.forgeVersion
|
||||
}
|
||||
|
||||
if (options.fabricVersion != null) {
|
||||
const fms = new FabricModStructure(
|
||||
absoluteServerRoot,
|
||||
relativeServerRoot,
|
||||
this.baseUrl,
|
||||
minecraftVersion,
|
||||
[]
|
||||
)
|
||||
await fms.init()
|
||||
modContainer = fms.getContainerDirectory()
|
||||
serverMetaOpts.fabricVersion = options.fabricVersion
|
||||
}
|
||||
|
||||
const serverMeta: ServerMeta = addSchemaToObject(
|
||||
getDefaultServerMeta(id, minecraftVersion.toString(), serverMetaOpts),
|
||||
SchemaTypes.ServerMetaSchema,
|
||||
@@ -99,7 +115,7 @@ export class ServerStructure extends BaseModelStructure<Server> {
|
||||
await mfs.init()
|
||||
|
||||
return {
|
||||
forgeModContainer,
|
||||
modContainer,
|
||||
libraryContainer: libS.getContainerDirectory(),
|
||||
miscFileContainer: mfs.getContainerDirectory()
|
||||
}
|
||||
@@ -184,6 +200,27 @@ export class ServerStructure extends BaseModelStructure<Server> {
|
||||
modules.push(...forgeModModules)
|
||||
}
|
||||
|
||||
if(serverMeta.fabric) {
|
||||
const fabricResolver = new FabricResolver(dirname(this.containerDirectory), '', this.baseUrl, serverMeta.fabric.version, minecraftVersion)
|
||||
if (!fabricResolver.isForVersion(minecraftVersion, serverMeta.fabric.version)) {
|
||||
throw new Error(`Fabric resolver does not support Fabric ${serverMeta.fabric.version}!`)
|
||||
}
|
||||
|
||||
const fabricModule = await fabricResolver.getModule()
|
||||
modules.push(fabricModule)
|
||||
|
||||
const fabricModStruct = new FabricModStructure(
|
||||
absoluteServerRoot,
|
||||
relativeServerRoot,
|
||||
this.baseUrl,
|
||||
minecraftVersion,
|
||||
untrackedFiles
|
||||
)
|
||||
|
||||
const fabricModModules = await fabricModStruct.getSpecModel()
|
||||
modules.push(...fabricModModules)
|
||||
}
|
||||
|
||||
const libraryStruct = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, untrackedFiles)
|
||||
const libraryModules = await libraryStruct.getSpecModel()
|
||||
modules.push(...libraryModules)
|
||||
|
||||
91
src/structure/spec_model/module/FabricMod.struct.ts
Normal file
91
src/structure/spec_model/module/FabricMod.struct.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import StreamZip from 'node-stream-zip'
|
||||
import { Type } from 'helios-distribution-types'
|
||||
import { capitalize } from '../../../util/StringUtils.js'
|
||||
import { FabricModJson } from '../../../model/fabric/FabricModJson.js'
|
||||
import { MinecraftVersion } from '../../../util/MinecraftVersion.js'
|
||||
import { BaseModStructure } from './Mod.struct.js'
|
||||
import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js'
|
||||
|
||||
export class FabricModStructure extends BaseModStructure<FabricModJson> {
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
minecraftVersion: MinecraftVersion,
|
||||
untrackedFiles: UntrackedFilesOption[]
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, 'fabricmods', baseUrl, minecraftVersion, Type.FabricMod, untrackedFiles)
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
return 'FabricModStructure'
|
||||
}
|
||||
|
||||
protected async getModuleId(name: string, path: string): Promise<string> {
|
||||
const fmData = await this.getModMetadata(name, path)
|
||||
let group
|
||||
if (fmData.entrypoints != null) {
|
||||
for (const t of ['main', 'client', 'server']) {
|
||||
if (fmData.entrypoints[t] != null && fmData.entrypoints[t].length > 0) {
|
||||
const entrypoint = fmData.entrypoints[t][0]
|
||||
group = typeof entrypoint === 'string' ? entrypoint : entrypoint.value
|
||||
break
|
||||
}
|
||||
}
|
||||
// adapted from https://github.com/dscalzi/Claritas/blob/master/src/main/java/com/dscalzi/claritas/util/DataUtil.java
|
||||
if (group != null) {
|
||||
const packageBits = group.split('.')
|
||||
const blacklist = ['common', 'util', 'internal', 'tweaker', 'tweak', 'client', ...['forge', 'fabric', 'bukkit', 'sponge'].filter(t => t !== fmData.id)]
|
||||
// Note: Entry point is a fully qualified class name, hence why this adaptation pops immediately (drop class name).
|
||||
while (packageBits.length > 0) {
|
||||
packageBits.pop()
|
||||
const term = packageBits[packageBits.length - 1]
|
||||
if ((term !== fmData.id && !blacklist.includes(term)) || packageBits.length === 1 || (packageBits.length === 2 && term === fmData.id)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
group = packageBits.join('.')
|
||||
}
|
||||
}
|
||||
return this.generateMavenIdentifier(group || this.getDefaultGroup(), fmData.id, fmData.version)
|
||||
}
|
||||
protected async getModuleName(name: string, path: string): Promise<string> {
|
||||
const fmData = await this.getModMetadata(name, path)
|
||||
return capitalize(fmData.name || fmData.id)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected processZip(zip: StreamZip, name: string, path: string): FabricModJson {
|
||||
|
||||
let raw: Buffer | undefined
|
||||
try {
|
||||
raw = zip.entryDataSync('fabric.mod.json')
|
||||
} catch(err) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw.toString()) as FabricModJson
|
||||
this.modMetadata[name] = parsed
|
||||
} catch (err) {
|
||||
this.logger.error(`FabricMod ${name} contains an invalid fabric.mod.json file.`)
|
||||
}
|
||||
} else {
|
||||
this.logger.error(`FabricMod ${name} does not contain fabric.mod.json file.`)
|
||||
}
|
||||
|
||||
const crudeInference = this.attemptCrudeInference(name)
|
||||
|
||||
if(this.modMetadata[name] == null) {
|
||||
this.modMetadata[name] = ({
|
||||
id: crudeInference.name.toLowerCase(),
|
||||
name: crudeInference.name,
|
||||
version: crudeInference.version
|
||||
})
|
||||
}
|
||||
|
||||
return this.modMetadata[name]!
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Stats } from 'fs'
|
||||
import { Type, Module } from 'helios-distribution-types'
|
||||
import { join } from 'path'
|
||||
import { URL } from 'url'
|
||||
import { Type } from 'helios-distribution-types'
|
||||
import { VersionSegmented } from '../../../util/VersionSegmented.js'
|
||||
import { MinecraftVersion } from '../../../util/MinecraftVersion.js'
|
||||
import { ToggleableModuleStructure } from './ToggleableModule.struct.js'
|
||||
import { BaseModStructure } from './Mod.struct.js'
|
||||
import { LibraryType } from '../../../model/claritas/ClaritasLibraryType.js'
|
||||
import { ClaritasException } from './Module.struct.js'
|
||||
import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js'
|
||||
|
||||
export abstract class BaseForgeModStructure extends ToggleableModuleStructure implements VersionSegmented {
|
||||
export abstract class BaseForgeModStructure<T> extends BaseModStructure<T> implements VersionSegmented {
|
||||
|
||||
protected readonly EXAMPLE_MOD_ID = 'examplemod'
|
||||
|
||||
@@ -23,26 +20,8 @@ export abstract class BaseForgeModStructure extends ToggleableModuleStructure im
|
||||
super(absoluteRoot, relativeRoot, 'forgemods', baseUrl, minecraftVersion, Type.ForgeMod, untrackedFiles)
|
||||
}
|
||||
|
||||
public async getSpecModel(): Promise<Module[]> {
|
||||
// Sort by file name to allow control of load order.
|
||||
return (await super.getSpecModel()).sort((a, b) => {
|
||||
const aFileName = a.artifact.url.substring(a.artifact.url.lastIndexOf('/')+1)
|
||||
const bFileName = b.artifact.url.substring(b.artifact.url.lastIndexOf('/')+1)
|
||||
return aFileName.localeCompare(bFileName)
|
||||
})
|
||||
}
|
||||
|
||||
public abstract isForVersion(version: MinecraftVersion, libraryVersion: string): boolean
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected async getModuleUrl(name: string, path: string, stats: Stats): Promise<string> {
|
||||
return new URL(join(this.relativeRoot, this.getActiveNamespace(), name), this.baseUrl).toString()
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected async getModulePath(name: string, path: string, stats: Stats): Promise<string | null> {
|
||||
return null
|
||||
}
|
||||
|
||||
protected getClaritasExceptions(): ClaritasException[] {
|
||||
return [{
|
||||
exceptionName: 'optifine',
|
||||
|
||||
66
src/structure/spec_model/module/Mod.struct.ts
Normal file
66
src/structure/spec_model/module/Mod.struct.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Stats } from 'fs'
|
||||
import { Module } from 'helios-distribution-types'
|
||||
import StreamZip from 'node-stream-zip'
|
||||
import { join } from 'path'
|
||||
import { URL } from 'url'
|
||||
import { ToggleableModuleStructure } from './ToggleableModule.struct.js'
|
||||
|
||||
export abstract class BaseModStructure<T> extends ToggleableModuleStructure {
|
||||
|
||||
protected modMetadata: {[property: string]: T | undefined} = {}
|
||||
|
||||
public async getSpecModel(): Promise<Module[]> {
|
||||
// Sort by file name to allow control of load order.
|
||||
return (await super.getSpecModel()).sort((a, b) => {
|
||||
const aFileName = a.artifact.url.substring(a.artifact.url.lastIndexOf('/')+1)
|
||||
const bFileName = b.artifact.url.substring(b.artifact.url.lastIndexOf('/')+1)
|
||||
return aFileName.localeCompare(bFileName)
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected async getModuleUrl(name: string, path: string, stats: Stats): Promise<string> {
|
||||
return new URL(join(this.relativeRoot, this.getActiveNamespace(), name), this.baseUrl).toString()
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected async getModulePath(name: string, path: string, stats: Stats): Promise<string | null> {
|
||||
return null
|
||||
}
|
||||
|
||||
protected getModMetadata(name: string, path: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.modMetadata, name)) {
|
||||
|
||||
const zip = new StreamZip({
|
||||
file: path,
|
||||
storeEntries: true
|
||||
})
|
||||
|
||||
zip.on('error', err => {
|
||||
this.logger.error(`Failure while processing ${path}`)
|
||||
reject(err)
|
||||
})
|
||||
zip.on('ready', () => {
|
||||
try {
|
||||
const res = this.processZip(zip, name, path)
|
||||
zip.close()
|
||||
resolve(res)
|
||||
return
|
||||
} catch(err) {
|
||||
zip.close()
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
resolve(this.modMetadata[name]!)
|
||||
return
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
protected abstract processZip(zip: StreamZip, name: string, path: string): T
|
||||
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { BaseForgeModStructure } from '../ForgeMod.struct.js'
|
||||
import { MinecraftVersion } from '../../../../util/MinecraftVersion.js'
|
||||
import { UntrackedFilesOption } from '../../../../model/nebula/ServerMeta.js'
|
||||
|
||||
export class ForgeModStructure113 extends BaseForgeModStructure {
|
||||
export class ForgeModStructure113 extends BaseForgeModStructure<ModsToml> {
|
||||
|
||||
public static readonly IMPLEMENTATION_VERSION_REGEX = /^Implementation-Version: (.+)[\r\n]/
|
||||
|
||||
@@ -16,8 +16,6 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
|
||||
return VersionUtil.isVersionAcceptable(version, [13, 14, 15, 16, 17, 18, 19, 20])
|
||||
}
|
||||
|
||||
private forgeModMetadata: {[property: string]: ModsToml | undefined} = {}
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
@@ -37,48 +35,14 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
|
||||
}
|
||||
|
||||
protected async getModuleId(name: string, path: string): Promise<string> {
|
||||
const fmData = await this.getForgeModMetadata(name, path)
|
||||
const fmData = await this.getModMetadata(name, path)
|
||||
return this.generateMavenIdentifier(this.getClaritasGroup(path), fmData.mods[0].modId, fmData.mods[0].version)
|
||||
}
|
||||
protected async getModuleName(name: string, path: string): Promise<string> {
|
||||
return capitalize((await this.getForgeModMetadata(name, path)).mods[0].displayName)
|
||||
return capitalize((await this.getModMetadata(name, path)).mods[0].displayName)
|
||||
}
|
||||
|
||||
private getForgeModMetadata(name: string, path: string): Promise<ModsToml> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.forgeModMetadata, name)) {
|
||||
|
||||
const zip = new StreamZip({
|
||||
file: path,
|
||||
storeEntries: true
|
||||
})
|
||||
|
||||
zip.on('error', err => {
|
||||
this.logger.error(`Failure while processing ${path}`)
|
||||
reject(err)
|
||||
})
|
||||
zip.on('ready', () => {
|
||||
try {
|
||||
const res = this.processZip(zip, name, path)
|
||||
zip.close()
|
||||
resolve(res)
|
||||
return
|
||||
} catch(err) {
|
||||
zip.close()
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
resolve(this.forgeModMetadata[name]!)
|
||||
return
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
private processZip(zip: StreamZip, name: string, path: string): ModsToml {
|
||||
protected processZip(zip: StreamZip, name: string, path: string): ModsToml {
|
||||
|
||||
// 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
|
||||
@@ -96,7 +60,7 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
|
||||
const info = changelogBuf.toString().split('\n')[0].trim()
|
||||
const version = info.split(' ')[1]
|
||||
|
||||
this.forgeModMetadata[name] = ({
|
||||
this.modMetadata[name] = ({
|
||||
modLoader: 'javafml',
|
||||
loaderVersion: '',
|
||||
mods: [{
|
||||
@@ -108,7 +72,7 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
|
||||
}]
|
||||
})
|
||||
|
||||
return this.forgeModMetadata[name]!
|
||||
return this.modMetadata[name]!
|
||||
}
|
||||
|
||||
let raw: Buffer | undefined
|
||||
@@ -121,7 +85,7 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = toml.parse(raw.toString()) as ModsToml
|
||||
this.forgeModMetadata[name] = parsed
|
||||
this.modMetadata[name] = parsed
|
||||
} catch (err) {
|
||||
this.logger.error(`ForgeMod ${name} contains an invalid mods.toml file.`)
|
||||
}
|
||||
@@ -133,16 +97,16 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
|
||||
|
||||
if(cRes == null) {
|
||||
this.logger.error(`Claritas failed to yield metadata for ForgeMod ${name}!`)
|
||||
this.logger.error('Is this mod malformated or does Claritas need an update?')
|
||||
this.logger.error('Is this mod malformatted or does Claritas need an update?')
|
||||
}
|
||||
|
||||
const claritasId = cRes?.id
|
||||
|
||||
const crudeInference = this.attemptCrudeInference(name)
|
||||
|
||||
if(this.forgeModMetadata[name] != null) {
|
||||
if(this.modMetadata[name] != null) {
|
||||
|
||||
const x = this.forgeModMetadata[name]!
|
||||
const x = this.modMetadata[name]!
|
||||
for(const entry of x.mods) {
|
||||
|
||||
if(entry.modId === this.EXAMPLE_MOD_ID) {
|
||||
@@ -171,7 +135,7 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
|
||||
}
|
||||
|
||||
} else {
|
||||
this.forgeModMetadata[name] = ({
|
||||
this.modMetadata[name] = ({
|
||||
modLoader: 'javafml',
|
||||
loaderVersion: '',
|
||||
mods: [{
|
||||
@@ -183,7 +147,7 @@ export class ForgeModStructure113 extends BaseForgeModStructure {
|
||||
})
|
||||
}
|
||||
|
||||
return this.forgeModMetadata[name]!
|
||||
return this.modMetadata[name]!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,15 +8,13 @@ import { MinecraftVersion } from '../../../../util/MinecraftVersion.js'
|
||||
import { ForgeModType_1_7 } from '../../../../model/claritas/ClaritasResult.js'
|
||||
import { UntrackedFilesOption } from '../../../../model/nebula/ServerMeta.js'
|
||||
|
||||
export class ForgeModStructure17 extends BaseForgeModStructure {
|
||||
export class ForgeModStructure17 extends BaseForgeModStructure<McModInfo> {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
|
||||
return VersionUtil.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12])
|
||||
}
|
||||
|
||||
private forgeModMetadata: {[property: string]: McModInfo | undefined} = {}
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
@@ -36,45 +34,11 @@ export class ForgeModStructure17 extends BaseForgeModStructure {
|
||||
}
|
||||
|
||||
protected async getModuleId(name: string, path: string): Promise<string> {
|
||||
const fmData = await this.getForgeModMetadata(name, path)
|
||||
const fmData = await this.getModMetadata(name, path)
|
||||
return this.generateMavenIdentifier(this.getClaritasGroup(path), fmData.modid, fmData.version)
|
||||
}
|
||||
protected async getModuleName(name: string, path: string): Promise<string> {
|
||||
return capitalize((await this.getForgeModMetadata(name, path)).name)
|
||||
}
|
||||
|
||||
private getForgeModMetadata(name: string, path: string): Promise<McModInfo> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.forgeModMetadata, name)) {
|
||||
|
||||
const zip = new StreamZip({
|
||||
file: path,
|
||||
storeEntries: true
|
||||
})
|
||||
|
||||
zip.on('error', err => {
|
||||
this.logger.error(`Failure while processing ${path}`)
|
||||
reject(err)
|
||||
})
|
||||
zip.on('ready', () => {
|
||||
try {
|
||||
const res = this.processZip(zip, name, path)
|
||||
zip.close()
|
||||
resolve(res)
|
||||
return
|
||||
} catch(err) {
|
||||
zip.close()
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
resolve(this.forgeModMetadata[name]!)
|
||||
return
|
||||
}
|
||||
|
||||
})
|
||||
return capitalize((await this.getModMetadata(name, path)).name)
|
||||
}
|
||||
|
||||
private isMalformedVersion(version: string): boolean {
|
||||
@@ -82,7 +46,7 @@ export class ForgeModStructure17 extends BaseForgeModStructure {
|
||||
return version.trim().length === 0 || version.indexOf('@') > -1 || version.indexOf('$') > -1
|
||||
}
|
||||
|
||||
private processZip(zip: StreamZip, name: string, path: string): McModInfo {
|
||||
protected processZip(zip: StreamZip, name: string, path: string): McModInfo {
|
||||
// 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.
|
||||
@@ -98,13 +62,13 @@ export class ForgeModStructure17 extends BaseForgeModStructure {
|
||||
|
||||
const info = changelogBuf.toString().split('\n')[0].trim()
|
||||
const version = info.split(' ')[1]
|
||||
this.forgeModMetadata[name] = ({
|
||||
this.modMetadata[name] = ({
|
||||
modid: 'optifine',
|
||||
name: info,
|
||||
version,
|
||||
mcversion: version.substring(0, version.indexOf('_'))
|
||||
}) as McModInfo
|
||||
return this.forgeModMetadata[name]!
|
||||
return this.modMetadata[name]!
|
||||
}
|
||||
|
||||
let raw: Buffer | undefined
|
||||
@@ -120,9 +84,9 @@ export class ForgeModStructure17 extends BaseForgeModStructure {
|
||||
const resolved = JSON.parse(raw.toString()) as (McModInfoList | McModInfo[])
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(resolved, 'modListVersion')) {
|
||||
this.forgeModMetadata[name] = (resolved as McModInfoList).modList[0]
|
||||
this.modMetadata[name] = (resolved as McModInfoList).modList[0]
|
||||
} else {
|
||||
this.forgeModMetadata[name] = (resolved as McModInfo[])[0]
|
||||
this.modMetadata[name] = (resolved as McModInfo[])[0]
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
@@ -158,16 +122,16 @@ export class ForgeModStructure17 extends BaseForgeModStructure {
|
||||
|
||||
// Validate
|
||||
const crudeInference = this.attemptCrudeInference(name)
|
||||
if(this.forgeModMetadata[name] != null) {
|
||||
if(this.modMetadata[name] != null) {
|
||||
|
||||
const x = this.forgeModMetadata[name]!
|
||||
const x = this.modMetadata[name]!
|
||||
if(x.modid == null || x.modid === '' || x.modid === this.EXAMPLE_MOD_ID) {
|
||||
x.modid = this.discernResult(claritasId, crudeInference.name.toLowerCase())
|
||||
x.name = this.discernResult(claritasName, crudeInference.name)
|
||||
}
|
||||
|
||||
if(this.forgeModMetadata[name]!.version != null) {
|
||||
const isMalformedVersion = this.isMalformedVersion(this.forgeModMetadata[name]!.version)
|
||||
if(this.modMetadata[name]!.version != null) {
|
||||
const isMalformedVersion = this.isMalformedVersion(this.modMetadata[name]!.version)
|
||||
if(isMalformedVersion) {
|
||||
x.version = this.discernResult(claritasVersion, crudeInference.version)
|
||||
}
|
||||
@@ -177,14 +141,14 @@ export class ForgeModStructure17 extends BaseForgeModStructure {
|
||||
|
||||
|
||||
} else {
|
||||
this.forgeModMetadata[name] = ({
|
||||
this.modMetadata[name] = ({
|
||||
modid: this.discernResult(claritasId, crudeInference.name.toLowerCase()),
|
||||
name: this.discernResult(claritasName, crudeInference.name),
|
||||
version: this.discernResult(claritasVersion, crudeInference.version)
|
||||
}) as McModInfo
|
||||
}
|
||||
|
||||
return this.forgeModMetadata[name]!
|
||||
return this.modMetadata[name]!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export class VersionSegmentedRegistry {
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
untrackedFiles: UntrackedFilesOption[]
|
||||
): BaseForgeModStructure {
|
||||
): BaseForgeModStructure<unknown> {
|
||||
for (const impl of VersionSegmentedRegistry.FORGEMOD_STRUCT_IMPL) {
|
||||
if (impl.isForVersion(minecraftVersion, forgeVersion)) {
|
||||
return new impl(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, untrackedFiles)
|
||||
|
||||
@@ -2,6 +2,7 @@ import got from 'got'
|
||||
import { PromotionsSlim } from '../model/forge/PromotionsSlim.js'
|
||||
import { MinecraftVersion } from './MinecraftVersion.js'
|
||||
import { LoggerUtil } from './LoggerUtil.js'
|
||||
import { FabricInstallerMeta, FabricLoaderMeta, FabricProfileJson, FabricVersionMeta } from '../model/fabric/FabricMeta.js'
|
||||
|
||||
export class VersionUtil {
|
||||
|
||||
@@ -21,6 +22,35 @@ export class VersionUtil {
|
||||
return false
|
||||
}
|
||||
|
||||
public static versionGte(version: string, min: string): boolean {
|
||||
|
||||
if(version === min) {
|
||||
return true
|
||||
}
|
||||
|
||||
const left = version.split('.').map(x => Number(x))
|
||||
const right = min.split('.').map(x => Number(x))
|
||||
|
||||
if(left.length != right.length) {
|
||||
throw new Error('Cannot compare mismatched versions.')
|
||||
}
|
||||
|
||||
for(let i=0; i<left.length; i++) {
|
||||
if(left[i] > right[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public static isPromotionVersion(version: string): boolean {
|
||||
return VersionUtil.PROMOTION_TYPE.indexOf(version.toLowerCase()) > -1
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// Forge
|
||||
|
||||
public static isOneDotTwelveFG2(libraryVersion: string): boolean {
|
||||
const maxFG2 = [14, 23, 5, 2847]
|
||||
const verSplit = libraryVersion.split('.').map(v => Number(v))
|
||||
@@ -34,10 +64,6 @@ export class VersionUtil {
|
||||
return true
|
||||
}
|
||||
|
||||
public static isPromotionVersion(version: string): boolean {
|
||||
return VersionUtil.PROMOTION_TYPE.indexOf(version.toLowerCase()) > -1
|
||||
}
|
||||
|
||||
public static async getPromotionIndex(): Promise<PromotionsSlim> {
|
||||
const response = await got.get<PromotionsSlim>({
|
||||
method: 'get',
|
||||
@@ -67,26 +93,49 @@ export class VersionUtil {
|
||||
return version
|
||||
}
|
||||
|
||||
public static versionGte(version: string, min: string): boolean {
|
||||
// -------------------------------
|
||||
// Fabric
|
||||
|
||||
if(version === min) {
|
||||
return true
|
||||
public static async getFabricInstallerMeta(): Promise<FabricInstallerMeta[]> {
|
||||
const response = await got.get<FabricInstallerMeta[]>({
|
||||
method: 'get',
|
||||
url: 'https://meta.fabricmc.net/v2/versions/installer',
|
||||
responseType: 'json'
|
||||
})
|
||||
return response.body
|
||||
}
|
||||
|
||||
const left = version.split('.').map(x => Number(x))
|
||||
const right = min.split('.').map(x => Number(x))
|
||||
|
||||
if(left.length != right.length) {
|
||||
throw new Error('Cannot compare mismatched versions.')
|
||||
public static async getFabricLoaderMeta(): Promise<FabricLoaderMeta[]> {
|
||||
const response = await got.get<FabricLoaderMeta[]>({
|
||||
method: 'get',
|
||||
url: 'https://meta.fabricmc.net/v2/versions/loader',
|
||||
responseType: 'json'
|
||||
})
|
||||
return response.body
|
||||
}
|
||||
|
||||
for(let i=0; i<left.length; i++) {
|
||||
if(left[i] > right[i]) {
|
||||
return true
|
||||
}
|
||||
public static async getFabricGameMeta(): Promise<FabricVersionMeta[]> {
|
||||
const response = await got.get<FabricVersionMeta[]>({
|
||||
method: 'get',
|
||||
url: 'https://meta.fabricmc.net/v2/versions/game',
|
||||
responseType: 'json'
|
||||
})
|
||||
return response.body
|
||||
}
|
||||
|
||||
return false
|
||||
public static async getFabricProfileJson(gameVersion: string, loaderVersion: string): Promise<FabricProfileJson> {
|
||||
const response = await got.get<FabricProfileJson>({
|
||||
method: 'get',
|
||||
url: `https://meta.fabricmc.net/v2/versions/loader/${gameVersion}/${loaderVersion}/profile/json`,
|
||||
responseType: 'json'
|
||||
})
|
||||
return response.body
|
||||
}
|
||||
|
||||
public static async getPromotedFabricVersion(promotion: string): Promise<string> {
|
||||
const stable = promotion.toLowerCase() === 'recommended'
|
||||
const fabricLoaderMeta = await this.getFabricLoaderMeta()
|
||||
return !stable ? fabricLoaderMeta[0].version : fabricLoaderMeta.find(({ stable }) => stable)!.version
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user