Add command to generate server from CurseForge modpack.
This commit is contained in:
16
README.md
16
README.md
@@ -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 a distribution file from the root file structure.
|
||||
|
||||
49
src/index.ts
49
src/index.ts
@@ -14,6 +14,7 @@ import { VersionUtil } from './util/VersionUtil.js'
|
||||
import { MinecraftVersion } from './util/MinecraftVersion.js'
|
||||
import { LoggerUtil } from './util/LoggerUtil.js'
|
||||
import { generateSchemas } from './util/SchemaUtil.js'
|
||||
import { CurseForgeParser } from './parser/CurseForgeParser.js'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
@@ -134,6 +135,7 @@ const initRootCommand: CommandModule = {
|
||||
try {
|
||||
await generateSchemas(argv.root as string)
|
||||
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}`)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to init new root at ${argv.root}`, error)
|
||||
@@ -203,7 +205,53 @@ const generateServerCommand: CommandModule = {
|
||||
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.',
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.command(generateServerCurseForgeCommand)
|
||||
.command(generateServerCommand)
|
||||
.command(generateDistroCommand)
|
||||
.command(generateSchemasCommand)
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface UntrackedFilesOption {
|
||||
}
|
||||
|
||||
export interface ServerMetaOptions {
|
||||
version?: string
|
||||
forgeVersion?: string
|
||||
}
|
||||
|
||||
@@ -20,7 +21,7 @@ export function getDefaultServerMeta(id: string, version: string, options?: Serv
|
||||
|
||||
const servMeta: ServerMeta = {
|
||||
meta: {
|
||||
version: '1.0.0',
|
||||
version: options?.version ?? '1.0.0',
|
||||
name: `${id} (Minecraft ${version})`,
|
||||
description: `${id} Running Minecraft ${version}`,
|
||||
address: 'localhost:25565',
|
||||
|
||||
114
src/parser/CurseForgeParser.ts
Normal file
114
src/parser/CurseForgeParser.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,6 +23,10 @@ export abstract class BaseFileStructure implements FileStructure {
|
||||
mkdirs(this.containerDirectory)
|
||||
}
|
||||
|
||||
public getContainerDirectory(): string {
|
||||
return this.containerDirectory
|
||||
}
|
||||
|
||||
public abstract getLoggerName(): string
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ import { LibraryStructure } from './module/Library.struct.js'
|
||||
import { MinecraftVersion } from '../../util/MinecraftVersion.js'
|
||||
import { addSchemaToObject, SchemaTypes } from '../../util/SchemaUtil.js'
|
||||
|
||||
export interface CreateServerResult {
|
||||
forgeModContainer?: string
|
||||
libraryContainer: string
|
||||
miscFileContainer: string
|
||||
}
|
||||
|
||||
export class ServerStructure extends BaseModelStructure<Server> {
|
||||
|
||||
private readonly ID_REGEX = /(.+-(.+)$)/
|
||||
@@ -36,25 +42,33 @@ export class ServerStructure extends BaseModelStructure<Server> {
|
||||
return this.resolvedModels
|
||||
}
|
||||
|
||||
public static getEffectiveId(id: string, minecraftVersion: MinecraftVersion): string {
|
||||
return `${id}-${minecraftVersion}`
|
||||
}
|
||||
|
||||
public async createServer(
|
||||
id: string,
|
||||
minecraftVersion: MinecraftVersion,
|
||||
options: {
|
||||
version?: string
|
||||
forgeVersion?: string
|
||||
}
|
||||
): Promise<void> {
|
||||
const effectiveId = `${id}-${minecraftVersion}`
|
||||
): Promise<CreateServerResult | null> {
|
||||
const effectiveId = ServerStructure.getEffectiveId(id, minecraftVersion)
|
||||
const absoluteServerRoot = resolvePath(this.containerDirectory, effectiveId)
|
||||
const relativeServerRoot = join(this.relativeRoot, effectiveId)
|
||||
|
||||
if (await pathExists(absoluteServerRoot)) {
|
||||
this.logger.error('Server already exists! Aborting.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
await mkdirs(absoluteServerRoot)
|
||||
|
||||
const serverMetaOpts: ServerMetaOptions = {}
|
||||
const serverMetaOpts: ServerMetaOptions = {
|
||||
version: options.version
|
||||
}
|
||||
let forgeModContainer: string | undefined = undefined
|
||||
|
||||
if (options.forgeVersion != null) {
|
||||
const fms = VersionSegmentedRegistry.getForgeModStruct(
|
||||
@@ -66,6 +80,7 @@ export class ServerStructure extends BaseModelStructure<Server> {
|
||||
[]
|
||||
)
|
||||
await fms.init()
|
||||
forgeModContainer = fms.getContainerDirectory()
|
||||
serverMetaOpts.forgeVersion = options.forgeVersion
|
||||
}
|
||||
|
||||
@@ -82,6 +97,12 @@ export class ServerStructure extends BaseModelStructure<Server> {
|
||||
const mfs = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, [])
|
||||
await mfs.init()
|
||||
|
||||
return {
|
||||
forgeModContainer,
|
||||
libraryContainer: libS.getContainerDirectory(),
|
||||
miscFileContainer: mfs.getContainerDirectory()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async _doSeverRetrieval(): Promise<Server[]> {
|
||||
|
||||
@@ -14,7 +14,7 @@ export class VersionSegmentedRegistry {
|
||||
ForgeGradle3Adapter
|
||||
]
|
||||
|
||||
public static readonly FORGEMOD_STRUCT_IML = [
|
||||
public static readonly FORGEMOD_STRUCT_IMPL = [
|
||||
ForgeModStructure17,
|
||||
ForgeModStructure113
|
||||
]
|
||||
@@ -44,7 +44,7 @@ export class VersionSegmentedRegistry {
|
||||
baseUrl: string,
|
||||
untrackedFiles: UntrackedFilesOption[]
|
||||
): BaseForgeModStructure {
|
||||
for (const impl of VersionSegmentedRegistry.FORGEMOD_STRUCT_IML) {
|
||||
for (const impl of VersionSegmentedRegistry.FORGEMOD_STRUCT_IMPL) {
|
||||
if (impl.isForVersion(minecraftVersion, forgeVersion)) {
|
||||
return new impl(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, untrackedFiles)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user