Add command to generate server from CurseForge modpack.

This commit is contained in:
Daniel Scalzi
2023-03-18 17:26:35 -04:00
parent 71c2e9baa0
commit 6b2d9edf26
7 changed files with 212 additions and 7 deletions

View File

@@ -122,6 +122,22 @@ Options:
--- ---
#### Generate Server from CurseForge Modpack
Generate an new server in the root directory, including files and mods from an existing CurseForge modpack.
`generate server-curseforge <id> <zipFile>`
The cursforge modpack must be downloaded as a zip and placed into `${ROOT}/modpacks/curseforge`. Pass the name of the modpack as the `<zipFile>` argument.
>
> Example Usage
>
> `generate server-curseforge WesterosCraft-Prod The+WesterosCraft+Modpack-2.1.6.zip`
>
---
#### Generate Distribution #### Generate Distribution
Generate a distribution file from the root file structure. Generate a distribution file from the root file structure.

View File

@@ -14,6 +14,7 @@ import { VersionUtil } from './util/VersionUtil.js'
import { MinecraftVersion } from './util/MinecraftVersion.js' import { MinecraftVersion } from './util/MinecraftVersion.js'
import { LoggerUtil } from './util/LoggerUtil.js' import { LoggerUtil } from './util/LoggerUtil.js'
import { generateSchemas } from './util/SchemaUtil.js' import { generateSchemas } from './util/SchemaUtil.js'
import { CurseForgeParser } from './parser/CurseForgeParser.js'
dotenv.config() dotenv.config()
@@ -134,6 +135,7 @@ const initRootCommand: CommandModule = {
try { try {
await generateSchemas(argv.root as string) await generateSchemas(argv.root as string)
await new DistributionStructure(argv.root as string, '', false, false).init() await new DistributionStructure(argv.root as string, '', false, false).init()
await new CurseForgeParser(argv.root as string, '').init()
logger.info(`Successfully created new root at ${argv.root}`) logger.info(`Successfully created new root at ${argv.root}`)
} catch (error) { } catch (error) {
logger.error(`Failed to init new root at ${argv.root}`, error) logger.error(`Failed to init new root at ${argv.root}`, error)
@@ -203,7 +205,53 @@ const generateServerCommand: CommandModule = {
forgeVersion: argv.forge as string forgeVersion: argv.forge as string
} }
) )
}
}
const generateServerCurseForgeCommand: CommandModule = {
command: 'server-curseforge <id> <zipName>',
describe: 'Generate a new server configuration from a CurseForge modpack.',
builder: (yargs) => {
// yargs = rootOption(yargs)
return yargs
.positional('id', {
describe: 'Server id.',
type: 'string'
})
.positional('zipName', {
describe: 'The name of the modpack zip file.',
type: 'string'
})
},
handler: async (argv) => {
argv.root = getRoot()
logger.debug(`Root set to ${argv.root}`)
logger.debug(`Generating server ${argv.id} using CurseForge modpack ${argv.zipName} as a template.`)
const parser = new CurseForgeParser(argv.root as string, argv.zipName as string)
const modpackManifest = await parser.getModpackManifest()
const minecraftVersion = new MinecraftVersion(modpackManifest.minecraft.version)
// Extract forge version
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}`)
const serverStruct = new ServerStructure(argv.root as string, getBaseURL(), false, false)
const createServerResult = await serverStruct.createServer(
argv.id as string,
minecraftVersion,
{
version: modpackManifest.version,
forgeVersion
}
)
if(createServerResult) {
await parser.enrichServer(createServerResult, modpackManifest)
}
} }
} }
@@ -286,6 +334,7 @@ const generateCommand: CommandModule = {
describe: 'Base generate command.', describe: 'Base generate command.',
builder: (yargs) => { builder: (yargs) => {
return yargs return yargs
.command(generateServerCurseForgeCommand)
.command(generateServerCommand) .command(generateServerCommand)
.command(generateDistroCommand) .command(generateDistroCommand)
.command(generateSchemasCommand) .command(generateSchemasCommand)

View File

@@ -13,6 +13,7 @@ export interface UntrackedFilesOption {
} }
export interface ServerMetaOptions { export interface ServerMetaOptions {
version?: string
forgeVersion?: string forgeVersion?: string
} }
@@ -20,7 +21,7 @@ export function getDefaultServerMeta(id: string, version: string, options?: Serv
const servMeta: ServerMeta = { const servMeta: ServerMeta = {
meta: { meta: {
version: '1.0.0', version: options?.version ?? '1.0.0',
name: `${id} (Minecraft ${version})`, name: `${id} (Minecraft ${version})`,
description: `${id} Running Minecraft ${version}`, description: `${id} Running Minecraft ${version}`,
address: 'localhost:25565', address: 'localhost:25565',

View File

@@ -0,0 +1,114 @@
import { createWriteStream } from 'fs'
import { mkdirs } from 'fs-extra/esm'
import got from 'got'
import StreamZip from 'node-stream-zip'
import { join, resolve } from 'path'
import { pipeline } from 'stream/promises'
import { ToggleableNamespace } from '../structure/spec_model/module/ToggleableModule.struct.js'
import { CreateServerResult } from '../structure/spec_model/Server.struct.js'
import { LoggerUtil } from '../util/LoggerUtil.js'
const log = LoggerUtil.getLogger('CurseForgeParser')
// No idea if this is right
export interface CurseForgeManifest {
minecraft: {
version: string
modLoaders: {
id: string
primary: boolean
}[]
}
manifestType: string
manifestVersion: number
name: string
version: string
author: string
files: {
projectID: number
fileID: number
required: boolean
}[]
overrides: string
}
export interface CurseForgeModFileResponse {
data: {
id: number
gameId: number
isAvailable: boolean
displayName: string
fileName: string
downloadUrl: string
// There are more fields that we don't use right now.
}
}
export class CurseForgeParser {
private static cfClient = got.extend({
prefixUrl: 'https://api.curseforge.com/v1',
responseType: 'json',
headers: {
'X-API-KEY': '$2a$10$JL4kTO/N/oXIM6o3uTYC3eLxGrOI4BIAqpX4vAFeIPoXiTtagidkK'
}
})
private modpackDir: string
private zipPath: string
constructor(
private absoluteRoot: string,
private zipFileName: string
) {
this.modpackDir = join(absoluteRoot, 'modpacks', 'curseforge')
this.zipPath = join(this.modpackDir, zipFileName)
}
public async init(): Promise<void> {
await mkdirs(this.modpackDir)
}
public async getModpackManifest(): Promise<CurseForgeManifest> {
const zip = new StreamZip.async({ file: this.zipPath })
return JSON.parse((await zip.entryData('manifest.json')).toString('utf8'))
}
public async enrichServer(createServerResult: CreateServerResult, manifest: CurseForgeManifest): Promise<void> {
log.debug('Enriching server.')
// Extract overrides
const zip = new StreamZip.async({ file: this.zipPath })
try {
if(manifest.overrides) {
await zip.extract(manifest.overrides, createServerResult.miscFileContainer)
}
}
finally {
await zip.close()
}
if(createServerResult.forgeModContainer) {
const requiredPath = resolve(createServerResult.forgeModContainer, ToggleableNamespace.REQUIRED)
const optionalPath = resolve(createServerResult.forgeModContainer, ToggleableNamespace.OPTIONAL_ON)
// Download mods
for(const file of manifest.files) {
log.debug(`Processing - Mod: ${file.projectID}, File: ${file.fileID}`)
const modInfo = (await CurseForgeParser.cfClient.get<CurseForgeModFileResponse>(`mods/${file.projectID}/files/${file.fileID}`)).body
log.debug(`Downloading ${modInfo.data.fileName}`)
const isJar = modInfo.data.fileName.toLowerCase().endsWith('jar')
const downloadStream = got.stream(modInfo.data.downloadUrl)
const dir = isJar ? file.required ? requiredPath : optionalPath : createServerResult.miscFileContainer
const fileWriterStream = createWriteStream(join(dir, modInfo.data.fileName))
await pipeline(downloadStream, fileWriterStream)
}
}
}
}

View File

@@ -23,6 +23,10 @@ export abstract class BaseFileStructure implements FileStructure {
mkdirs(this.containerDirectory) mkdirs(this.containerDirectory)
} }
public getContainerDirectory(): string {
return this.containerDirectory
}
public abstract getLoggerName(): string public abstract getLoggerName(): string
} }

View File

@@ -11,6 +11,12 @@ import { LibraryStructure } from './module/Library.struct.js'
import { MinecraftVersion } from '../../util/MinecraftVersion.js' import { MinecraftVersion } from '../../util/MinecraftVersion.js'
import { addSchemaToObject, SchemaTypes } from '../../util/SchemaUtil.js' import { addSchemaToObject, SchemaTypes } from '../../util/SchemaUtil.js'
export interface CreateServerResult {
forgeModContainer?: string
libraryContainer: string
miscFileContainer: string
}
export class ServerStructure extends BaseModelStructure<Server> { export class ServerStructure extends BaseModelStructure<Server> {
private readonly ID_REGEX = /(.+-(.+)$)/ private readonly ID_REGEX = /(.+-(.+)$)/
@@ -36,25 +42,33 @@ export class ServerStructure extends BaseModelStructure<Server> {
return this.resolvedModels return this.resolvedModels
} }
public static getEffectiveId(id: string, minecraftVersion: MinecraftVersion): string {
return `${id}-${minecraftVersion}`
}
public async createServer( public async createServer(
id: string, id: string,
minecraftVersion: MinecraftVersion, minecraftVersion: MinecraftVersion,
options: { options: {
version?: string
forgeVersion?: string forgeVersion?: string
} }
): Promise<void> { ): Promise<CreateServerResult | null> {
const effectiveId = `${id}-${minecraftVersion}` const effectiveId = ServerStructure.getEffectiveId(id, minecraftVersion)
const absoluteServerRoot = resolvePath(this.containerDirectory, effectiveId) const absoluteServerRoot = resolvePath(this.containerDirectory, effectiveId)
const relativeServerRoot = join(this.relativeRoot, effectiveId) const relativeServerRoot = join(this.relativeRoot, effectiveId)
if (await pathExists(absoluteServerRoot)) { if (await pathExists(absoluteServerRoot)) {
this.logger.error('Server already exists! Aborting.') this.logger.error('Server already exists! Aborting.')
return return null
} }
await mkdirs(absoluteServerRoot) await mkdirs(absoluteServerRoot)
const serverMetaOpts: ServerMetaOptions = {} const serverMetaOpts: ServerMetaOptions = {
version: options.version
}
let forgeModContainer: string | undefined = undefined
if (options.forgeVersion != null) { if (options.forgeVersion != null) {
const fms = VersionSegmentedRegistry.getForgeModStruct( const fms = VersionSegmentedRegistry.getForgeModStruct(
@@ -66,6 +80,7 @@ export class ServerStructure extends BaseModelStructure<Server> {
[] []
) )
await fms.init() await fms.init()
forgeModContainer = fms.getContainerDirectory()
serverMetaOpts.forgeVersion = options.forgeVersion serverMetaOpts.forgeVersion = options.forgeVersion
} }
@@ -82,6 +97,12 @@ export class ServerStructure extends BaseModelStructure<Server> {
const mfs = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, []) const mfs = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, [])
await mfs.init() await mfs.init()
return {
forgeModContainer,
libraryContainer: libS.getContainerDirectory(),
miscFileContainer: mfs.getContainerDirectory()
}
} }
private async _doSeverRetrieval(): Promise<Server[]> { private async _doSeverRetrieval(): Promise<Server[]> {

View File

@@ -14,7 +14,7 @@ export class VersionSegmentedRegistry {
ForgeGradle3Adapter ForgeGradle3Adapter
] ]
public static readonly FORGEMOD_STRUCT_IML = [ public static readonly FORGEMOD_STRUCT_IMPL = [
ForgeModStructure17, ForgeModStructure17,
ForgeModStructure113 ForgeModStructure113
] ]
@@ -44,7 +44,7 @@ export class VersionSegmentedRegistry {
baseUrl: string, baseUrl: string,
untrackedFiles: UntrackedFilesOption[] untrackedFiles: UntrackedFilesOption[]
): BaseForgeModStructure { ): BaseForgeModStructure {
for (const impl of VersionSegmentedRegistry.FORGEMOD_STRUCT_IML) { for (const impl of VersionSegmentedRegistry.FORGEMOD_STRUCT_IMPL) {
if (impl.isForVersion(minecraftVersion, forgeVersion)) { if (impl.isForVersion(minecraftVersion, forgeVersion)) {
return new impl(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, untrackedFiles) return new impl(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, untrackedFiles)
} }