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 Distribution
|
||||||
|
|
||||||
Generate a distribution file from the root file structure.
|
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 { 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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
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)
|
mkdirs(this.containerDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getContainerDirectory(): string {
|
||||||
|
return this.containerDirectory
|
||||||
|
}
|
||||||
|
|
||||||
public abstract getLoggerName(): string
|
public abstract getLoggerName(): string
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[]> {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user