SurviePote 2/4: atlas Nebula (serveur SurviePote-26.3, distribution.json, sans binaires)
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
import dotenv from 'dotenv'
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { resolve as resolvePath } from 'path'
|
||||
import { URL } from 'url'
|
||||
import { inspect } from 'util'
|
||||
import yargs from 'yargs/yargs'
|
||||
import { Argv, CommandModule } from 'yargs'
|
||||
import { hideBin } from 'yargs/helpers'
|
||||
import { DistributionStructure } from './structure/spec_model/Distribution.struct.js'
|
||||
import { ServerStructure } from './structure/spec_model/Server.struct.js'
|
||||
import { VersionSegmentedRegistry } from './util/VersionSegmentedRegistry.js'
|
||||
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()
|
||||
|
||||
const logger = LoggerUtil.getLogger('Index')
|
||||
|
||||
function getRoot(): string {
|
||||
return resolvePath(process.env.ROOT!)
|
||||
}
|
||||
|
||||
function getHeliosDataFolder(): string | null {
|
||||
if(process.env.HELIOS_DATA_FOLDER) {
|
||||
return resolvePath(process.env.HELIOS_DATA_FOLDER)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getBaseURL(): string {
|
||||
let baseUrl = process.env.BASE_URL!
|
||||
// Users must provide protocol in all other instances.
|
||||
if (!baseUrl.includes('//')) {
|
||||
if (baseUrl.toLowerCase().startsWith('localhost')) {
|
||||
baseUrl = 'http://' + baseUrl
|
||||
} else {
|
||||
throw new TypeError('Please provide a URL protocol (ex. http:// or https://)')
|
||||
}
|
||||
}
|
||||
return (new URL(baseUrl)).toString()
|
||||
}
|
||||
|
||||
function installLocalOption(yargs: Argv): Argv {
|
||||
return yargs.option('installLocal', {
|
||||
describe: 'Install the generated distribution to your local Helios data folder.',
|
||||
type: 'boolean',
|
||||
demandOption: false,
|
||||
global: false,
|
||||
default: false
|
||||
})
|
||||
}
|
||||
|
||||
function discardOutputOption(yargs: Argv): Argv {
|
||||
return yargs.option('discardOutput', {
|
||||
describe: 'Delete cached output after it is no longer required. May be useful if disk space is limited.',
|
||||
type: 'boolean',
|
||||
demandOption: false,
|
||||
global: false,
|
||||
default: false
|
||||
})
|
||||
}
|
||||
|
||||
function invalidateCacheOption(yargs: Argv): Argv {
|
||||
return yargs.option('invalidateCache', {
|
||||
describe: 'Invalidate and delete existing caches as they are encountered. Requires fresh cache generation.',
|
||||
type: 'boolean',
|
||||
demandOption: false,
|
||||
global: false,
|
||||
default: false
|
||||
})
|
||||
}
|
||||
|
||||
// function rootOption(yargs: Argv) {
|
||||
// return yargs.option('root', {
|
||||
// describe: 'File structure root.',
|
||||
// type: 'string',
|
||||
// demandOption: true,
|
||||
// global: true
|
||||
// })
|
||||
// .coerce({
|
||||
// root: resolvePath
|
||||
// })
|
||||
// }
|
||||
|
||||
// function baseUrlOption(yargs: Argv) {
|
||||
// return yargs.option('baseUrl', {
|
||||
// describe: 'Base url of your file host.',
|
||||
// type: 'string',
|
||||
// demandOption: true,
|
||||
// global: true
|
||||
// })
|
||||
// .coerce({
|
||||
// baseUrl: (arg: string) => {
|
||||
// // Users must provide protocol in all other instances.
|
||||
// if (arg.indexOf('//') === -1) {
|
||||
// if (arg.toLowerCase().startsWith('localhost')) {
|
||||
// arg = 'http://' + arg
|
||||
// } else {
|
||||
// throw new TypeError('Please provide a URL protocol (ex. http:// or https://)')
|
||||
// }
|
||||
// }
|
||||
// return (new URL(arg)).toString()
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
function namePositional(yargs: Argv) {
|
||||
return yargs.option('name', {
|
||||
describe: 'Distribution index file name.',
|
||||
type: 'string',
|
||||
default: 'distribution'
|
||||
})
|
||||
}
|
||||
|
||||
// -------------
|
||||
// Init Commands
|
||||
|
||||
const initRootCommand: CommandModule = {
|
||||
command: 'root',
|
||||
describe: 'Generate an empty standard file structure.',
|
||||
builder: (yargs) => {
|
||||
// yargs = rootOption(yargs)
|
||||
return yargs
|
||||
},
|
||||
handler: async (argv) => {
|
||||
argv.root = getRoot()
|
||||
|
||||
logger.debug(`Root set to ${argv.root}`)
|
||||
logger.debug('Invoked init root.')
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initCommand: CommandModule = {
|
||||
command: 'init',
|
||||
aliases: ['i'],
|
||||
describe: 'Base init command.',
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.command(initRootCommand)
|
||||
},
|
||||
handler: (argv) => {
|
||||
argv._handled = true
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// Generate Commands
|
||||
|
||||
const generateServerCommand: CommandModule = {
|
||||
command: 'server <id> <version>',
|
||||
describe: 'Generate a new server configuration.',
|
||||
builder: (yargs) => {
|
||||
// yargs = rootOption(yargs)
|
||||
return yargs
|
||||
.positional('id', {
|
||||
describe: 'Server id.',
|
||||
type: 'string'
|
||||
})
|
||||
.positional('version', {
|
||||
describe: 'Minecraft version.',
|
||||
type: 'string'
|
||||
})
|
||||
.option('forge', {
|
||||
describe: 'Forge version.',
|
||||
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└ Fabric version: ${argv.fabric}`
|
||||
)
|
||||
|
||||
const minecraftVersion = new MinecraftVersion(argv.version as string)
|
||||
|
||||
if(argv.forge != null) {
|
||||
if (VersionUtil.isPromotionVersion(argv.forge as string)) {
|
||||
logger.debug(`Resolving ${argv.forge as string} Forge Version..`)
|
||||
const version = await VersionUtil.getPromotedForgeVersion(minecraftVersion, argv.forge as string)
|
||||
logger.debug(`Forge version set to ${version}`)
|
||||
argv.forge = version
|
||||
}
|
||||
if(minecraftVersion.isGreaterThanOrEqualTo(new MinecraftVersion('1.20.3'))) {
|
||||
logger.error('┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓')
|
||||
logger.error('┃ !! WARNING !! ┃')
|
||||
logger.error('┃ ┃')
|
||||
logger.error('┃ Forge 1.20.3+ removed support for --fml.modLists. ┃')
|
||||
logger.error('┃ Helios Launcher can no longer load mods through Forge. ┃')
|
||||
logger.error('┃ Please use Fabric or await NeoForged Support. ┃')
|
||||
logger.error('┃ ┃')
|
||||
logger.error('┃ !! WARNING !! ┃')
|
||||
logger.error('┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛')
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
fabricVersion: argv.fabric 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
|
||||
// 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}`)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateDistroCommand: CommandModule = {
|
||||
command: 'distro [name]',
|
||||
describe: 'Generate a distribution index from the root file structure.',
|
||||
builder: (yargs) => {
|
||||
yargs = installLocalOption(yargs)
|
||||
yargs = discardOutputOption(yargs)
|
||||
yargs = invalidateCacheOption(yargs)
|
||||
yargs = namePositional(yargs)
|
||||
return yargs
|
||||
},
|
||||
handler: async (argv) => {
|
||||
argv.root = getRoot()
|
||||
argv.baseUrl = getBaseURL()
|
||||
|
||||
const finalName = `${argv.name}.json`
|
||||
|
||||
logger.debug(`Root set to ${argv.root}`)
|
||||
logger.debug(`Base Url set to ${argv.baseUrl}`)
|
||||
logger.debug(`Install option set to ${argv.installLocal}`)
|
||||
logger.debug(`Discard Output option set to ${argv.discardOutput}`)
|
||||
logger.debug(`Invalidate Cache option set to ${argv.invalidateCache}`)
|
||||
logger.debug(`Invoked generate distro name ${finalName}.`)
|
||||
|
||||
const doLocalInstall = argv.installLocal as boolean
|
||||
const discardOutput = argv.discardOutput as boolean ?? false
|
||||
const invalidateCache = argv.invalidateCache as boolean ?? false
|
||||
const heliosDataFolder = getHeliosDataFolder()
|
||||
if(doLocalInstall && heliosDataFolder == null) {
|
||||
logger.error('You MUST specify HELIOS_DATA_FOLDER in your .env when using the --installLocal option.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const distributionStruct = new DistributionStructure(argv.root as string, argv.baseUrl as string, discardOutput, invalidateCache)
|
||||
const distro = await distributionStruct.getSpecModel()
|
||||
const distroOut = JSON.stringify(distro, null, 2)
|
||||
const distroPath = resolvePath(argv.root as string, finalName)
|
||||
await writeFile(distroPath, distroOut)
|
||||
logger.info(`Successfully generated ${finalName}`)
|
||||
logger.info(`Saved to ${distroPath}`)
|
||||
logger.debug('Preview:\n', distro)
|
||||
if(doLocalInstall) {
|
||||
const finalDestination = resolvePath(heliosDataFolder!, finalName)
|
||||
logger.info(`Installing distribution to ${finalDestination}`)
|
||||
await writeFile(finalDestination, distroOut)
|
||||
logger.info('Success!')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Failed to generate distribution with root ${argv.root}.`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateSchemasCommand: CommandModule = {
|
||||
command: 'schemas',
|
||||
describe: 'Generate json schemas.',
|
||||
handler: async (argv) => {
|
||||
argv.root = getRoot()
|
||||
|
||||
logger.debug(`Root set to ${argv.root}`)
|
||||
logger.debug('Invoked generate schemas.')
|
||||
|
||||
try {
|
||||
await generateSchemas(argv.root as string)
|
||||
logger.info('Successfully generated schemas')
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Failed to generate schemas with root ${argv.root}.`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateCommand: CommandModule = {
|
||||
command: 'generate',
|
||||
aliases: ['g'],
|
||||
describe: 'Base generate command.',
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.command(generateServerCurseForgeCommand)
|
||||
.command(generateServerCommand)
|
||||
.command(generateDistroCommand)
|
||||
.command(generateSchemasCommand)
|
||||
},
|
||||
handler: (argv) => {
|
||||
argv._handled = true
|
||||
}
|
||||
}
|
||||
|
||||
const validateCommand: CommandModule = {
|
||||
command: 'validate [name]',
|
||||
describe: 'Validate a distribution.json against the spec.',
|
||||
builder: (yargs) => {
|
||||
return namePositional(yargs)
|
||||
},
|
||||
handler: (argv) => {
|
||||
logger.debug(`Invoked validate with name ${argv.name}.json`)
|
||||
}
|
||||
}
|
||||
|
||||
const latestForgeCommand: CommandModule = {
|
||||
command: 'latest-forge <version>',
|
||||
describe: 'Get the latest version of forge.',
|
||||
handler: async (argv) => {
|
||||
logger.debug(`Invoked latest-forge with version ${argv.version}.`)
|
||||
|
||||
const minecraftVersion = new MinecraftVersion(argv.version as string)
|
||||
const forgeVer = await VersionUtil.getPromotedForgeVersion(minecraftVersion, 'latest')
|
||||
logger.info(`Latest version: Forge ${forgeVer} (${argv.version})`)
|
||||
}
|
||||
}
|
||||
|
||||
const recommendedForgeCommand: CommandModule = {
|
||||
command: 'recommended-forge <version>',
|
||||
describe: 'Get the recommended version of forge. Returns latest if there is no recommended build.',
|
||||
handler: async (argv) => {
|
||||
logger.debug(`Invoked recommended-forge with version ${argv.version}.`)
|
||||
|
||||
const index = await VersionUtil.getPromotionIndex()
|
||||
const minecraftVersion = new MinecraftVersion(argv.version as string)
|
||||
|
||||
let forgeVer = VersionUtil.getPromotedVersionStrict(index, minecraftVersion, 'recommended')
|
||||
if (forgeVer != null) {
|
||||
logger.info(`Recommended version: Forge ${forgeVer} (${minecraftVersion})`)
|
||||
} else {
|
||||
logger.info(`No recommended build for ${minecraftVersion}. Checking for latest version..`)
|
||||
forgeVer = VersionUtil.getPromotedVersionStrict(index, minecraftVersion, 'latest')
|
||||
if (forgeVer != null) {
|
||||
logger.info(`Latest version: Forge ${forgeVer} (${minecraftVersion})`)
|
||||
} else {
|
||||
logger.info(`No build available for ${minecraftVersion}.`)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const testCommand: CommandModule = {
|
||||
command: 'test <mcVer> <forgeVer>',
|
||||
describe: 'Validate a distribution.json against the spec.',
|
||||
builder: (yargs) => {
|
||||
return namePositional(yargs)
|
||||
},
|
||||
handler: async (argv) => {
|
||||
logger.debug(`Invoked test with mcVer ${argv.mcVer} forgeVer ${argv.forgeVer}`)
|
||||
logger.info(process.cwd())
|
||||
const mcVer = new MinecraftVersion(argv.mcVer as string)
|
||||
const resolver = VersionSegmentedRegistry.getForgeResolver(mcVer,
|
||||
argv.forgeVer as string, getRoot(), '', getBaseURL(), false, false)
|
||||
if (resolver != null) {
|
||||
const mdl = await resolver.getModule()
|
||||
logger.info(inspect(mdl, false, null, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Registering yargs configuration.
|
||||
await yargs(hideBin(process.argv))
|
||||
.version(false)
|
||||
.scriptName('')
|
||||
.command(initCommand)
|
||||
.command(generateCommand)
|
||||
.command(validateCommand)
|
||||
.command(latestForgeCommand)
|
||||
.command(recommendedForgeCommand)
|
||||
.command(testCommand)
|
||||
.demandCommand()
|
||||
.help()
|
||||
.argv
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum LibraryType {
|
||||
|
||||
FORGE = 'FORGE'
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export interface ClaritasModuleMetadata {
|
||||
|
||||
/**
|
||||
* Present on ForgeMods (1.7-1.12) of type MOD.
|
||||
* Possibly present on ForgeMods (1.7-1.12) of type CORE_MOD and TWEAKER.
|
||||
* Always present on ForgeMods 1.13+.
|
||||
* Never present in all other circumstances.
|
||||
*/
|
||||
id?: string
|
||||
/**
|
||||
* Never present on ForgeMods (1.7-1.12) of type UNKNOWN.
|
||||
* Present in all other circumstances.
|
||||
*/
|
||||
group?: string
|
||||
/**
|
||||
* Possibly present on ForgeMods (1.7-1.12).
|
||||
* Never present in all other circumstances.
|
||||
*/
|
||||
version?: string
|
||||
/**
|
||||
* Possibly present on ForgeMods (1.7-1.12).
|
||||
* Never present in all other circumstances.
|
||||
*/
|
||||
name?: string
|
||||
/**
|
||||
* Always present on ForgeMods (1.7-1.12).
|
||||
* Never present in all other circumstances.
|
||||
*/
|
||||
modType?: ForgeModType_1_7
|
||||
|
||||
}
|
||||
|
||||
export enum ForgeModType_1_7 {
|
||||
MOD = 'MOD',
|
||||
CORE_MOD = 'CORE_MOD',
|
||||
TWEAKER = 'TWEAKER',
|
||||
UNKNOWN = 'UNKNOWN'
|
||||
}
|
||||
|
||||
export interface ClaritasResult {
|
||||
|
||||
[jarPath: string]: ClaritasModuleMetadata | undefined
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}[]
|
||||
}
|
||||
@@ -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[] }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// https://mcforge.readthedocs.io/en/latest/gettingstarted/structuring/#the-mcmodinfo-file
|
||||
export interface McModInfo {
|
||||
|
||||
modid: string
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
mcversion: string
|
||||
url: string
|
||||
updateUrl?: string
|
||||
updateJSON: string
|
||||
authorList: string[]
|
||||
credits: string
|
||||
logoFile: string
|
||||
screenshots: string[]
|
||||
parent: string
|
||||
useDependencyInformation: boolean
|
||||
requiredMods: string[]
|
||||
dependencies: string[]
|
||||
dependants: string[] // Spelled as dependants on forge's wiki.
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { McModInfo } from './McModInfo.js'
|
||||
|
||||
export interface McModInfoList {
|
||||
|
||||
modListVersion: number
|
||||
modList: McModInfo[]
|
||||
}
|
||||
@@ -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: {
|
||||
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'
|
||||
}}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface PromotionsSlim {
|
||||
|
||||
homepage: string
|
||||
promos: {
|
||||
[id: string]: string
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface VersionManifestFG2 {
|
||||
|
||||
id: string
|
||||
time: string
|
||||
releaseTime: string
|
||||
type: string
|
||||
minecraftArguments: string
|
||||
mainClass: string
|
||||
inheritsFrom: string
|
||||
jar: string
|
||||
logging: Record<string, unknown>
|
||||
libraries: {
|
||||
name: string
|
||||
url?: string
|
||||
checksums?: string[]
|
||||
serverreq?: boolean
|
||||
clientreq?: boolean
|
||||
comment?: string
|
||||
}[]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface VersionManifestFG3 {
|
||||
|
||||
id: string
|
||||
time: string
|
||||
releaseTime: string
|
||||
type: string
|
||||
mainClass: string
|
||||
inheritsFrom: string
|
||||
logging: Record<string, unknown>
|
||||
arguments: {
|
||||
game: string[]
|
||||
}
|
||||
libraries: {
|
||||
name: string
|
||||
downloads: {
|
||||
artifact: {
|
||||
path: string
|
||||
url: string
|
||||
sha1: string
|
||||
size: number
|
||||
}
|
||||
}
|
||||
}[]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Distribution } from 'helios-distribution-types'
|
||||
|
||||
export interface DistroMeta {
|
||||
|
||||
/**
|
||||
* Distribution metadata to be forwarded to the distribution file.
|
||||
*/
|
||||
meta: {
|
||||
rss: Distribution['rss']
|
||||
discord?: Distribution['discord']
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function getDefaultDistroMeta(): DistroMeta {
|
||||
|
||||
return {
|
||||
|
||||
meta: {
|
||||
rss: '<LINK TO RSS FEED>',
|
||||
discord: {
|
||||
clientId: '<FILL IN OR REMOVE DISCORD OBJECT>',
|
||||
smallImageText: '<FILL IN OR REMOVE DISCORD OBJECT>',
|
||||
smallImageKey: '<FILL IN OR REMOVE DISCORD OBJECT>'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Server } from 'helios-distribution-types'
|
||||
|
||||
export interface UntrackedFilesOption {
|
||||
/**
|
||||
* The subdirectory these patterns will be applied to. Ex.
|
||||
* [ "files", "forgegemods" ]
|
||||
*/
|
||||
appliesTo: string[]
|
||||
/**
|
||||
* Glob patterns to match against the file.
|
||||
*/
|
||||
patterns: string[]
|
||||
}
|
||||
|
||||
export interface ServerMetaOptions {
|
||||
version?: string
|
||||
forgeVersion?: string
|
||||
fabricVersion?: string
|
||||
}
|
||||
|
||||
export function getDefaultServerMeta(id: string, version: string, options?: ServerMetaOptions): ServerMeta {
|
||||
|
||||
const servMeta: ServerMeta = {
|
||||
meta: {
|
||||
version: options?.version ?? '1.0.0',
|
||||
name: `${id} (Minecraft ${version})`,
|
||||
description: `${id} Running Minecraft ${version}`,
|
||||
icon: 'How to set the server icon: https://github.com/dscalzi/Nebula#setting-the-server-icon',
|
||||
address: 'localhost:25565',
|
||||
discord: {
|
||||
shortId: '<FILL IN OR REMOVE DISCORD OBJECT>',
|
||||
largeImageText: '<FILL IN OR REMOVE DISCORD OBJECT>',
|
||||
largeImageKey: '<FILL IN OR REMOVE DISCORD OBJECT>'
|
||||
},
|
||||
mainServer: false,
|
||||
autoconnect: false
|
||||
}
|
||||
}
|
||||
|
||||
if(options?.forgeVersion) {
|
||||
servMeta.meta.description = `${servMeta.meta.description} (Forge v${options.forgeVersion})`
|
||||
servMeta.forge = {
|
||||
version: options.forgeVersion
|
||||
}
|
||||
}
|
||||
|
||||
if(options?.fabricVersion) {
|
||||
servMeta.meta.description = `${servMeta.meta.description} (Fabric v${options.fabricVersion})`
|
||||
servMeta.fabric = {
|
||||
version: options.fabricVersion
|
||||
}
|
||||
}
|
||||
|
||||
// Add empty untracked files.
|
||||
servMeta.untrackedFiles = []
|
||||
|
||||
return servMeta
|
||||
}
|
||||
|
||||
export interface ServerMeta {
|
||||
|
||||
/**
|
||||
* Server metadata to be forwarded to the distribution file.
|
||||
*/
|
||||
meta: {
|
||||
version: Server['version']
|
||||
name: Server['name']
|
||||
description: Server['description']
|
||||
icon?: Server['icon']
|
||||
address: Server['address']
|
||||
discord?: Server['discord']
|
||||
mainServer: Server['mainServer']
|
||||
autoconnect: Server['autoconnect']
|
||||
javaOptions?: Server['javaOptions']
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties related to Forge.
|
||||
*/
|
||||
forge?: {
|
||||
/**
|
||||
* The forge version. This does NOT include the minecraft version.
|
||||
* Ex. 14.23.5.2854
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
untrackedFiles?: UntrackedFilesOption[]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface Asset {
|
||||
|
||||
url: string
|
||||
path: string
|
||||
size: number
|
||||
hashType: string
|
||||
hash: string
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
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 CurseForgeModResponse {
|
||||
data: {
|
||||
id: number
|
||||
gameId: number
|
||||
name: string
|
||||
slug: string
|
||||
// There are more fields that we don't use right now.
|
||||
}
|
||||
}
|
||||
|
||||
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')) as CurseForgeManifest
|
||||
}
|
||||
|
||||
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.modContainer) {
|
||||
const requiredPath = resolve(createServerResult.modContainer, ToggleableNamespace.REQUIRED)
|
||||
const optionalPath = resolve(createServerResult.modContainer, ToggleableNamespace.OPTIONAL_ON)
|
||||
|
||||
const disallowedFiles: { name: string, fileName: string, url: string }[] = []
|
||||
|
||||
// Download mods
|
||||
for(const file of manifest.files) {
|
||||
log.debug(`Processing - Mod: ${file.projectID}, File: ${file.fileID}`)
|
||||
const fileInfo = (await CurseForgeParser.cfClient.get<CurseForgeModFileResponse>(`mods/${file.projectID}/files/${file.fileID}`)).body
|
||||
log.debug(`Downloading ${fileInfo.data.fileName}`)
|
||||
|
||||
let dir: string
|
||||
const fileNameLower = fileInfo.data.fileName.toLowerCase()
|
||||
if(fileNameLower.endsWith('jar')) {
|
||||
dir = file.required ? requiredPath : optionalPath
|
||||
}
|
||||
else if(fileNameLower.endsWith('zip')) {
|
||||
// Assume it's a resource pack.
|
||||
dir = join(createServerResult.miscFileContainer, 'resourcepacks')
|
||||
await mkdirs(dir)
|
||||
}
|
||||
else {
|
||||
dir = createServerResult.miscFileContainer
|
||||
}
|
||||
|
||||
const thirdPartyDisallowed = fileInfo.data.downloadUrl == null
|
||||
if(thirdPartyDisallowed) {
|
||||
|
||||
log.warn(`${fileInfo.data.fileName} is not available for 3rd-party download through the curseforge API!`)
|
||||
const modInfo = (await CurseForgeParser.cfClient.get<CurseForgeModResponse>(`mods/${file.projectID}`)).body
|
||||
disallowedFiles.push({
|
||||
name: modInfo.data.name,
|
||||
fileName: fileInfo.data.fileName,
|
||||
url: `https://www.curseforge.com/minecraft/mc-mods/${modInfo.data.slug}/download/${file.fileID}`
|
||||
})
|
||||
|
||||
} else {
|
||||
const downloadStream = got.stream(fileInfo.data.downloadUrl)
|
||||
const fileWriterStream = createWriteStream(join(dir, fileInfo.data.fileName))
|
||||
|
||||
await pipeline(downloadStream, fileWriterStream)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(disallowedFiles.length > 0) {
|
||||
|
||||
log.error('============================================')
|
||||
log.error('\x1b[41mWARNING\x1b[0m')
|
||||
log.error(`${disallowedFiles.length} files declared by this modpack do not permit 3rd-party downloads.`)
|
||||
log.error('They MUST be downloaded manually.')
|
||||
log.error('The mods and their download urls will be listed below.')
|
||||
log.error('============================================')
|
||||
|
||||
for(const file of disallowedFiles) {
|
||||
log.error(`${file.name} (${file.fileName}) - \x1b[32m${file.url}\x1b[0m`)
|
||||
}
|
||||
|
||||
log.error('============================================')
|
||||
log.error('YOUR MODPACK IS NOT FULLY GENERATED!')
|
||||
log.error('MANUAL ACTION REQUIRED! SCROLL UP AND READ THE WARNING!')
|
||||
log.error('============================================')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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 {
|
||||
|
||||
constructor(
|
||||
protected absoluteRoot: string,
|
||||
protected relativeRoot: string,
|
||||
protected baseUrl: string
|
||||
) {}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Module } from 'helios-distribution-types'
|
||||
|
||||
export interface Resolver {
|
||||
|
||||
getModule(): Promise<Module>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import StreamZip from 'node-stream-zip'
|
||||
import { RepoStructure } from '../../structure/repo/Repo.struct.js'
|
||||
import { BaseResolver } from '../BaseResolver.js'
|
||||
import { MinecraftVersion } from '../../util/MinecraftVersion.js'
|
||||
import { VersionUtil } from '../../util/VersionUtil.js'
|
||||
import { LoggerUtil } from '../../util/LoggerUtil.js'
|
||||
|
||||
export abstract class ForgeResolver extends BaseResolver {
|
||||
|
||||
protected readonly MOJANG_REMOTE_REPOSITORY = 'https://libraries.minecraft.net/'
|
||||
protected readonly REMOTE_REPOSITORY = 'https://files.minecraftforge.net/maven/'
|
||||
|
||||
protected repoStructure: RepoStructure
|
||||
protected artifactVersion: string
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
protected minecraftVersion: MinecraftVersion,
|
||||
protected forgeVersion: string,
|
||||
protected discardOutput: boolean,
|
||||
protected invalidateCache: boolean
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, baseUrl)
|
||||
this.repoStructure = new RepoStructure(absoluteRoot, relativeRoot, 'forge')
|
||||
this.artifactVersion = this.inferArtifactVersion()
|
||||
this.checkSecurity()
|
||||
}
|
||||
|
||||
public checkSecurity(): void {
|
||||
const major = this.minecraftVersion.getMajor()
|
||||
const minor = this.minecraftVersion.getMinor()
|
||||
|
||||
// https://github.com/apache/logging-log4j2/pull/608
|
||||
// https://github.com/advisories/GHSA-jfh8-c2jp-5v3q
|
||||
// https://www.minecraft.net/en-us/article/important-message--security-vulnerability-java-edition
|
||||
// https://twitter.com/gigaherz/status/1469331288368861195
|
||||
// https://gist.github.com/TheCurle/f15a6b63ceee3be58bff5e7a97c3a4e6
|
||||
|
||||
const patchMatrix: { [major: number]: string } = {
|
||||
18: '38.0.17',
|
||||
17: '37.1.1',
|
||||
16: '36.2.20',
|
||||
15: '31.2.56',
|
||||
14: '28.2.25',
|
||||
13: '25.0.222',
|
||||
12: '14.23.5.2857'
|
||||
}
|
||||
|
||||
const isVulnerable = major == 1 && (minor <= 18 && minor >= 12)
|
||||
const hasPatch = major == 1 && minor >= 12
|
||||
let unsafe
|
||||
|
||||
if(isVulnerable) {
|
||||
if(hasPatch) {
|
||||
unsafe = !VersionUtil.versionGte(this.forgeVersion, patchMatrix[minor])
|
||||
} else {
|
||||
unsafe = true
|
||||
}
|
||||
}
|
||||
|
||||
if(unsafe) {
|
||||
|
||||
const logger = LoggerUtil.getLogger('ForgeSecurity')
|
||||
|
||||
logger.error('==================================================================')
|
||||
logger.error(' WARNING ')
|
||||
logger.error(' This version of Forge is vulnerable to a CRITICAL RCE exploit. ')
|
||||
logger.error(' DO NOT USE THIS VERSION! ')
|
||||
if(hasPatch) {
|
||||
logger.error(` A patch is available as of Minecraft Forge v${patchMatrix[minor]} `)
|
||||
}
|
||||
else {
|
||||
logger.error(' There is no patch available for this version. ')
|
||||
}
|
||||
logger.error('==================================================================')
|
||||
|
||||
logger.error('To abort, use CTRL + C.')
|
||||
logger.error('Nebula will proceed in 15 seconds..')
|
||||
const target = new Date().getTime() + (15*1000)
|
||||
while(new Date().getTime() <= target) {
|
||||
// Wait
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Coverage is not 100% but that doesnt matter.
|
||||
// It's enough and you should always use the latest version anyway.
|
||||
public inferArtifactVersion(): string {
|
||||
const version = `${this.minecraftVersion}-${this.forgeVersion}`
|
||||
|
||||
const ver = this.forgeVersion.split('.')
|
||||
const major = Number(ver[0])
|
||||
if ([12, 11, 10].includes(major)) {
|
||||
const minor = Number(ver[1])
|
||||
const revision = Number(ver[2])
|
||||
const extra = Number(ver[3])
|
||||
|
||||
if (major === 10) {
|
||||
if (minor === 13 && revision >= 2 && extra >= 1300) {
|
||||
return `${version}-1.7.10`
|
||||
}
|
||||
}
|
||||
else if (major === 11) {
|
||||
if (minor === 15) {
|
||||
if (revision === 1 && extra >= 1890) {
|
||||
return `${version}-1.8.9`
|
||||
} else if (revision === 0 && extra <= 1654) {
|
||||
return `${version}-1.8.8`
|
||||
}
|
||||
} else if (minor === 14 && revision === 0 && extra <= 1295) {
|
||||
return `${version}-1.8`
|
||||
}
|
||||
} else if (major === 12) {
|
||||
if (minor === 17 && revision === 0 && extra <= 1936) {
|
||||
return `${version}-1.9.4`
|
||||
} else if (minor === 16) {
|
||||
if (revision === 0 && extra <= 1885) {
|
||||
return `${version}-1.9`
|
||||
} else if (revision === 1 && extra === 1938) {
|
||||
return `${version}-1.9.0`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
protected async getVersionManifestFromJar(jarPath: string): Promise<Buffer>{
|
||||
return new Promise((resolve, reject) => {
|
||||
const zip = new StreamZip({
|
||||
file: jarPath,
|
||||
storeEntries: true
|
||||
})
|
||||
zip.on('ready', () => {
|
||||
try {
|
||||
const data = zip.entryDataSync('version.json')
|
||||
zip.close()
|
||||
resolve(data)
|
||||
} catch(err) {
|
||||
reject(err)
|
||||
}
|
||||
|
||||
})
|
||||
zip.on('error', err => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { createHash } from 'crypto'
|
||||
import { copy, mkdirs, pathExists, remove } from 'fs-extra/esm'
|
||||
import { lstat, readFile } from 'fs/promises'
|
||||
import { Module, Type } from 'helios-distribution-types'
|
||||
import { basename, join } from 'path'
|
||||
import { VersionManifestFG2 } from '../../../model/forge/VersionManifestFG2.js'
|
||||
import { LibRepoStructure } from '../../../structure/repo/LibRepo.struct.js'
|
||||
import { MavenUtil } from '../../../util/MavenUtil.js'
|
||||
import { PackXZExtractWrapper } from '../../../util/java/PackXZExtractWrapper.js'
|
||||
import { VersionUtil } from '../../../util/VersionUtil.js'
|
||||
import { ForgeResolver } from '../Forge.resolver.js'
|
||||
import { MinecraftVersion } from '../../../util/MinecraftVersion.js'
|
||||
import { LoggerUtil } from '../../../util/LoggerUtil.js'
|
||||
|
||||
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never
|
||||
|
||||
export class ForgeGradle2Adapter extends ForgeResolver {
|
||||
|
||||
private static readonly logger = LoggerUtil.getLogger('FG2 Adapter')
|
||||
|
||||
public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
|
||||
if(version.getMinor() === 12 && !VersionUtil.isOneDotTwelveFG2(libraryVersion)) {
|
||||
return false
|
||||
}
|
||||
return VersionUtil.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12])
|
||||
}
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
minecraftVersion: MinecraftVersion,
|
||||
forgeVersion: string,
|
||||
discardOutput: boolean,
|
||||
invalidateCache: boolean
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, forgeVersion, discardOutput, invalidateCache)
|
||||
}
|
||||
|
||||
public async getModule(): Promise<Module> {
|
||||
return this.getForgeByVersion()
|
||||
}
|
||||
|
||||
public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
|
||||
return ForgeGradle2Adapter.isForVersion(version, libraryVersion)
|
||||
}
|
||||
|
||||
public async getForgeByVersion(): Promise<Module> {
|
||||
const libRepo = this.repoStructure.getLibRepoStruct()
|
||||
const targetLocalPath = libRepo.getLocalForge(this.artifactVersion, 'universal')
|
||||
ForgeGradle2Adapter.logger.debug(`Checking for forge version at ${targetLocalPath}..`)
|
||||
if (!await libRepo.artifactExists(targetLocalPath)) {
|
||||
ForgeGradle2Adapter.logger.debug('Forge not found locally, initializing download..')
|
||||
await libRepo.downloadArtifactByComponents(
|
||||
this.REMOTE_REPOSITORY,
|
||||
LibRepoStructure.FORGE_GROUP,
|
||||
LibRepoStructure.FORGE_ARTIFACT,
|
||||
this.artifactVersion, 'universal', 'jar')
|
||||
} else {
|
||||
ForgeGradle2Adapter.logger.debug('Using locally discovered forge.')
|
||||
}
|
||||
ForgeGradle2Adapter.logger.debug(`Beginning processing of Forge v${this.forgeVersion} (Minecraft ${this.minecraftVersion})`)
|
||||
|
||||
let versionManifestBuf: Buffer
|
||||
try {
|
||||
versionManifestBuf = await this.getVersionManifestFromJar(targetLocalPath)
|
||||
} catch(err) {
|
||||
throw new Error('Failed to find version.json in forge universal jar.')
|
||||
}
|
||||
|
||||
const versionManifest = JSON.parse(versionManifestBuf.toString()) as VersionManifestFG2
|
||||
|
||||
const forgeModule: Module = {
|
||||
id: MavenUtil.mavenComponentsToIdentifier(
|
||||
LibRepoStructure.FORGE_GROUP,
|
||||
LibRepoStructure.FORGE_ARTIFACT,
|
||||
this.artifactVersion, 'universal'
|
||||
),
|
||||
name: 'Minecraft Forge',
|
||||
type: Type.ForgeHosted,
|
||||
artifact: this.generateArtifact(
|
||||
await readFile(targetLocalPath),
|
||||
await lstat(targetLocalPath),
|
||||
libRepo.getArtifactUrlByComponents(
|
||||
this.baseUrl,
|
||||
LibRepoStructure.FORGE_GROUP,
|
||||
LibRepoStructure.FORGE_ARTIFACT,
|
||||
this.artifactVersion, 'universal'
|
||||
)
|
||||
),
|
||||
subModules: []
|
||||
}
|
||||
|
||||
const postProcessQueue = []
|
||||
|
||||
for (const lib of versionManifest.libraries) {
|
||||
if (lib.name.startsWith('net.minecraftforge:forge:')) {
|
||||
// We've already processed forge.
|
||||
continue
|
||||
}
|
||||
ForgeGradle2Adapter.logger.debug(`Processing ${lib.name}..`)
|
||||
|
||||
const extension = await this.determineExtension(lib, libRepo)
|
||||
const localPath = libRepo.getArtifactById(lib.name, extension)
|
||||
|
||||
const postProcess = extension === 'jar.pack.xz'
|
||||
|
||||
let queueDownload = !await libRepo.artifactExists(localPath)
|
||||
let libBuf
|
||||
|
||||
if (!queueDownload) {
|
||||
libBuf = await readFile(localPath)
|
||||
// VERIFY HASH
|
||||
if (!postProcess) { // Checksums for .pack.xz in the version.json are completely useless.
|
||||
if (lib.checksums?.length == 1) {
|
||||
const sha1 = createHash('sha1').update(libBuf).digest('hex')
|
||||
if (sha1 !== lib.checksums[0]) {
|
||||
ForgeGradle2Adapter.logger.debug('Hashes do not match, redownloading..')
|
||||
queueDownload = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ForgeGradle2Adapter.logger.debug('Not found locally, downloading..')
|
||||
queueDownload = true
|
||||
}
|
||||
|
||||
if (queueDownload) {
|
||||
await libRepo.downloadArtifactById(lib.url || this.MOJANG_REMOTE_REPOSITORY, lib.name, extension)
|
||||
libBuf = await readFile(localPath)
|
||||
} else {
|
||||
ForgeGradle2Adapter.logger.debug('Using local copy.')
|
||||
}
|
||||
|
||||
const stats = await lstat(localPath)
|
||||
|
||||
const mavenComponents = MavenUtil.getMavenComponents(lib.name)
|
||||
const properId = MavenUtil.mavenComponentsToIdentifier(
|
||||
mavenComponents.group, mavenComponents.artifact, mavenComponents.version,
|
||||
mavenComponents.classifier, extension
|
||||
)
|
||||
|
||||
forgeModule.subModules?.push({
|
||||
id: properId,
|
||||
name: `Minecraft Forge (${mavenComponents?.artifact})`,
|
||||
type: Type.Library,
|
||||
artifact: this.generateArtifact(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
libBuf!,
|
||||
stats,
|
||||
libRepo.getArtifactUrlByComponents(
|
||||
this.baseUrl,
|
||||
mavenComponents.group, mavenComponents.artifact,
|
||||
mavenComponents.version, mavenComponents.classifier, extension
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
if (postProcess) {
|
||||
postProcessQueue.push({
|
||||
id: properId,
|
||||
localPath
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (const entry of await this.processPackXZFiles(postProcessQueue)) {
|
||||
const el = forgeModule.subModules?.find((element) => element.id === entry.id)
|
||||
if (el != null) {
|
||||
el.artifact.MD5 = entry.MD5
|
||||
} else {
|
||||
ForgeGradle2Adapter.logger.error(`Error during post processing, could not update ${entry.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
return forgeModule
|
||||
}
|
||||
|
||||
private async determineExtension(lib: ArrayElement<VersionManifestFG2['libraries']>, libRepo: LibRepoStructure): Promise<string> {
|
||||
if(lib.url == null) {
|
||||
return 'jar'
|
||||
}
|
||||
ForgeGradle2Adapter.logger.debug('Determing extension..')
|
||||
const possibleExt = [
|
||||
'jar.pack.xz',
|
||||
'jar'
|
||||
]
|
||||
// Check locally.
|
||||
for(const ext of possibleExt) {
|
||||
const localPath = libRepo.getArtifactById(lib.name, ext)
|
||||
const exists = await libRepo.artifactExists(localPath)
|
||||
if(exists) {
|
||||
return ext
|
||||
}
|
||||
}
|
||||
// Check remote.
|
||||
for(const ext of possibleExt) {
|
||||
const exists = await libRepo.headArtifactById(this.REMOTE_REPOSITORY, lib.name, ext)
|
||||
if(exists) {
|
||||
return ext
|
||||
}
|
||||
}
|
||||
// Default to jar.
|
||||
return 'jar'
|
||||
}
|
||||
|
||||
private async processPackXZFiles(
|
||||
processingQueue: {id: string, localPath: string}[]
|
||||
): Promise<{id: string, MD5: string}[]> {
|
||||
|
||||
if(processingQueue.length == 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const accumulator = []
|
||||
|
||||
const tempDir = this.repoStructure.getTempDirectory()
|
||||
|
||||
if (await pathExists(tempDir)) {
|
||||
await remove(tempDir)
|
||||
}
|
||||
|
||||
await mkdirs(tempDir)
|
||||
|
||||
const files = []
|
||||
for (const entry of processingQueue) {
|
||||
const tmpFile = join(tempDir, basename(entry.localPath))
|
||||
await copy(entry.localPath, tmpFile)
|
||||
files.push(tmpFile)
|
||||
}
|
||||
|
||||
ForgeGradle2Adapter.logger.debug('Spawning PackXZExtract.')
|
||||
const packXZExecutor = new PackXZExtractWrapper()
|
||||
await packXZExecutor.extractUnpack(files)
|
||||
ForgeGradle2Adapter.logger.debug('All files extracted, calculating hashes..')
|
||||
|
||||
for (const entry of processingQueue) {
|
||||
const tmpFileName = basename(entry.localPath)
|
||||
const tmpFile = join(tempDir, tmpFileName.substring(0, tmpFileName.indexOf('.pack.xz')))
|
||||
const buf = await readFile(tmpFile)
|
||||
accumulator.push({
|
||||
id: entry.id,
|
||||
MD5: createHash('md5').update(buf).digest('hex')
|
||||
})
|
||||
}
|
||||
|
||||
ForgeGradle2Adapter.logger.debug('Complete, removing temp directory..')
|
||||
|
||||
await remove(tempDir)
|
||||
|
||||
return accumulator
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
import { ForgeResolver } from '../Forge.resolver.js'
|
||||
import { MinecraftVersion } from '../../../util/MinecraftVersion.js'
|
||||
import { LoggerUtil } from '../../../util/LoggerUtil.js'
|
||||
import { VersionUtil } from '../../../util/VersionUtil.js'
|
||||
import { Module, Type } from 'helios-distribution-types'
|
||||
import { LibRepoStructure } from '../../../structure/repo/LibRepo.struct.js'
|
||||
import { pathExists, remove, mkdirs, copy, writeJson } from 'fs-extra/esm'
|
||||
import { lstat, readFile, writeFile } from 'fs/promises'
|
||||
import { join, basename, dirname } from 'path'
|
||||
import { spawn } from 'child_process'
|
||||
import { JavaUtil } from '../../../util/java/JavaUtil.js'
|
||||
import { VersionManifestFG3 } from '../../../model/forge/VersionManifestFG3.js'
|
||||
import { MavenUtil } from '../../../util/MavenUtil.js'
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
interface GeneratedFile {
|
||||
name: string
|
||||
group: string
|
||||
artifact: string
|
||||
version: string
|
||||
classifiers: string[] | [undefined]
|
||||
skipIfNotPresent?: boolean
|
||||
classpath?: boolean
|
||||
}
|
||||
|
||||
export class ForgeGradle3Adapter extends ForgeResolver {
|
||||
|
||||
private static readonly logger = LoggerUtil.getLogger('FG3 Adapter')
|
||||
|
||||
private static readonly WILDCARD_MCP_VERSION = '${mcpVersion}'
|
||||
|
||||
public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
|
||||
if(version.getMinor() === 12 && VersionUtil.isOneDotTwelveFG2(libraryVersion)) {
|
||||
return false
|
||||
}
|
||||
return VersionUtil.isVersionAcceptable(version, [12, 13, 14, 15, 16, 17, 18, 19, 20, 21])
|
||||
}
|
||||
|
||||
public static isExecutableJar(version: MinecraftVersion): boolean {
|
||||
return version.isGreaterThanOrEqualTo(new MinecraftVersion('1.20.3'))
|
||||
}
|
||||
|
||||
private generatedFiles: GeneratedFile[] | undefined
|
||||
private wildcardsInUse: string[] | undefined
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
minecraftVersion: MinecraftVersion,
|
||||
forgeVersion: string,
|
||||
discardOutput: boolean,
|
||||
invalidateCache: boolean
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, forgeVersion, discardOutput, invalidateCache)
|
||||
this.configure()
|
||||
}
|
||||
|
||||
private configure(): void {
|
||||
|
||||
if(ForgeGradle3Adapter.isExecutableJar(this.minecraftVersion)) {
|
||||
|
||||
// Separate block for 1.20.4+
|
||||
|
||||
this.generatedFiles = [
|
||||
{
|
||||
name: 'universal jar',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.FORGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: ['universal'],
|
||||
},
|
||||
{
|
||||
name: 'client jar',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.FORGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: ['client']
|
||||
},
|
||||
{
|
||||
name: 'client shim',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.FORGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: ['shim'],
|
||||
classpath: false
|
||||
},
|
||||
{
|
||||
name: 'fmlcore',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.FMLCORE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: [undefined]
|
||||
},
|
||||
{
|
||||
name: 'javafmllanguage',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.JAVAFMLLANGUAGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: [undefined]
|
||||
},
|
||||
{
|
||||
name: 'mclanguage',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.MCLANGUAGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: [undefined]
|
||||
},
|
||||
{
|
||||
name: 'lowcodelanguage',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.LOWCODELANGUAGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: [undefined]
|
||||
}
|
||||
]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Configure for 13, 14, 15, 16, 17, 18, 19
|
||||
if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [13, 14, 15, 16, 17, 18, 19, 20])) {
|
||||
|
||||
// https://github.com/MinecraftForge/MinecraftForge/commit/97d4652f5fe15931b980117efabdff332f9f6428
|
||||
const mcpUnifiedVersion = `${this.minecraftVersion}-${ForgeGradle3Adapter.WILDCARD_MCP_VERSION}`
|
||||
|
||||
this.generatedFiles = [
|
||||
{
|
||||
name: 'universal jar',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.FORGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: ['universal'],
|
||||
classpath: false
|
||||
},
|
||||
{
|
||||
name: 'client jar',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.FORGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: ['client'],
|
||||
classpath: false
|
||||
},
|
||||
{
|
||||
name: 'client data',
|
||||
group: LibRepoStructure.MINECRAFT_GROUP,
|
||||
artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT,
|
||||
version: this.minecraftVersion.toString(),
|
||||
classifiers: ['data'],
|
||||
skipIfNotPresent: true,
|
||||
classpath: false
|
||||
},
|
||||
{
|
||||
name: 'client srg',
|
||||
group: LibRepoStructure.MINECRAFT_GROUP,
|
||||
artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT,
|
||||
version: mcpUnifiedVersion,
|
||||
classifiers: ['srg'],
|
||||
classpath: false
|
||||
}
|
||||
]
|
||||
this.wildcardsInUse = [
|
||||
ForgeGradle3Adapter.WILDCARD_MCP_VERSION
|
||||
]
|
||||
|
||||
if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [13, 14, 15, 16])) {
|
||||
|
||||
// Base jar present for 1.13-1.16
|
||||
|
||||
this.generatedFiles.unshift(
|
||||
{
|
||||
name: 'base jar',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.FORGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: [undefined]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [17, 18, 19, 20])) {
|
||||
|
||||
// Added in 1.17+
|
||||
|
||||
this.generatedFiles.unshift(
|
||||
{
|
||||
name: 'fmlcore',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.FMLCORE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: [undefined]
|
||||
},
|
||||
{
|
||||
name: 'javafmllanguage',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.JAVAFMLLANGUAGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: [undefined]
|
||||
},
|
||||
{
|
||||
name: 'mclanguage',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.MCLANGUAGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: [undefined]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (VersionUtil.isVersionAcceptable(this.minecraftVersion, [18, 19, 20])) {
|
||||
|
||||
// Added in 1.18+
|
||||
|
||||
this.generatedFiles.unshift(
|
||||
{
|
||||
name: 'lowcodelanguage',
|
||||
group: LibRepoStructure.FORGE_GROUP,
|
||||
artifact: LibRepoStructure.LOWCODELANGUAGE_ARTIFACT,
|
||||
version: this.artifactVersion,
|
||||
classifiers: [undefined]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [13, 14, 15])) {
|
||||
|
||||
// 13, 14, 15 use just the MC version.
|
||||
|
||||
this.generatedFiles.push(
|
||||
{
|
||||
name: 'client slim',
|
||||
group: LibRepoStructure.MINECRAFT_GROUP,
|
||||
artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT,
|
||||
version: this.minecraftVersion.toString(),
|
||||
classifiers: [
|
||||
'slim',
|
||||
'slim-stable'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'client extra',
|
||||
group: LibRepoStructure.MINECRAFT_GROUP,
|
||||
artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT,
|
||||
version: this.minecraftVersion.toString(),
|
||||
classifiers: [
|
||||
'extra',
|
||||
'extra-stable'
|
||||
]
|
||||
}
|
||||
)
|
||||
} else {
|
||||
|
||||
// 16+ uses the mcp unified version.
|
||||
|
||||
this.generatedFiles.push(
|
||||
{
|
||||
name: 'client slim',
|
||||
group: LibRepoStructure.MINECRAFT_GROUP,
|
||||
artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT,
|
||||
version: mcpUnifiedVersion,
|
||||
classifiers: [
|
||||
'slim',
|
||||
'slim-stable'
|
||||
],
|
||||
classpath: false
|
||||
},
|
||||
{
|
||||
name: 'client extra',
|
||||
group: LibRepoStructure.MINECRAFT_GROUP,
|
||||
artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT,
|
||||
version: mcpUnifiedVersion,
|
||||
classifiers: [
|
||||
'extra',
|
||||
'extra-stable'
|
||||
],
|
||||
classpath: false
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Configure for 12
|
||||
if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [12])) {
|
||||
// NOTHING TO CONFIGURE
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
public async getModule(): Promise<Module> {
|
||||
return this.process()
|
||||
}
|
||||
|
||||
public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
|
||||
return ForgeGradle3Adapter.isForVersion(version, libraryVersion)
|
||||
}
|
||||
|
||||
private async process(): Promise<Module> {
|
||||
const libRepo = this.repoStructure.getLibRepoStruct()
|
||||
|
||||
// Get Installer
|
||||
const installerPath = libRepo.getLocalForge(this.artifactVersion, 'installer')
|
||||
ForgeGradle3Adapter.logger.debug(`Checking for forge installer at ${installerPath}..`)
|
||||
if (!await libRepo.artifactExists(installerPath)) {
|
||||
ForgeGradle3Adapter.logger.debug('Forge installer not found locally, initializing download..')
|
||||
await libRepo.downloadArtifactByComponents(
|
||||
this.REMOTE_REPOSITORY,
|
||||
LibRepoStructure.FORGE_GROUP,
|
||||
LibRepoStructure.FORGE_ARTIFACT,
|
||||
this.artifactVersion, 'installer', 'jar'
|
||||
)
|
||||
} else {
|
||||
ForgeGradle3Adapter.logger.debug('Using locally discovered forge installer.')
|
||||
}
|
||||
ForgeGradle3Adapter.logger.debug(`Beginning processing of Forge v${this.forgeVersion} (Minecraft ${this.minecraftVersion})`)
|
||||
|
||||
if(this.generatedFiles != null && this.generatedFiles.length > 0) {
|
||||
// Run installer
|
||||
return this.processWithInstaller(installerPath)
|
||||
} else {
|
||||
// Installer not required
|
||||
return this.processWithoutInstaller(installerPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async processWithInstaller(installerPath: string): Promise<Module> {
|
||||
|
||||
let doInstall = true
|
||||
// Check cache.
|
||||
const cacheDir = this.repoStructure.getForgeCacheDirectory(this.artifactVersion)
|
||||
if (await pathExists(cacheDir)) {
|
||||
if(this.invalidateCache) {
|
||||
ForgeGradle3Adapter.logger.info(`Removing existing cache ${cacheDir}..`)
|
||||
await remove(cacheDir)
|
||||
} else {
|
||||
// Use cache.
|
||||
doInstall = false
|
||||
ForgeGradle3Adapter.logger.info(`Using cached results at ${cacheDir}.`)
|
||||
}
|
||||
} else {
|
||||
await mkdirs(cacheDir)
|
||||
}
|
||||
const installerOutputDir = cacheDir
|
||||
|
||||
if(doInstall) {
|
||||
const workingInstaller = join(installerOutputDir, basename(installerPath))
|
||||
|
||||
await copy(installerPath, workingInstaller)
|
||||
|
||||
// Required for the installer to function.
|
||||
await writeFile(join(installerOutputDir, 'launcher_profiles.json'), JSON.stringify({}))
|
||||
|
||||
ForgeGradle3Adapter.logger.debug('Spawning forge installer')
|
||||
|
||||
ForgeGradle3Adapter.logger.info('============== [ IMPORTANT ] ==============')
|
||||
ForgeGradle3Adapter.logger.info('When the installer opens please set the client installation directory to:')
|
||||
ForgeGradle3Adapter.logger.info(installerOutputDir)
|
||||
ForgeGradle3Adapter.logger.info('===========================================')
|
||||
|
||||
await this.executeInstaller(workingInstaller)
|
||||
|
||||
ForgeGradle3Adapter.logger.debug('Installer finished, beginning processing..')
|
||||
}
|
||||
|
||||
await this.verifyInstallerRan(installerOutputDir)
|
||||
|
||||
ForgeGradle3Adapter.logger.debug('Processing Version Manifest')
|
||||
const versionManifestTuple = await this.processVersionManifest(installerOutputDir)
|
||||
const versionManifest = versionManifestTuple[0]
|
||||
|
||||
ForgeGradle3Adapter.logger.debug('Processing generated forge files.')
|
||||
const forgeModule = await this.processForgeModule(versionManifest, installerOutputDir)
|
||||
|
||||
// Attach version.json module.
|
||||
forgeModule.subModules?.unshift(versionManifestTuple[1])
|
||||
|
||||
ForgeGradle3Adapter.logger.debug('Processing Libraries')
|
||||
const libs = await this.processLibraries(versionManifest, installerOutputDir)
|
||||
|
||||
forgeModule.subModules = forgeModule.subModules?.concat(libs)
|
||||
|
||||
if(this.discardOutput) {
|
||||
ForgeGradle3Adapter.logger.info(`Removing installer output at ${installerOutputDir}..`)
|
||||
await remove(installerOutputDir)
|
||||
ForgeGradle3Adapter.logger.info('Removed successfully.')
|
||||
}
|
||||
|
||||
return forgeModule
|
||||
|
||||
}
|
||||
|
||||
private getVersionManifestPath(installerOutputDir: string): string {
|
||||
const versionRepo = this.repoStructure.getVersionRepoStruct()
|
||||
const versionName = versionRepo.getFileName(this.minecraftVersion, this.forgeVersion)
|
||||
return join(installerOutputDir, 'versions', versionName, `${versionName}.json`)
|
||||
}
|
||||
|
||||
private async verifyInstallerRan(installerOutputDir: string): Promise<void> {
|
||||
const versionManifestPath = this.getVersionManifestPath(installerOutputDir)
|
||||
|
||||
if(!await pathExists(versionManifestPath)) {
|
||||
await remove(installerOutputDir)
|
||||
throw new Error(`Forge was either not installed or installed to the wrong location. When the forge installer opens, you MUST set the installation directory to ${installerOutputDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async processVersionManifest(installerOutputDir: string): Promise<[VersionManifestFG3, Module]> {
|
||||
const versionRepo = this.repoStructure.getVersionRepoStruct()
|
||||
const versionManifestPath = this.getVersionManifestPath(installerOutputDir)
|
||||
|
||||
const versionManifestBuf = await readFile(versionManifestPath)
|
||||
const versionManifest = JSON.parse(versionManifestBuf.toString()) as VersionManifestFG3
|
||||
|
||||
const versionManifestModule: Module = {
|
||||
id: this.artifactVersion,
|
||||
name: 'Minecraft Forge (version.json)',
|
||||
type: Type.VersionManifest,
|
||||
artifact: this.generateArtifact(
|
||||
versionManifestBuf,
|
||||
await lstat(versionManifestPath),
|
||||
versionRepo.getVersionManifestURL(this.baseUrl, this.minecraftVersion, this.forgeVersion)
|
||||
)
|
||||
}
|
||||
|
||||
const destination = versionRepo.getVersionManifest(
|
||||
this.minecraftVersion,
|
||||
this.forgeVersion
|
||||
)
|
||||
|
||||
await copy(versionManifestPath, destination, {overwrite: true})
|
||||
|
||||
return [versionManifest, versionManifestModule]
|
||||
}
|
||||
|
||||
private async processForgeModule(versionManifest: VersionManifestFG3, installerOutputDir: string): Promise<Module> {
|
||||
|
||||
const libDir = join(installerOutputDir, 'libraries')
|
||||
|
||||
if(this.wildcardsInUse) {
|
||||
if(this.wildcardsInUse.includes(ForgeGradle3Adapter.WILDCARD_MCP_VERSION)) {
|
||||
|
||||
const mcpVersion = this.getMCPVersion(versionManifest.arguments.game)
|
||||
if(mcpVersion == null) {
|
||||
throw new Error('MCP Version not found.. did forge change their format?')
|
||||
}
|
||||
|
||||
this.generatedFiles = this.generatedFiles!.map(f => {
|
||||
if(f.version.includes(ForgeGradle3Adapter.WILDCARD_MCP_VERSION)) {
|
||||
return {
|
||||
...f,
|
||||
version: f.version.replace(ForgeGradle3Adapter.WILDCARD_MCP_VERSION, mcpVersion)
|
||||
}
|
||||
}
|
||||
return f
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const mdls: Module[] = []
|
||||
|
||||
for (const entry of this.generatedFiles!) {
|
||||
|
||||
const targetLocations: string[] = []
|
||||
let located = false
|
||||
|
||||
classifierLoop:
|
||||
for (const _classifier of entry.classifiers) {
|
||||
|
||||
const targetLocalPath = join(
|
||||
libDir,
|
||||
MavenUtil.mavenComponentsAsNormalizedPath(entry.group, entry.artifact, entry.version, _classifier)
|
||||
)
|
||||
|
||||
targetLocations.push(targetLocalPath)
|
||||
|
||||
const exists = await pathExists(targetLocalPath)
|
||||
if (exists) {
|
||||
|
||||
mdls.push({
|
||||
id: MavenUtil.mavenComponentsToIdentifier(
|
||||
entry.group,
|
||||
entry.artifact,
|
||||
entry.version,
|
||||
_classifier
|
||||
),
|
||||
name: `Minecraft Forge (${entry.name})`,
|
||||
type: Type.Library,
|
||||
classpath: entry.classpath ?? true,
|
||||
artifact: this.generateArtifact(
|
||||
await readFile(targetLocalPath),
|
||||
await lstat(targetLocalPath),
|
||||
this.repoStructure.getLibRepoStruct().getArtifactUrlByComponents(
|
||||
this.baseUrl,
|
||||
entry.group,
|
||||
entry.artifact,
|
||||
entry.version,
|
||||
_classifier
|
||||
)
|
||||
),
|
||||
subModules: []
|
||||
})
|
||||
|
||||
const destination = this.repoStructure.getLibRepoStruct().getArtifactByComponents(
|
||||
entry.group,
|
||||
entry.artifact,
|
||||
entry.version,
|
||||
_classifier
|
||||
)
|
||||
|
||||
await copy(targetLocalPath, destination, {overwrite: true})
|
||||
|
||||
located = true
|
||||
break classifierLoop
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!entry.skipIfNotPresent && !located) {
|
||||
throw new Error(`Required file ${entry.name} not found at any expected location:\n\t${targetLocations.join('\n\t')}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const forgeModule = mdls.shift()!
|
||||
forgeModule.type = Type.ForgeHosted
|
||||
forgeModule.subModules = mdls
|
||||
|
||||
return forgeModule
|
||||
}
|
||||
|
||||
private async processLibraries(manifest: VersionManifestFG3, installerOutputDir: string): Promise<Module[]> {
|
||||
|
||||
const libDir = join(installerOutputDir, 'libraries')
|
||||
const libRepo = this.repoStructure.getLibRepoStruct()
|
||||
|
||||
const mdls: Module[] = []
|
||||
|
||||
for (const entry of manifest.libraries) {
|
||||
const artifact = entry.downloads.artifact
|
||||
if (artifact.url) {
|
||||
|
||||
const targetLocalPath = join(libDir, artifact.path)
|
||||
|
||||
if (!await pathExists(targetLocalPath)) {
|
||||
throw new Error(`Expected library ${entry.name} not found!`)
|
||||
}
|
||||
|
||||
const components = MavenUtil.getMavenComponents(entry.name)
|
||||
|
||||
mdls.push({
|
||||
id: entry.name,
|
||||
name: `Minecraft Forge (${components.artifact})`,
|
||||
type: Type.Library,
|
||||
artifact: this.generateArtifact(
|
||||
await readFile(targetLocalPath),
|
||||
await lstat(targetLocalPath),
|
||||
libRepo.getArtifactUrlByComponents(
|
||||
this.baseUrl,
|
||||
components.group,
|
||||
components.artifact,
|
||||
components.version,
|
||||
components.classifier,
|
||||
components.extension
|
||||
)
|
||||
)
|
||||
})
|
||||
const destination = libRepo.getArtifactByComponents(
|
||||
components.group,
|
||||
components.artifact,
|
||||
components.version,
|
||||
components.classifier,
|
||||
components.extension
|
||||
)
|
||||
|
||||
await copy(targetLocalPath, destination, {overwrite: true})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return mdls
|
||||
|
||||
}
|
||||
|
||||
private executeInstaller(installerExec: string): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
const fiLogger = LoggerUtil.getLogger('Forge Installer')
|
||||
const child = spawn(JavaUtil.getJavaExecutable(), [
|
||||
'-jar',
|
||||
installerExec
|
||||
], {
|
||||
cwd: dirname(installerExec)
|
||||
})
|
||||
child.stdout.on('data', (data) => fiLogger.info(data.toString('utf8').trim()))
|
||||
child.stderr.on('data', (data) => fiLogger.error(data.toString('utf8').trim()))
|
||||
child.on('close', code => {
|
||||
if(code === 0) {
|
||||
fiLogger.info('Exited with code', code)
|
||||
} else {
|
||||
fiLogger.error('Exited with code', code)
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private getMCPVersion(args: string[]): string | null {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--fml.mcpVersion') {
|
||||
return args[i + 1]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private async processWithoutInstaller(installerPath: string): Promise<Module> {
|
||||
|
||||
// Extract version.json from installer.
|
||||
|
||||
let versionManifestBuf: Buffer
|
||||
try {
|
||||
versionManifestBuf = await this.getVersionManifestFromJar(installerPath)
|
||||
} catch(err) {
|
||||
throw new Error('Failed to find version.json in forge installer jar.')
|
||||
}
|
||||
|
||||
const versionManifest = JSON.parse(versionManifestBuf.toString()) as VersionManifestFG3
|
||||
|
||||
// Save Version Manifest
|
||||
const versionManifestDest = this.repoStructure.getVersionRepoStruct().getVersionManifest(
|
||||
this.minecraftVersion,
|
||||
this.forgeVersion
|
||||
)
|
||||
await mkdirs(dirname(versionManifestDest))
|
||||
await writeJson(versionManifestDest, versionManifest, { spaces: 4 })
|
||||
|
||||
const libRepo = this.repoStructure.getLibRepoStruct()
|
||||
const universalLocalPath = libRepo.getLocalForge(this.artifactVersion, 'universal')
|
||||
ForgeGradle3Adapter.logger.debug(`Checking for Forge Universal jar at ${universalLocalPath}..`)
|
||||
|
||||
const forgeMdl = versionManifest.libraries.find(val => val.name.startsWith('net.minecraftforge:forge:'))
|
||||
|
||||
if(forgeMdl == null) {
|
||||
throw new Error('Forge entry not found in version.json!')
|
||||
}
|
||||
|
||||
let forgeUniversalBuffer
|
||||
|
||||
// Check for local universal jar.
|
||||
if (await libRepo.artifactExists(universalLocalPath)) {
|
||||
const localUniBuf = await readFile(universalLocalPath)
|
||||
const sha1 = createHash('sha1').update(localUniBuf).digest('hex')
|
||||
if(sha1 !== forgeMdl.downloads.artifact.sha1) {
|
||||
ForgeGradle3Adapter.logger.debug('SHA-1 of local universal jar does not match version.json entry.')
|
||||
ForgeGradle3Adapter.logger.debug('Redownloading Forge Universal jar..')
|
||||
} else {
|
||||
ForgeGradle3Adapter.logger.debug('Using locally discovered forge.')
|
||||
forgeUniversalBuffer = localUniBuf
|
||||
}
|
||||
} else {
|
||||
ForgeGradle3Adapter.logger.debug('Forge Universal jar not found locally, initializing download..')
|
||||
}
|
||||
|
||||
// Download if local is missing or corrupt
|
||||
if(!forgeUniversalBuffer) {
|
||||
await libRepo.downloadArtifactByComponents(
|
||||
this.REMOTE_REPOSITORY,
|
||||
LibRepoStructure.FORGE_GROUP,
|
||||
LibRepoStructure.FORGE_ARTIFACT,
|
||||
this.artifactVersion, 'universal', 'jar')
|
||||
forgeUniversalBuffer = await readFile(universalLocalPath)
|
||||
}
|
||||
|
||||
ForgeGradle3Adapter.logger.debug(`Beginning processing of Forge v${this.forgeVersion} (Minecraft ${this.minecraftVersion})`)
|
||||
|
||||
const forgeModule: Module = {
|
||||
id: MavenUtil.mavenComponentsToIdentifier(
|
||||
LibRepoStructure.FORGE_GROUP,
|
||||
LibRepoStructure.FORGE_ARTIFACT,
|
||||
this.artifactVersion, 'universal'
|
||||
),
|
||||
name: 'Minecraft Forge',
|
||||
type: Type.ForgeHosted,
|
||||
artifact: this.generateArtifact(
|
||||
forgeUniversalBuffer,
|
||||
await lstat(universalLocalPath),
|
||||
libRepo.getArtifactUrlByComponents(
|
||||
this.baseUrl,
|
||||
LibRepoStructure.FORGE_GROUP,
|
||||
LibRepoStructure.FORGE_ARTIFACT,
|
||||
this.artifactVersion, 'universal'
|
||||
)
|
||||
),
|
||||
subModules: []
|
||||
}
|
||||
|
||||
// Attach Version Manifest module.
|
||||
forgeModule.subModules?.push({
|
||||
id: this.artifactVersion,
|
||||
name: 'Minecraft Forge (version.json)',
|
||||
type: Type.VersionManifest,
|
||||
artifact: this.generateArtifact(
|
||||
await readFile(versionManifestDest),
|
||||
await lstat(versionManifestDest),
|
||||
this.repoStructure.getVersionRepoStruct().getVersionManifestURL(
|
||||
this.baseUrl, this.minecraftVersion, this.forgeVersion)
|
||||
)
|
||||
})
|
||||
|
||||
for(const lib of versionManifest.libraries) {
|
||||
if (lib.name.startsWith('net.minecraftforge:forge:')) {
|
||||
// We've already processed forge.
|
||||
continue
|
||||
}
|
||||
ForgeGradle3Adapter.logger.debug(`Processing ${lib.name}..`)
|
||||
|
||||
const extension = 'jar'
|
||||
const localPath = libRepo.getArtifactById(lib.name, extension)
|
||||
|
||||
let queueDownload = !await libRepo.artifactExists(localPath)
|
||||
let libBuf
|
||||
|
||||
if (!queueDownload) {
|
||||
libBuf = await readFile(localPath)
|
||||
const sha1 = createHash('sha1').update(libBuf).digest('hex')
|
||||
if (sha1 !== lib.downloads.artifact.sha1) {
|
||||
ForgeGradle3Adapter.logger.debug('Hashes do not match, redownloading..')
|
||||
queueDownload = true
|
||||
}
|
||||
} else {
|
||||
ForgeGradle3Adapter.logger.debug('Not found locally, downloading..')
|
||||
queueDownload = true
|
||||
}
|
||||
|
||||
if (queueDownload) {
|
||||
await libRepo.downloadArtifactDirect(lib.downloads.artifact.url, lib.downloads.artifact.path)
|
||||
libBuf = await readFile(localPath)
|
||||
} else {
|
||||
ForgeGradle3Adapter.logger.debug('Using local copy.')
|
||||
}
|
||||
|
||||
const stats = await lstat(localPath)
|
||||
|
||||
const mavenComponents = MavenUtil.getMavenComponents(lib.name)
|
||||
const properId = MavenUtil.mavenComponentsToIdentifier(
|
||||
mavenComponents.group, mavenComponents.artifact, mavenComponents.version,
|
||||
mavenComponents.classifier, extension
|
||||
)
|
||||
|
||||
forgeModule.subModules?.push({
|
||||
id: properId,
|
||||
name: `Minecraft Forge (${mavenComponents?.artifact})`,
|
||||
type: Type.Library,
|
||||
artifact: this.generateArtifact(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
libBuf!,
|
||||
stats,
|
||||
libRepo.getArtifactUrlByComponents(
|
||||
this.baseUrl,
|
||||
mavenComponents.group, mavenComponents.artifact,
|
||||
mavenComponents.version, mavenComponents.classifier, extension
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return forgeModule
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { mkdirs } from 'fs-extra/esm'
|
||||
import { join, resolve } from 'path'
|
||||
import { FileStructure } from './FileStructure.js'
|
||||
import { Logger } from 'winston'
|
||||
import { LoggerUtil } from '../util/LoggerUtil.js'
|
||||
|
||||
export abstract class BaseFileStructure implements FileStructure {
|
||||
|
||||
protected logger: Logger
|
||||
protected containerDirectory: string
|
||||
|
||||
constructor(
|
||||
protected absoluteRoot: string,
|
||||
protected relativeRoot: string,
|
||||
protected structRoot: string
|
||||
) {
|
||||
this.relativeRoot = join(relativeRoot, structRoot)
|
||||
this.containerDirectory = resolve(absoluteRoot, structRoot)
|
||||
this.logger = LoggerUtil.getLogger(this.getLoggerName())
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await mkdirs(this.containerDirectory)
|
||||
}
|
||||
|
||||
public getContainerDirectory(): string {
|
||||
return this.containerDirectory
|
||||
}
|
||||
|
||||
public abstract getLoggerName(): string
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface FileStructure {
|
||||
|
||||
init(): Promise<void>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import got from 'got'
|
||||
import { mkdirs, pathExists } from 'fs-extra/esm'
|
||||
import { createWriteStream } from 'fs'
|
||||
import { dirname, join, resolve } from 'path'
|
||||
import { URL } from 'url'
|
||||
import { MavenUtil } from '../../util/MavenUtil.js'
|
||||
import { BaseFileStructure } from '../BaseFileStructure.js'
|
||||
import { LoggerUtil } from '../../util/LoggerUtil.js'
|
||||
|
||||
export abstract class BaseMavenRepo extends BaseFileStructure {
|
||||
|
||||
private static readonly logger = LoggerUtil.getLogger('BaseMavenRepo')
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
structRoot: string
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, structRoot)
|
||||
}
|
||||
|
||||
public getArtifactById(mavenIdentifier: string, extension?: string): string {
|
||||
return resolve(this.containerDirectory, MavenUtil.mavenIdentifierAsPath(mavenIdentifier, extension))
|
||||
}
|
||||
|
||||
public getArtifactByComponents(
|
||||
group: string, artifact: string, version: string, classifier?: string, extension = 'jar'
|
||||
): string {
|
||||
return resolve(this.containerDirectory,
|
||||
MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension))
|
||||
}
|
||||
|
||||
public getArtifactUrlByComponents(
|
||||
baseURL: string, group: string, artifact: string, version: string, classifier?: string, extension = 'jar'
|
||||
): string {
|
||||
return new URL(join(this.relativeRoot,
|
||||
MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension)), baseURL).toString()
|
||||
}
|
||||
|
||||
public async artifactExists(path: string): Promise<boolean> {
|
||||
return pathExists(path)
|
||||
}
|
||||
|
||||
public async downloadArtifactById(url: string, mavenIdentifier: string, extension?: string): Promise<void> {
|
||||
return this.downloadArtifactBase(url, MavenUtil.mavenIdentifierAsPath(mavenIdentifier, extension))
|
||||
}
|
||||
|
||||
public async downloadArtifactByComponents(
|
||||
url: string, group: string, artifact: string, version: string, classifier?: string, extension?: string
|
||||
): Promise<void> {
|
||||
return this.downloadArtifactBase(url,
|
||||
MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension))
|
||||
}
|
||||
|
||||
private async downloadArtifactBase(url: string, relative: string): Promise<void> {
|
||||
const resolvedURL = new URL(relative, url).toString()
|
||||
return this.downloadArtifactDirect(resolvedURL, relative)
|
||||
}
|
||||
|
||||
public async downloadArtifactDirect(url: string, path: string): Promise<void> {
|
||||
BaseMavenRepo.logger.debug(`Downloading ${url}..`)
|
||||
const request = got.stream.get({ url })
|
||||
const localPath = resolve(this.containerDirectory, path)
|
||||
await mkdirs(dirname(localPath))
|
||||
const writer = createWriteStream(localPath)
|
||||
request.pipe(writer)
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', () => {
|
||||
BaseMavenRepo.logger.debug(`Completed download of ${url}.`)
|
||||
resolve()
|
||||
})
|
||||
writer.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
public async headArtifactById(url: string, mavenIdentifier: string, extension?: string): Promise<boolean> {
|
||||
return this.headArtifactBase(url, MavenUtil.mavenIdentifierAsPath(mavenIdentifier, extension))
|
||||
}
|
||||
|
||||
public async headArtifactByComponents(
|
||||
url: string, group: string, artifact: string, version: string, classifier?: string, extension?: string
|
||||
): Promise<boolean> {
|
||||
return this.headArtifactBase(url,
|
||||
MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension))
|
||||
}
|
||||
|
||||
private async headArtifactBase(url: string, relative: string): Promise<boolean> {
|
||||
const resolvedURL = new URL(relative, url).toString()
|
||||
try {
|
||||
const response = await got.head({
|
||||
url: resolvedURL
|
||||
})
|
||||
return response.statusCode === 200
|
||||
} catch (ignored) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { BaseMavenRepo } from './BaseMavenRepo.js'
|
||||
|
||||
export class LibRepoStructure extends BaseMavenRepo {
|
||||
|
||||
public static readonly MINECRAFT_GROUP = 'net.minecraft'
|
||||
public static readonly MINECRAFT_CLIENT_ARTIFACT = 'client'
|
||||
|
||||
public static readonly FORGE_GROUP = 'net.minecraftforge'
|
||||
public static readonly FORGE_ARTIFACT = 'forge'
|
||||
public static readonly FMLCORE_ARTIFACT = 'fmlcore'
|
||||
public static readonly JAVAFMLLANGUAGE_ARTIFACT = 'javafmllanguage'
|
||||
public static readonly MCLANGUAGE_ARTIFACT = 'mclanguage'
|
||||
public static readonly LOWCODELANGUAGE_ARTIFACT = 'lowcodelanguage'
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, 'lib')
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
return 'LibRepoStructure'
|
||||
}
|
||||
|
||||
public getLocalForge(version: string, classifier?: string): string {
|
||||
return this.getArtifactByComponents(
|
||||
LibRepoStructure.FORGE_GROUP,
|
||||
LibRepoStructure.FORGE_ARTIFACT,
|
||||
version, classifier, 'jar')
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { mkdirs } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { BaseFileStructure } from '../BaseFileStructure.js'
|
||||
import { LibRepoStructure } from './LibRepo.struct.js'
|
||||
import { VersionRepoStructure } from './VersionRepo.struct.js'
|
||||
|
||||
export class RepoStructure extends BaseFileStructure {
|
||||
|
||||
private libRepoStruct: LibRepoStructure
|
||||
private versionRepoStruct: VersionRepoStructure
|
||||
|
||||
constructor(
|
||||
absoluteRoot: 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, name)
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
return 'RepoStructure'
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await super.init()
|
||||
await this.libRepoStruct.init()
|
||||
await this.versionRepoStruct.init()
|
||||
await mkdirs(this.getCacheDirectory())
|
||||
}
|
||||
|
||||
public getLibRepoStruct(): LibRepoStructure {
|
||||
return this.libRepoStruct
|
||||
}
|
||||
|
||||
public getVersionRepoStruct(): VersionRepoStructure {
|
||||
return this.versionRepoStruct
|
||||
}
|
||||
|
||||
public getTempDirectory(): string {
|
||||
return join(this.absoluteRoot, 'temp')
|
||||
}
|
||||
|
||||
public getWorkDirectory(): string {
|
||||
return join(this.absoluteRoot, 'work')
|
||||
}
|
||||
|
||||
public getCacheDirectory(): string {
|
||||
return join(this.absoluteRoot, 'cache')
|
||||
}
|
||||
|
||||
public getForgeCacheDirectory(artifactVersion: string): string {
|
||||
return join(this.getCacheDirectory(), 'forge', artifactVersion)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { join } from 'path'
|
||||
import { URL } from 'url'
|
||||
import { BaseFileStructure } from '../BaseFileStructure.js'
|
||||
import { MinecraftVersion } from '../../util/MinecraftVersion.js'
|
||||
|
||||
export class VersionRepoStructure extends BaseFileStructure {
|
||||
|
||||
private name: string
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
name: string
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, 'versions')
|
||||
this.name = name
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
return 'VersionRepoStructure'
|
||||
}
|
||||
|
||||
public getFileName(minecraftVersion: MinecraftVersion, loaderVersion: string): string {
|
||||
return `${minecraftVersion}-${this.name}-${loaderVersion}`
|
||||
}
|
||||
|
||||
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, loaderVersion: string): string {
|
||||
const fileName = this.getFileName(minecraftVersion, loaderVersion)
|
||||
return new URL(join(this.relativeRoot, fileName, `${fileName}.json`), url).toString()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { BaseFileStructure } from '../BaseFileStructure.js'
|
||||
import { SpecModelStructure } from './SpecModelStructure.js'
|
||||
|
||||
export abstract class BaseModelStructure<T> extends BaseFileStructure implements SpecModelStructure<T[]> {
|
||||
|
||||
protected resolvedModels: T[] | undefined
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
structRoot: string,
|
||||
protected baseUrl: string
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, structRoot)
|
||||
}
|
||||
|
||||
public abstract getSpecModel(): Promise<T[]>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { mkdirs, pathExists } from 'fs-extra/esm'
|
||||
import { readFile, writeFile } from 'fs/promises'
|
||||
import { Distribution } from 'helios-distribution-types'
|
||||
import { SpecModelStructure } from './SpecModelStructure.js'
|
||||
import { ServerStructure } from './Server.struct.js'
|
||||
import { join, resolve } from 'path'
|
||||
import { DistroMeta, getDefaultDistroMeta } from '../../model/nebula/DistroMeta.js'
|
||||
import { addSchemaToObject, SchemaTypes } from '../../util/SchemaUtil.js'
|
||||
import { LoggerUtil } from '../../util/LoggerUtil.js'
|
||||
|
||||
const logger = LoggerUtil.getLogger('DistributionStructure')
|
||||
|
||||
export class DistributionStructure implements SpecModelStructure<Distribution> {
|
||||
|
||||
private readonly DISTRO_META_FILE = 'distrometa.json'
|
||||
|
||||
private serverStruct: ServerStructure
|
||||
private metaPath: string
|
||||
|
||||
constructor(
|
||||
private absoluteRoot: string,
|
||||
private baseUrl: string,
|
||||
discardOutput: boolean,
|
||||
invalidateCache: boolean
|
||||
) {
|
||||
this.serverStruct = new ServerStructure(this.absoluteRoot, this.baseUrl, discardOutput, invalidateCache)
|
||||
this.metaPath = join(this.absoluteRoot, 'meta')
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await mkdirs(this.absoluteRoot)
|
||||
await mkdirs(this.metaPath)
|
||||
|
||||
const distroMetaFile = resolve(this.metaPath, this.DISTRO_META_FILE)
|
||||
if(await pathExists(distroMetaFile)) {
|
||||
logger.warn(`Distro Meta file already exists at ${distroMetaFile}!`)
|
||||
logger.warn('If you wish to regenerate this file, you must delete the existing one!')
|
||||
} else {
|
||||
const distroMeta: DistroMeta = addSchemaToObject(
|
||||
getDefaultDistroMeta(),
|
||||
SchemaTypes.DistroMetaSchema,
|
||||
this.absoluteRoot
|
||||
)
|
||||
await writeFile(distroMetaFile, JSON.stringify(distroMeta, null, 2))
|
||||
}
|
||||
|
||||
await this.serverStruct.init()
|
||||
}
|
||||
|
||||
public async getSpecModel(): Promise<Distribution> {
|
||||
|
||||
const distroMeta = JSON.parse(await readFile(resolve(this.metaPath, this.DISTRO_META_FILE), 'utf-8')) as DistroMeta
|
||||
|
||||
return {
|
||||
version: '1.0.0',
|
||||
rss: distroMeta.meta.rss,
|
||||
...(distroMeta.meta.discord ? {discord: distroMeta.meta.discord} : {}),
|
||||
servers: await this.serverStruct.getSpecModel()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { mkdirs, pathExists } from 'fs-extra/esm'
|
||||
import { lstat, readdir, readFile, writeFile } from 'fs/promises'
|
||||
import { Server, Module } from 'helios-distribution-types'
|
||||
import { dirname, join, resolve as resolvePath } from 'path'
|
||||
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 {
|
||||
modContainer?: string
|
||||
libraryContainer: string
|
||||
miscFileContainer: string
|
||||
}
|
||||
|
||||
export class ServerStructure extends BaseModelStructure<Server> {
|
||||
|
||||
private readonly ID_REGEX = /(.+-(.+)$)/
|
||||
private readonly SERVER_META_FILE = 'servermeta.json'
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
baseUrl: string,
|
||||
private discardOutput: boolean,
|
||||
private invalidateCache: boolean
|
||||
) {
|
||||
super(absoluteRoot, '', 'servers', baseUrl)
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
return 'ServerStructure'
|
||||
}
|
||||
|
||||
public async getSpecModel(): Promise<Server[]> {
|
||||
if (this.resolvedModels == null) {
|
||||
this.resolvedModels = await this._doSeverRetrieval()
|
||||
}
|
||||
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
|
||||
fabricVersion?: string
|
||||
}
|
||||
): 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 null
|
||||
}
|
||||
|
||||
await mkdirs(absoluteServerRoot)
|
||||
|
||||
const serverMetaOpts: ServerMetaOptions = {
|
||||
version: options.version
|
||||
}
|
||||
let modContainer: string | undefined = undefined
|
||||
|
||||
if (options.forgeVersion != null) {
|
||||
const fms = VersionSegmentedRegistry.getForgeModStruct(
|
||||
minecraftVersion,
|
||||
options.forgeVersion,
|
||||
absoluteServerRoot,
|
||||
relativeServerRoot,
|
||||
this.baseUrl,
|
||||
[]
|
||||
)
|
||||
await fms.init()
|
||||
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,
|
||||
this.absoluteRoot
|
||||
)
|
||||
await writeFile(resolvePath(absoluteServerRoot, this.SERVER_META_FILE), JSON.stringify(serverMeta, null, 2))
|
||||
|
||||
const libS = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, [])
|
||||
await libS.init()
|
||||
|
||||
const mfs = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, [])
|
||||
await mfs.init()
|
||||
|
||||
return {
|
||||
modContainer,
|
||||
libraryContainer: libS.getContainerDirectory(),
|
||||
miscFileContainer: mfs.getContainerDirectory()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async _doSeverRetrieval(): Promise<Server[]> {
|
||||
|
||||
const accumulator: Server[] = []
|
||||
const files = await readdir(this.containerDirectory)
|
||||
for (const file of files) {
|
||||
const absoluteServerRoot = resolvePath(this.containerDirectory, file)
|
||||
const relativeServerRoot = join(this.relativeRoot, file)
|
||||
if ((await lstat(absoluteServerRoot)).isDirectory()) {
|
||||
|
||||
this.logger.info(`Beginning processing of ${file}.`)
|
||||
|
||||
const match = this.ID_REGEX.exec(file)
|
||||
if (match == null) {
|
||||
this.logger.warn(`Server directory ${file} does not match the defined standard.`)
|
||||
this.logger.warn('All server ids must end with -<minecraft version> (ex. -1.12.2)')
|
||||
continue
|
||||
}
|
||||
|
||||
// Read server meta
|
||||
const serverMeta = JSON.parse(await readFile(resolvePath(absoluteServerRoot, this.SERVER_META_FILE), 'utf-8')) as ServerMeta
|
||||
const minecraftVersion = new MinecraftVersion(match[2])
|
||||
const untrackedFiles: UntrackedFilesOption[] = serverMeta.untrackedFiles || []
|
||||
|
||||
let iconUrl: string = null!
|
||||
|
||||
// Resolve server icon
|
||||
|
||||
if(serverMeta.meta.icon && isValidUrl(serverMeta.meta.icon)) {
|
||||
// Use the url they gave us.
|
||||
iconUrl = serverMeta.meta.icon
|
||||
} else {
|
||||
|
||||
this.logger.info('Server icon is either not set or not a valid URL.')
|
||||
this.logger.info(`Looking for an icon file at ${absoluteServerRoot}`)
|
||||
|
||||
const subFiles = await readdir(absoluteServerRoot)
|
||||
for (const subFile of subFiles) {
|
||||
const caseInsensitive = subFile.toLowerCase()
|
||||
if (caseInsensitive.endsWith('.jpg') || caseInsensitive.endsWith('.png')) {
|
||||
iconUrl = new URL(join(relativeServerRoot, subFile), this.baseUrl).toString()
|
||||
}
|
||||
}
|
||||
|
||||
if (!iconUrl) {
|
||||
this.logger.warn(`No icon file found for server ${file}.`)
|
||||
}
|
||||
}
|
||||
|
||||
const modules: Module[] = []
|
||||
|
||||
if(serverMeta.forge) {
|
||||
const forgeResolver = VersionSegmentedRegistry.getForgeResolver(
|
||||
minecraftVersion,
|
||||
serverMeta.forge.version,
|
||||
dirname(this.containerDirectory),
|
||||
'',
|
||||
this.baseUrl,
|
||||
this.discardOutput,
|
||||
this.invalidateCache
|
||||
)
|
||||
|
||||
// Resolve forge
|
||||
const forgeItselfModule = await forgeResolver.getModule()
|
||||
modules.push(forgeItselfModule)
|
||||
|
||||
const forgeModStruct = VersionSegmentedRegistry.getForgeModStruct(
|
||||
minecraftVersion,
|
||||
serverMeta.forge.version,
|
||||
absoluteServerRoot,
|
||||
relativeServerRoot,
|
||||
this.baseUrl,
|
||||
untrackedFiles
|
||||
)
|
||||
|
||||
const forgeModModules = await forgeModStruct.getSpecModel()
|
||||
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)
|
||||
|
||||
const fileStruct = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, untrackedFiles)
|
||||
const fileModules = await fileStruct.getSpecModel()
|
||||
modules.push(...fileModules)
|
||||
|
||||
accumulator.push({
|
||||
id: match[1],
|
||||
name: serverMeta.meta.name,
|
||||
description: serverMeta.meta.description,
|
||||
icon: iconUrl,
|
||||
version: serverMeta.meta.version,
|
||||
address: serverMeta.meta.address,
|
||||
minecraftVersion: match[2],
|
||||
...(serverMeta.meta.discord ? {discord: serverMeta.meta.discord} : {}),
|
||||
mainServer: serverMeta.meta.mainServer,
|
||||
autoconnect: serverMeta.meta.autoconnect,
|
||||
javaOptions: serverMeta.meta.javaOptions,
|
||||
modules
|
||||
})
|
||||
|
||||
} else {
|
||||
this.logger.warn(`Path ${file} in server directory is not a directory!`)
|
||||
}
|
||||
}
|
||||
return accumulator
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { FileStructure } from '../FileStructure.js'
|
||||
|
||||
export interface SpecModelStructure<T> extends FileStructure {
|
||||
|
||||
getSpecModel(): Promise<T>
|
||||
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Stats } from 'fs'
|
||||
import { Type, Module } from 'helios-distribution-types'
|
||||
import { URL } from 'url'
|
||||
import { ModuleStructure } from './Module.struct.js'
|
||||
import { readdir, stat } from 'fs/promises'
|
||||
import { join, resolve, sep } from 'path'
|
||||
import { MinecraftVersion } from '../../../util/MinecraftVersion.js'
|
||||
import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js'
|
||||
|
||||
export class MiscFileStructure extends ModuleStructure {
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
minecraftVersion: MinecraftVersion,
|
||||
untrackedFiles: UntrackedFilesOption[]
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, 'files', baseUrl, minecraftVersion, Type.File, untrackedFiles)
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
return 'MiscFileStructure'
|
||||
}
|
||||
|
||||
public async getSpecModel(): Promise<Module[]> {
|
||||
if (this.resolvedModels == null) {
|
||||
this.resolvedModels = await this.recursiveModuleScan(this.containerDirectory)
|
||||
}
|
||||
|
||||
return this.resolvedModels
|
||||
}
|
||||
|
||||
protected async recursiveModuleScan(dir: string): Promise<Module[]> {
|
||||
let acc: Module[] = []
|
||||
const subdirs = await readdir(dir)
|
||||
for (const file of subdirs) {
|
||||
const filePath = resolve(dir, file)
|
||||
const stats = await stat(filePath)
|
||||
if (stats.isDirectory()) {
|
||||
acc = acc.concat(await this.recursiveModuleScan(filePath))
|
||||
} else {
|
||||
if(!this.FILE_NAME_BLACKLIST.includes(file)) {
|
||||
acc.push(await this.parseModule(file, filePath, stats))
|
||||
}
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
protected async getModuleId(name: string, path: string): Promise<string> {
|
||||
return name
|
||||
}
|
||||
protected async getModuleName(name: string, path: string): Promise<string> {
|
||||
return name
|
||||
}
|
||||
protected async getModuleUrl(name: string, path: string, stats: Stats): Promise<string> {
|
||||
return new URL(join(this.relativeRoot, ...path.substr(this.containerDirectory.length+1).split(sep)), this.baseUrl).toString()
|
||||
}
|
||||
protected async getModulePath(name: string, path: string, stats: Stats): Promise<string | null> {
|
||||
return path.substr(this.containerDirectory.length+1).replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Type } from 'helios-distribution-types'
|
||||
import { VersionSegmented } from '../../../util/VersionSegmented.js'
|
||||
import { MinecraftVersion } from '../../../util/MinecraftVersion.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<T> extends BaseModStructure<T> implements VersionSegmented {
|
||||
|
||||
protected readonly EXAMPLE_MOD_ID = 'examplemod'
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
minecraftVersion: MinecraftVersion,
|
||||
untrackedFiles: UntrackedFilesOption[]
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, 'forgemods', baseUrl, minecraftVersion, Type.ForgeMod, untrackedFiles)
|
||||
}
|
||||
|
||||
public abstract isForVersion(version: MinecraftVersion, libraryVersion: string): boolean
|
||||
|
||||
protected getClaritasExceptions(): ClaritasException[] {
|
||||
return [{
|
||||
exceptionName: 'optifine',
|
||||
proxyMetadata: {
|
||||
group: 'net.optifine'
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
protected getClaritasType(): LibraryType {
|
||||
return LibraryType.FORGE
|
||||
}
|
||||
|
||||
protected discernResult(claritasValue: string | undefined, crudeInference: string): string {
|
||||
return (claritasValue == null || claritasValue == '') ? crudeInference : claritasValue
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ModuleStructure } from './Module.struct.js'
|
||||
import { Type, TypeMetadata } from 'helios-distribution-types'
|
||||
import { Stats } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { URL } from 'url'
|
||||
import { MinecraftVersion } from '../../../util/MinecraftVersion.js'
|
||||
import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js'
|
||||
|
||||
export class LibraryStructure extends ModuleStructure {
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
minecraftVersion: MinecraftVersion,
|
||||
untrackedFiles: UntrackedFilesOption[]
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, 'libraries', baseUrl, minecraftVersion, Type.Library, untrackedFiles, (name: string) => {
|
||||
return name.toLowerCase().endsWith(TypeMetadata[this.type].defaultExtension!)
|
||||
})
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
return 'LibraryStructure'
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected async getModuleId(name: string, path: string): Promise<string> {
|
||||
const inference = this.attemptCrudeInference(name)
|
||||
return this.generateMavenIdentifier(this.getDefaultGroup(), inference.name, inference.version)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected async getModuleName(name: string, path: string): Promise<string> {
|
||||
const inference = this.attemptCrudeInference(name)
|
||||
return inference.name
|
||||
}
|
||||
// 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, 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { minimatch } from 'minimatch'
|
||||
import { createHash } from 'crypto'
|
||||
import { pathExists } from 'fs-extra/esm'
|
||||
import { Stats } from 'fs'
|
||||
import { lstat, readdir, readFile } from 'fs/promises'
|
||||
import { Artifact, Module, Type, TypeMetadata } from 'helios-distribution-types'
|
||||
import { resolve } from 'path'
|
||||
import { BaseModelStructure } from '../BaseModel.struct.js'
|
||||
import { LibraryType } from '../../../model/claritas/ClaritasLibraryType.js'
|
||||
import { ClaritasResult, ClaritasModuleMetadata } from '../../../model/claritas/ClaritasResult.js'
|
||||
import { ClaritasWrapper } from '../../../util/java/ClaritasWrapper.js'
|
||||
import { MinecraftVersion } from '../../../util/MinecraftVersion.js'
|
||||
import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js'
|
||||
|
||||
export interface ModuleCandidate {
|
||||
file: string
|
||||
filePath: string
|
||||
stats: Stats
|
||||
}
|
||||
|
||||
export interface ClaritasException {
|
||||
exceptionName: string
|
||||
proxyMetadata: ClaritasModuleMetadata
|
||||
}
|
||||
|
||||
export abstract class ModuleStructure extends BaseModelStructure<Module> {
|
||||
|
||||
private readonly crudeRegex = /(.+?)-(.+).[jJ][aA][rR]/
|
||||
protected readonly DEFAULT_VERSION = '0.0.0'
|
||||
protected readonly FILE_NAME_BLACKLIST = [
|
||||
'.gitkeep'
|
||||
]
|
||||
|
||||
protected untrackedFilePatterns: string[] // List of glob patterns.
|
||||
protected claritasResult!: ClaritasResult
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
structRoot: string,
|
||||
baseUrl: string,
|
||||
protected minecraftVersion: MinecraftVersion,
|
||||
protected type: Type,
|
||||
untrackedFiles: UntrackedFilesOption[],
|
||||
protected filter?: ((name: string, path: string, stats: Stats) => boolean)
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, structRoot, baseUrl)
|
||||
this.untrackedFilePatterns = this.determineUntrackedFiles(structRoot, untrackedFiles)
|
||||
}
|
||||
|
||||
public async getSpecModel(): Promise<Module[]> {
|
||||
if (this.resolvedModels == null) {
|
||||
this.resolvedModels = await this._doModuleRetrieval(await this._doModuleDiscovery(this.containerDirectory))
|
||||
}
|
||||
|
||||
return this.resolvedModels
|
||||
}
|
||||
|
||||
protected getDefaultGroup(): string {
|
||||
return `generated.${this.type.toLowerCase()}`
|
||||
}
|
||||
|
||||
protected generateMavenIdentifier(group: string, id: string, version: string): string {
|
||||
return `${group}:${id}:${version}@${TypeMetadata[this.type].defaultExtension}`
|
||||
}
|
||||
|
||||
protected attemptCrudeInference(name: string): { name: string, version: string } {
|
||||
const result = this.crudeRegex.exec(name)
|
||||
if(result != null) {
|
||||
return {
|
||||
name: result[1],
|
||||
version: result[2]
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
name: name.substring(0, name.lastIndexOf('.')),
|
||||
version: this.DEFAULT_VERSION
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getClaritasGroup(path: string): string {
|
||||
return this.claritasResult[path]?.group || this.getDefaultGroup()
|
||||
}
|
||||
|
||||
protected getClaritasExceptions(): ClaritasException[] {
|
||||
return []
|
||||
}
|
||||
|
||||
protected getClaritasType(): LibraryType | null {
|
||||
return null
|
||||
}
|
||||
|
||||
protected abstract getModuleId(name: string, path: string): Promise<string>
|
||||
protected abstract getModuleName(name: string, path: string): Promise<string>
|
||||
protected abstract getModuleUrl(name: string, path: string, stats: Stats): Promise<string>
|
||||
protected abstract getModulePath(name: string, path: string, stats: Stats): Promise<string | null>
|
||||
|
||||
protected async parseModule(file: string, filePath: string, stats: Stats): Promise<Module> {
|
||||
|
||||
const artifact: Artifact = {
|
||||
size: stats.size,
|
||||
url: await this.getModuleUrl(file, filePath, stats)
|
||||
}
|
||||
|
||||
const relativeToContainer = filePath.substr(this.containerDirectory.length+1)
|
||||
const untrackedByPattern = this.isFileUntracked(relativeToContainer)
|
||||
if(!untrackedByPattern) {
|
||||
const buf = await readFile(filePath)
|
||||
artifact.MD5 = createHash('md5').update(buf).digest('hex')
|
||||
} else {
|
||||
this.logger.debug(`File ${relativeToContainer} is untracked. Matching pattern: ${untrackedByPattern}`)
|
||||
}
|
||||
|
||||
const mdl: Module = {
|
||||
id: await this.getModuleId(file, filePath),
|
||||
name: await this.getModuleName(file, filePath),
|
||||
type: this.type,
|
||||
artifact
|
||||
}
|
||||
const pth = await this.getModulePath(file, filePath, stats)
|
||||
if (pth) {
|
||||
mdl.artifact.path = pth
|
||||
}
|
||||
return mdl
|
||||
}
|
||||
|
||||
protected async _doModuleDiscovery(scanDirectory: string): Promise<ModuleCandidate[]> {
|
||||
|
||||
const moduleCandidates: ModuleCandidate[] = []
|
||||
|
||||
if (await pathExists(scanDirectory)) {
|
||||
const files = await readdir(scanDirectory)
|
||||
for (const file of files) {
|
||||
const filePath = resolve(scanDirectory, file)
|
||||
const stats = await lstat(filePath)
|
||||
if (stats.isFile()) {
|
||||
if(!this.FILE_NAME_BLACKLIST.includes(file)) {
|
||||
if(this.filter == null || this.filter(file, filePath, stats)) {
|
||||
moduleCandidates.push({file, filePath, stats})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moduleCandidates
|
||||
|
||||
}
|
||||
|
||||
protected async invokeClaritas(moduleCandidates: ModuleCandidate[]): Promise<void> {
|
||||
if(this.getClaritasType() != null) {
|
||||
const claritasExecutor = new ClaritasWrapper(this.absoluteRoot)
|
||||
|
||||
let claritasCandidates = moduleCandidates
|
||||
const exceptionCandidates: [ModuleCandidate, ClaritasException][] = []
|
||||
for(const exception of this.getClaritasExceptions()) {
|
||||
const exceptionCandidate = moduleCandidates.find((value) => value.file.toLowerCase().includes(exception.exceptionName))
|
||||
if(exceptionCandidate != null) {
|
||||
exceptionCandidates.push([exceptionCandidate, exception])
|
||||
claritasCandidates = claritasCandidates.filter((value) => !value.file.toLowerCase().includes(exception.exceptionName))
|
||||
}
|
||||
}
|
||||
|
||||
this.claritasResult = await claritasExecutor.execute(
|
||||
this.getClaritasType()!,
|
||||
this.minecraftVersion,
|
||||
claritasCandidates.map(entry => entry.filePath)
|
||||
)
|
||||
|
||||
if(this.claritasResult == null) {
|
||||
this.logger.error('Failed to process Claritas result!')
|
||||
} else {
|
||||
for(const [candidate, exception] of exceptionCandidates) {
|
||||
this.claritasResult[candidate.filePath] = exception.proxyMetadata
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async _doModuleRetrieval(moduleCandidates: ModuleCandidate[], options?: {
|
||||
preProcess?: (candidate: ModuleCandidate) => void
|
||||
postProcess?: (module: Module) => void
|
||||
}): Promise<Module[]> {
|
||||
|
||||
const accumulator: Module[] = []
|
||||
|
||||
if(moduleCandidates.length > 0) {
|
||||
|
||||
// Invoke Claritas and attach result to class.
|
||||
await this.invokeClaritas(moduleCandidates)
|
||||
|
||||
// Process Modules
|
||||
for(const candidate of moduleCandidates) {
|
||||
options?.preProcess?.(candidate)
|
||||
const mdl = await this.parseModule(candidate.file, candidate.filePath, candidate.stats)
|
||||
options?.postProcess?.(mdl)
|
||||
accumulator.push(mdl)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return accumulator
|
||||
|
||||
}
|
||||
|
||||
protected determineUntrackedFiles(targetStructRoot: string, untrackedFileOptions?: UntrackedFilesOption[]): string[] {
|
||||
if(untrackedFileOptions) {
|
||||
return untrackedFileOptions
|
||||
.filter(x => x.appliesTo.includes(targetStructRoot))
|
||||
.reduce((acc, cur) => acc.concat(cur.patterns), [] as string[])
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// Will return the matching pattern, undefined if no match.
|
||||
protected isFileUntracked(pathRelativeToContainer: string): string | undefined {
|
||||
return this.untrackedFilePatterns.find(pattern => minimatch(pathRelativeToContainer, pattern))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { ModuleStructure, ModuleCandidate } from './Module.struct.js'
|
||||
import { Type, Module } from 'helios-distribution-types'
|
||||
import { mkdirs } from 'fs-extra/esm'
|
||||
import { Stats } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import { MinecraftVersion } from '../../../util/MinecraftVersion.js'
|
||||
import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js'
|
||||
|
||||
export enum ToggleableNamespace {
|
||||
|
||||
REQUIRED = 'required',
|
||||
OPTIONAL_ON = 'optionalon',
|
||||
OPTIONAL_OFF = 'optionaloff'
|
||||
|
||||
}
|
||||
|
||||
export interface ToggleableModuleCandidate extends ModuleCandidate {
|
||||
namespace: ToggleableNamespace
|
||||
}
|
||||
|
||||
export abstract class ToggleableModuleStructure extends ModuleStructure {
|
||||
|
||||
private activeNamespace: string | undefined
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
structRoot: string,
|
||||
baseUrl: string,
|
||||
minecraftVersion: MinecraftVersion,
|
||||
type: Type,
|
||||
untrackedFiles: UntrackedFilesOption[],
|
||||
filter?: ((name: string, path: string, stats: Stats) => boolean)
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, structRoot, baseUrl, minecraftVersion, type, untrackedFiles, filter)
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await super.init()
|
||||
for(const namespace of Object.values(ToggleableNamespace)) {
|
||||
await mkdirs(resolve(this.containerDirectory, namespace))
|
||||
}
|
||||
}
|
||||
|
||||
public async getSpecModel(): Promise<Module[]> {
|
||||
if (this.resolvedModels == null) {
|
||||
|
||||
const moduleCandidates: ToggleableModuleCandidate[] = []
|
||||
for(const value of Object.values(ToggleableNamespace)) {
|
||||
moduleCandidates.push(...(await super._doModuleDiscovery(resolve(this.containerDirectory, value))).map(val => ({...val, namespace: value})))
|
||||
}
|
||||
|
||||
this.resolvedModels = await this._doModuleRetrieval(moduleCandidates, {
|
||||
preProcess: (candidate) => {
|
||||
this.activeNamespace = (candidate as ToggleableModuleCandidate).namespace
|
||||
},
|
||||
postProcess: (module) => {
|
||||
this.getNamespaceMapper(this.activeNamespace as ToggleableNamespace)(module)
|
||||
}
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
this.activeNamespace = undefined
|
||||
|
||||
}
|
||||
|
||||
return this.resolvedModels
|
||||
}
|
||||
|
||||
protected getActiveNamespace(): string {
|
||||
return this.activeNamespace || ''
|
||||
}
|
||||
|
||||
protected getNamespaceMapper(namespace: ToggleableNamespace): (x: Module) => void {
|
||||
switch(namespace) {
|
||||
case ToggleableNamespace.REQUIRED:
|
||||
return (): void => { /* do nothing */ }
|
||||
case ToggleableNamespace.OPTIONAL_ON:
|
||||
return (x): void => { x.required = { value: false } }
|
||||
case ToggleableNamespace.OPTIONAL_OFF:
|
||||
return (x): void => { x.required = { value: false, def: false } }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import StreamZip from 'node-stream-zip'
|
||||
import toml from 'toml'
|
||||
import { capitalize } from '../../../../util/StringUtils.js'
|
||||
import { VersionUtil } from '../../../../util/VersionUtil.js'
|
||||
import { ModsToml } from '../../../../model/forge/ModsToml.js'
|
||||
import { BaseForgeModStructure } from '../ForgeMod.struct.js'
|
||||
import { MinecraftVersion } from '../../../../util/MinecraftVersion.js'
|
||||
import { UntrackedFilesOption } from '../../../../model/nebula/ServerMeta.js'
|
||||
|
||||
export class ForgeModStructure113 extends BaseForgeModStructure<ModsToml> {
|
||||
|
||||
public static readonly IMPLEMENTATION_VERSION_REGEX = /^Implementation-Version: (.+)[\r\n]/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
|
||||
return VersionUtil.isVersionAcceptable(version, [13, 14, 15, 16, 17, 18, 19, 20, 21])
|
||||
}
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
minecraftVersion: MinecraftVersion,
|
||||
untrackedFiles: UntrackedFilesOption[]
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, untrackedFiles)
|
||||
}
|
||||
|
||||
public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
|
||||
return ForgeModStructure113.isForVersion(version, libraryVersion)
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
return 'ForgeModStructure (1.13)'
|
||||
}
|
||||
|
||||
protected async getModuleId(name: string, path: string): Promise<string> {
|
||||
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.getModMetadata(name, path)).mods[0].displayName)
|
||||
}
|
||||
|
||||
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
|
||||
// account for.
|
||||
if (name.toLowerCase().includes('optifine')) {
|
||||
|
||||
// Read zip for changelog.txt
|
||||
let changelogBuf: Buffer
|
||||
try {
|
||||
changelogBuf = zip.entryDataSync('changelog.txt')
|
||||
} catch(err) {
|
||||
throw new Error('Failed to read OptiFine changelog.')
|
||||
}
|
||||
|
||||
const info = changelogBuf.toString().split('\n')[0].trim()
|
||||
const version = info.split(' ')[1]
|
||||
|
||||
this.modMetadata[name] = ({
|
||||
modLoader: 'javafml',
|
||||
loaderVersion: '',
|
||||
mods: [{
|
||||
modId: 'optifine',
|
||||
version,
|
||||
displayName: 'OptiFine',
|
||||
description: `OptiFine is a Minecraft optimization mod.
|
||||
It allows Minecraft to run faster and look better with full support for shaders, HD textures and many configuration options.`
|
||||
}]
|
||||
})
|
||||
|
||||
return this.modMetadata[name]
|
||||
}
|
||||
|
||||
let raw: Buffer | undefined
|
||||
try {
|
||||
raw = zip.entryDataSync('META-INF/mods.toml')
|
||||
} catch(err) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = toml.parse(raw.toString()) as ModsToml
|
||||
this.modMetadata[name] = parsed
|
||||
} catch (err) {
|
||||
this.logger.error(`ForgeMod ${name} contains an invalid mods.toml file.`)
|
||||
}
|
||||
} else {
|
||||
this.logger.error(`ForgeMod ${name} does not contain mods.toml file.`)
|
||||
}
|
||||
|
||||
const cRes = this.claritasResult?.[path]
|
||||
|
||||
if(cRes == null) {
|
||||
this.logger.error(`Claritas failed to yield metadata for ForgeMod ${name}!`)
|
||||
this.logger.error('Is this mod malformatted or does Claritas need an update?')
|
||||
}
|
||||
|
||||
const claritasId = cRes?.id
|
||||
|
||||
const crudeInference = this.attemptCrudeInference(name)
|
||||
|
||||
if(this.modMetadata[name] != null) {
|
||||
|
||||
const x = this.modMetadata[name]
|
||||
for(const entry of x.mods) {
|
||||
|
||||
if(entry.modId === this.EXAMPLE_MOD_ID) {
|
||||
entry.modId = this.discernResult(claritasId, crudeInference.name.toLowerCase())
|
||||
entry.displayName = crudeInference.name
|
||||
}
|
||||
|
||||
if (entry.version === '${file.jarVersion}') {
|
||||
let version = crudeInference.version
|
||||
try {
|
||||
const manifest = zip.entryDataSync('META-INF/MANIFEST.MF')
|
||||
const keys = manifest.toString().split('\n')
|
||||
// this.logger.debug(keys)
|
||||
for (const key of keys) {
|
||||
const match = ForgeModStructure113.IMPLEMENTATION_VERSION_REGEX.exec(key)
|
||||
if (match != null) {
|
||||
version = match[1]
|
||||
}
|
||||
}
|
||||
this.logger.debug(`ForgeMod ${name} contains a version wildcard, inferring ${version}`)
|
||||
} catch {
|
||||
this.logger.debug(`ForgeMod ${name} contains a version wildcard yet no MANIFEST.MF.. Defaulting to ${version}`)
|
||||
}
|
||||
entry.version = version
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
this.modMetadata[name] = ({
|
||||
modLoader: 'javafml',
|
||||
loaderVersion: '',
|
||||
mods: [{
|
||||
modId: this.discernResult(claritasId, crudeInference.name.toLowerCase()),
|
||||
version: crudeInference.version,
|
||||
displayName: crudeInference.name,
|
||||
description: ''
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
return this.modMetadata[name]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import StreamZip from 'node-stream-zip'
|
||||
import { capitalize } from '../../../../util/StringUtils.js'
|
||||
import { VersionUtil } from '../../../../util/VersionUtil.js'
|
||||
import { McModInfo } from '../../../../model/forge/McModInfo.js'
|
||||
import { McModInfoList } from '../../../../model/forge/McModInfoList.js'
|
||||
import { BaseForgeModStructure } from '../ForgeMod.struct.js'
|
||||
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<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])
|
||||
}
|
||||
|
||||
constructor(
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
minecraftVersion: MinecraftVersion,
|
||||
untrackedFiles: UntrackedFilesOption[]
|
||||
) {
|
||||
super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, untrackedFiles)
|
||||
}
|
||||
|
||||
public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean {
|
||||
return ForgeModStructure17.isForVersion(version, libraryVersion)
|
||||
}
|
||||
|
||||
public getLoggerName(): string {
|
||||
return 'ForgeModStructure (1.7)'
|
||||
}
|
||||
|
||||
protected async getModuleId(name: string, path: string): Promise<string> {
|
||||
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.getModMetadata(name, path)).name)
|
||||
}
|
||||
|
||||
private isMalformedVersion(version: string): boolean {
|
||||
// Ex. empty, @VERSION@, ${version}
|
||||
return version.trim().length === 0 || version.includes('@') || version.includes('$')
|
||||
}
|
||||
|
||||
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.
|
||||
if (name.toLowerCase().includes('optifine')) {
|
||||
|
||||
// Read zip for changelog.txt
|
||||
let changelogBuf: Buffer
|
||||
try {
|
||||
changelogBuf = zip.entryDataSync('changelog.txt')
|
||||
} catch(err) {
|
||||
throw new Error('Failed to read OptiFine changelog.')
|
||||
}
|
||||
|
||||
const info = changelogBuf.toString().split('\n')[0].trim()
|
||||
const version = info.split(' ')[1]
|
||||
this.modMetadata[name] = ({
|
||||
modid: 'optifine',
|
||||
name: info,
|
||||
version,
|
||||
mcversion: version.substring(0, version.indexOf('_'))
|
||||
}) as McModInfo
|
||||
return this.modMetadata[name]
|
||||
}
|
||||
|
||||
let raw: Buffer | undefined
|
||||
try {
|
||||
raw = zip.entryDataSync('mcmod.info')
|
||||
} catch(err) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
// Assuming the main mod will be the first entry in this file.
|
||||
try {
|
||||
const resolved = JSON.parse(raw.toString()) as (McModInfoList | McModInfo[])
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(resolved, 'modListVersion')) {
|
||||
this.modMetadata[name] = (resolved as McModInfoList).modList[0]
|
||||
} else {
|
||||
this.modMetadata[name] = (resolved as McModInfo[])[0]
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
this.logger.error(`ForgeMod ${name} contains an invalid mcmod.info file.`)
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(`ForgeMod ${name} does not contain mcmod.info file.`)
|
||||
}
|
||||
|
||||
const cRes = this.claritasResult[path]
|
||||
|
||||
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?')
|
||||
} else {
|
||||
switch(cRes.modType!) {
|
||||
case ForgeModType_1_7.CORE_MOD:
|
||||
this.logger.info(`CORE_MOD Discovered: ForgeMod ${name} has no @Mod annotation. Metadata inference capabilities are limited.`)
|
||||
break
|
||||
case ForgeModType_1_7.TWEAKER:
|
||||
this.logger.info(`TWEAKER Discovered: ForgeMod ${name} has no @Mod annotation. Metadata inference capabilities may be limited.`)
|
||||
break
|
||||
case ForgeModType_1_7.UNKNOWN:
|
||||
this.logger.error(`Jar file ${name} is not a ForgeMod. Is it a library?`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const claritasId = cRes?.id
|
||||
const claritasVersion = cRes?.version
|
||||
const claritasName = cRes?.name
|
||||
|
||||
|
||||
// Validate
|
||||
const crudeInference = this.attemptCrudeInference(name)
|
||||
if(this.modMetadata[name] != null) {
|
||||
|
||||
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.modMetadata[name].version != null) {
|
||||
const isMalformedVersion = this.isMalformedVersion(this.modMetadata[name].version)
|
||||
if(isMalformedVersion) {
|
||||
x.version = this.discernResult(claritasVersion, crudeInference.version)
|
||||
}
|
||||
} else {
|
||||
x.version = this.discernResult(claritasVersion, crudeInference.version)
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
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.modMetadata[name]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createLogger, format, transports, Logger } from 'winston'
|
||||
import { SPLAT } from 'triple-beam'
|
||||
import { DateTime } from 'luxon'
|
||||
import { inspect } from 'util'
|
||||
|
||||
export class LoggerUtil {
|
||||
|
||||
public static getLogger(label: string): Logger {
|
||||
return createLogger({
|
||||
format: format.combine(
|
||||
format.label(),
|
||||
format.colorize(),
|
||||
format.label({ label }),
|
||||
format.printf(infoUnknown => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const info = infoUnknown as {[key: string | symbol]: any}
|
||||
if(info[SPLAT]) {
|
||||
if(info[SPLAT].length === 1 && info[SPLAT][0] instanceof Error) {
|
||||
const err: Error = info[SPLAT][0]
|
||||
if(info.message.length > err.message.length && info.message.endsWith(err.message)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
info.message = info.message.substring(0, info.message.length-err.message.length)
|
||||
}
|
||||
} else if(info[SPLAT].length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
info.message += ' ' + info[SPLAT].map((it: any) => {
|
||||
if(typeof it === 'object' && it != null) {
|
||||
return inspect(it, false, 4, true)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return it
|
||||
}).join(' ')
|
||||
}
|
||||
}
|
||||
if(typeof info.message === 'object') {
|
||||
info.message = inspect(info.message, false, 4, true)
|
||||
}
|
||||
return `[${DateTime.local().toFormat('yyyy-MM-dd TT').trim()}] [${info.level}] [${info.label}]: ${info.message}${info.stack ? `\n${info.stack}` : ''}`
|
||||
})
|
||||
),
|
||||
level: 'debug',
|
||||
transports: [
|
||||
new transports.Console()
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { normalize } from 'path'
|
||||
import { URL } from 'url'
|
||||
|
||||
export interface MavenComponents {
|
||||
group: string
|
||||
artifact: string
|
||||
version: string
|
||||
classifier?: string
|
||||
extension: string
|
||||
}
|
||||
|
||||
export class MavenUtil {
|
||||
|
||||
public static readonly ID_REGEX = /([^@:]+):([^@:]+):?([^@:]+)?:?(?:([^@:]+))?:?(?:@{1}([^@:]+))?/
|
||||
|
||||
public static mavenComponentsToIdentifier(
|
||||
group: string,
|
||||
artifact: string,
|
||||
version: string,
|
||||
classifier?: string,
|
||||
extension?: string
|
||||
): string {
|
||||
return `${group}:${artifact}:${version}${classifier != null ? `:${classifier}` : ''}${extension != null ? `@${extension}` : ''}`
|
||||
}
|
||||
|
||||
public static mavenComponentsToExtensionlessIdentifier(
|
||||
group: string,
|
||||
artifact: string,
|
||||
version: string,
|
||||
classifier?: string
|
||||
): string {
|
||||
return MavenUtil.mavenComponentsToIdentifier(group, artifact, version, classifier)
|
||||
}
|
||||
|
||||
public static mavenComponentsToVersionlessIdentifier(
|
||||
group: string,
|
||||
artifact: string
|
||||
): string {
|
||||
return `${group}:${artifact}`
|
||||
}
|
||||
|
||||
public static isMavenIdentifier(id: string): boolean {
|
||||
return MavenUtil.ID_REGEX.test(id)
|
||||
}
|
||||
|
||||
public static getMavenComponents(id: string, extension = 'jar'): MavenComponents {
|
||||
if (!MavenUtil.isMavenIdentifier(id)) {
|
||||
throw new Error('Id is not a maven identifier.')
|
||||
}
|
||||
|
||||
const result = MavenUtil.ID_REGEX.exec(id)
|
||||
|
||||
if (result != null) {
|
||||
return {
|
||||
group: result[1],
|
||||
artifact: result[2],
|
||||
version: result[3],
|
||||
classifier: result[4],
|
||||
extension: result[5] || extension
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to process maven data.')
|
||||
}
|
||||
|
||||
public static mavenIdentifierAsPath(id: string, extension = 'jar'): string {
|
||||
const tmp = MavenUtil.getMavenComponents(id, extension)
|
||||
|
||||
return MavenUtil.mavenComponentsAsPath(
|
||||
tmp.group, tmp.artifact, tmp.version, tmp.classifier, tmp.extension
|
||||
)
|
||||
}
|
||||
|
||||
public static mavenComponentsAsPath(
|
||||
group: string, artifact: string, version: string, classifier?: string, extension = 'jar'
|
||||
): string {
|
||||
return `${group.replace(/\./g, '/')}/${artifact}/${version}/${artifact}-${version}${classifier != null ? `-${classifier}` : ''}.${extension}`
|
||||
}
|
||||
|
||||
public static mavenIdentifierToUrl(id: string, extension = 'jar'): URL {
|
||||
return new URL(MavenUtil.mavenIdentifierAsPath(id, extension))
|
||||
}
|
||||
|
||||
public static mavenComponentsToUrl(
|
||||
group: string, artifact: string, version: string, classifier?: string, extension = 'jar'
|
||||
): URL {
|
||||
return new URL(MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension))
|
||||
}
|
||||
|
||||
public static mavenIdentifierToPath(id: string, extension = 'jar'): string {
|
||||
return normalize(MavenUtil.mavenIdentifierAsPath(id, extension))
|
||||
}
|
||||
|
||||
public static mavenComponentsAsNormalizedPath(
|
||||
group: string, artifact: string, version: string, classifier?: string, extension = 'jar'
|
||||
): string {
|
||||
return normalize(MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export class MetadataUtil {
|
||||
|
||||
public static completeGroupInference(partial: string, id: string): string {
|
||||
|
||||
const bits = partial.split('.')
|
||||
|
||||
let isBadTerm = true
|
||||
while(isBadTerm && bits.length > 2) {
|
||||
const term = bits[bits.length-1]
|
||||
if(term !== id) {
|
||||
isBadTerm = false
|
||||
} else {
|
||||
bits.pop()
|
||||
}
|
||||
}
|
||||
|
||||
return bits.join('.')
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export class MinecraftVersion {
|
||||
|
||||
private static readonly MINECRAFT_VERSION_REGEX = /(\d+).(\d+).?(\d+)?/
|
||||
|
||||
private readonly major: number
|
||||
private readonly minor: number
|
||||
private readonly revision: number | undefined
|
||||
|
||||
constructor(version: string) {
|
||||
const res = MinecraftVersion.MINECRAFT_VERSION_REGEX.exec(version)
|
||||
if(res != null) {
|
||||
this.major = Number(res[1])
|
||||
this.minor = Number(res[2])
|
||||
this.revision = res[3] != null ? Number(res[3]) : undefined
|
||||
} else {
|
||||
throw new Error(`${version} is not a valid minecraft version!`)
|
||||
}
|
||||
}
|
||||
|
||||
public static isMinecraftVersion(version: string): boolean {
|
||||
return MinecraftVersion.MINECRAFT_VERSION_REGEX.test(version)
|
||||
}
|
||||
|
||||
public getMajor(): number { return this.major }
|
||||
public getMinor(): number { return this.minor }
|
||||
public getRevision(): number | undefined { return this.revision }
|
||||
|
||||
public toString(): string { return `${this.major}.${this.minor}${this.revision != null ? '.' + this.revision : ''}`}
|
||||
|
||||
public compareTo(other: MinecraftVersion): number {
|
||||
// Compare major
|
||||
if (this.major !== other.major) {
|
||||
return this.major - other.major
|
||||
}
|
||||
|
||||
// Compare minor
|
||||
if (this.minor !== other.minor) {
|
||||
return this.minor - other.minor
|
||||
}
|
||||
|
||||
// Compare revision (null as 0)
|
||||
const thisRevision = this.revision ?? 0
|
||||
const otherRevision = other.revision ?? 0
|
||||
return thisRevision - otherRevision
|
||||
}
|
||||
|
||||
public isGreaterThanOrEqualTo(other: MinecraftVersion): boolean {
|
||||
return this.compareTo(other) >= 0
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { mkdirs, pathExists, remove } from 'fs-extra/esm'
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { join, resolve } from 'path'
|
||||
import { createGenerator } from 'ts-json-schema-generator'
|
||||
import { URL, fileURLToPath } from 'url'
|
||||
import { DistroMeta } from '../model/nebula/DistroMeta.js'
|
||||
import { ServerMeta } from '../model/nebula/ServerMeta.js'
|
||||
import { LoggerUtil } from './LoggerUtil.js'
|
||||
|
||||
const logger = LoggerUtil.getLogger('SchemaUtil')
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
interface SchemaType {
|
||||
/**
|
||||
* URL to the JSON schema for this type of file.
|
||||
* This is used by editors to validate and annotate the data.
|
||||
*/
|
||||
$schema?: string
|
||||
}
|
||||
|
||||
export type DistroMetaSchema = DistroMeta & SchemaType
|
||||
export type ServerMetaSchema = ServerMeta & SchemaType
|
||||
|
||||
export enum SchemaTypes {
|
||||
DistroMetaSchema = 'DistroMetaSchema',
|
||||
ServerMetaSchema = 'ServerMetaSchema'
|
||||
}
|
||||
|
||||
function getSchemaFileName(typeName: string): string {
|
||||
return `${typeName}.schema.json`
|
||||
}
|
||||
|
||||
function getSchemaDirectory(absoluteRoot: string): string {
|
||||
return resolve(absoluteRoot, 'schemas')
|
||||
}
|
||||
|
||||
function getSchemaLocation(typeName: string, absoluteRoot: string): string {
|
||||
return resolve(getSchemaDirectory(absoluteRoot), getSchemaFileName(typeName))
|
||||
}
|
||||
|
||||
export function addSchemaToObject<T>(obj: T, typeName: string, absoluteRoot: string): T {
|
||||
return {
|
||||
$schema: new URL(`file:${getSchemaLocation(typeName, absoluteRoot)}`).href,
|
||||
...obj
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateSchemas(absoluteRoot: string): Promise<void> {
|
||||
|
||||
const selfPath = __filename.replace('dist', 'src').replace('.js', '.ts')
|
||||
|
||||
const schemaDir = getSchemaDirectory(absoluteRoot)
|
||||
if(await pathExists(schemaDir)) {
|
||||
await remove(schemaDir)
|
||||
}
|
||||
await mkdirs(schemaDir)
|
||||
|
||||
for(const typeName of Object.values(SchemaTypes)) {
|
||||
|
||||
logger.info(`Generating schema for ${typeName}`)
|
||||
|
||||
const schema = createGenerator({
|
||||
tsconfig: join(__dirname, '..', '..', 'tsconfig.json'),
|
||||
path: selfPath,
|
||||
type: typeName
|
||||
}).createSchema(typeName)
|
||||
|
||||
const schemaString = JSON.stringify(schema)
|
||||
const schemaLoc = getSchemaLocation(typeName, absoluteRoot)
|
||||
await writeFile(schemaLoc, schemaString)
|
||||
|
||||
logger.info(`Schema for ${typeName} saved to ${schemaLoc}`)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function capitalize(str: string): string {
|
||||
if (!str) {
|
||||
return str
|
||||
}
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
export function isValidUrl(candidate: string): boolean {
|
||||
try {
|
||||
new URL(candidate)
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { MinecraftVersion } from './MinecraftVersion.js'
|
||||
|
||||
export interface VersionSegmented {
|
||||
|
||||
isForVersion(version: MinecraftVersion, libraryVersion: string): boolean
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ForgeModStructure113 } from '../structure/spec_model/module/forgemod/ForgeMod113.struct.js'
|
||||
import { ForgeModStructure17 } from '../structure/spec_model/module/forgemod/ForgeMod17.struct.js'
|
||||
import { ForgeGradle3Adapter } from '../resolver/forge/adapter/ForgeGradle3.resolver.js'
|
||||
import { ForgeGradle2Adapter } from '../resolver/forge/adapter/ForgeGradle2.resolver.js'
|
||||
import { ForgeResolver } from '../resolver/forge/Forge.resolver.js'
|
||||
import { BaseForgeModStructure } from '../structure/spec_model/module/ForgeMod.struct.js'
|
||||
import { MinecraftVersion } from './MinecraftVersion.js'
|
||||
import { UntrackedFilesOption } from '../model/nebula/ServerMeta.js'
|
||||
|
||||
export class VersionSegmentedRegistry {
|
||||
|
||||
public static readonly FORGE_ADAPTER_IMPL = [
|
||||
ForgeGradle2Adapter,
|
||||
ForgeGradle3Adapter
|
||||
]
|
||||
|
||||
public static readonly FORGEMOD_STRUCT_IMPL = [
|
||||
ForgeModStructure17,
|
||||
ForgeModStructure113
|
||||
]
|
||||
|
||||
public static getForgeResolver(
|
||||
minecraftVersion: MinecraftVersion,
|
||||
forgeVersion: string,
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseURL: string,
|
||||
discardOutput: boolean,
|
||||
invalidateCache: boolean
|
||||
): ForgeResolver {
|
||||
for (const impl of VersionSegmentedRegistry.FORGE_ADAPTER_IMPL) {
|
||||
if (impl.isForVersion(minecraftVersion, forgeVersion)) {
|
||||
return new impl(absoluteRoot, relativeRoot, baseURL, minecraftVersion, forgeVersion, discardOutput, invalidateCache)
|
||||
}
|
||||
}
|
||||
throw new Error(`No forge resolver found for Minecraft ${minecraftVersion}!`)
|
||||
}
|
||||
|
||||
public static getForgeModStruct(
|
||||
minecraftVersion: MinecraftVersion,
|
||||
forgeVersion: string,
|
||||
absoluteRoot: string,
|
||||
relativeRoot: string,
|
||||
baseUrl: string,
|
||||
untrackedFiles: UntrackedFilesOption[]
|
||||
): BaseForgeModStructure<unknown> {
|
||||
for (const impl of VersionSegmentedRegistry.FORGEMOD_STRUCT_IMPL) {
|
||||
if (impl.isForVersion(minecraftVersion, forgeVersion)) {
|
||||
return new impl(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, untrackedFiles)
|
||||
}
|
||||
}
|
||||
throw new Error(`No forge mod structure found for Minecraft ${minecraftVersion}!`)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
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 {
|
||||
|
||||
private static readonly logger = LoggerUtil.getLogger('VersionUtil')
|
||||
|
||||
public static readonly PROMOTION_TYPE = [
|
||||
'recommended',
|
||||
'latest'
|
||||
]
|
||||
|
||||
public static isVersionAcceptable(version: MinecraftVersion, acceptable: number[]): boolean {
|
||||
if (version.getMajor() === 1) {
|
||||
return acceptable.find((element) => version.getMinor() === element) != null
|
||||
}
|
||||
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.includes(version.toLowerCase())
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// Forge
|
||||
|
||||
public static isOneDotTwelveFG2(libraryVersion: string): boolean {
|
||||
const maxFG2 = [14, 23, 5, 2847]
|
||||
const verSplit = libraryVersion.split('.').map(v => Number(v))
|
||||
|
||||
for(let i=0; i<maxFG2.length; i++) {
|
||||
if(verSplit[i] > maxFG2[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public static async getPromotionIndex(): Promise<PromotionsSlim> {
|
||||
const response = await got.get<PromotionsSlim>({
|
||||
method: 'get',
|
||||
url: 'https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json',
|
||||
responseType: 'json'
|
||||
})
|
||||
return response.body
|
||||
}
|
||||
|
||||
public static getPromotedVersionStrict(index: PromotionsSlim, minecraftVersion: MinecraftVersion, promotion: string): string {
|
||||
const workingPromotion = promotion.toLowerCase()
|
||||
return index.promos[`${minecraftVersion}-${workingPromotion}`]
|
||||
}
|
||||
|
||||
public static async getPromotedForgeVersion(minecraftVersion: MinecraftVersion, promotion: string): Promise<string> {
|
||||
const workingPromotion = promotion.toLowerCase()
|
||||
const res = await VersionUtil.getPromotionIndex()
|
||||
let version = res.promos[`${minecraftVersion}-${workingPromotion}`]
|
||||
if (version == null) {
|
||||
VersionUtil.logger.warn(`No ${workingPromotion} version found for Forge ${minecraftVersion}.`)
|
||||
VersionUtil.logger.warn('Attempting to pull latest version instead.')
|
||||
version = res.promos[`${minecraftVersion}-latest`]
|
||||
if (version == null) {
|
||||
throw new Error(`No latest version found for Forge ${minecraftVersion}.`)
|
||||
}
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// Fabric
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { JarExecutor } from './JarExecutor.js'
|
||||
import { join, resolve } from 'path'
|
||||
import { ClaritasResult } from '../../model/claritas/ClaritasResult.js'
|
||||
import { MinecraftVersion } from '../MinecraftVersion.js'
|
||||
import { LibraryType } from '../../model/claritas/ClaritasLibraryType.js'
|
||||
import { pathExists, remove, mkdirs } from 'fs-extra/esm'
|
||||
import { readFile, writeFile } from 'fs/promises'
|
||||
|
||||
export class ClaritasWrapper extends JarExecutor<ClaritasResult> {
|
||||
|
||||
private readonly WORK_DIR: string
|
||||
private readonly ARG_FILE: string
|
||||
private readonly OUTPUT_FILE: string
|
||||
|
||||
constructor(cwd: string) {
|
||||
super('Claritas')
|
||||
|
||||
this.WORK_DIR = resolve(cwd, 'claritasWork')
|
||||
this.ARG_FILE = resolve(this.WORK_DIR, 'claritasArgFile.txt')
|
||||
this.OUTPUT_FILE = resolve(this.WORK_DIR, 'claritasOutput.json')
|
||||
|
||||
this.onCloseListeners.push(async (code) => {
|
||||
if(code !== 0) {
|
||||
this.logger.error('Claritas finished with non-zero exit code, ', code)
|
||||
this.lastExecutionResult = undefined!
|
||||
} else {
|
||||
if(await pathExists(this.OUTPUT_FILE)) {
|
||||
this.lastExecutionResult = JSON.parse((await readFile(this.OUTPUT_FILE)).toString('utf8')) as ClaritasResult
|
||||
} else {
|
||||
this.logger.error('Claritas output file not found when exit code is 0, is this a bug?')
|
||||
this.lastExecutionResult = undefined!
|
||||
}
|
||||
}
|
||||
await this.cleanOutput()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
protected getJarPath(): string {
|
||||
return join(process.cwd(), 'libraries', 'java', 'Claritas.jar')
|
||||
}
|
||||
|
||||
private async writeArgFile(...programArgs: string[]): Promise<void> {
|
||||
await mkdirs(this.WORK_DIR)
|
||||
await writeFile(
|
||||
this.ARG_FILE,
|
||||
programArgs.join('\n')
|
||||
)
|
||||
}
|
||||
|
||||
public async execute(libraryType: LibraryType, mcVersion: MinecraftVersion, absoluteJarPaths: string[]): Promise<ClaritasResult> {
|
||||
await this.writeArgFile(
|
||||
'--absoluteJarPaths', absoluteJarPaths.join(','),
|
||||
'--libraryType', libraryType,
|
||||
'--mcVersion', mcVersion.toString(),
|
||||
'--outputFile', this.OUTPUT_FILE,
|
||||
'--previewOutput', 'true'
|
||||
)
|
||||
return await super.executeJar(
|
||||
[`-Dclaritas.argFile=${this.ARG_FILE}`]
|
||||
)
|
||||
}
|
||||
|
||||
private async cleanOutput(): Promise<void> {
|
||||
if(await pathExists(this.WORK_DIR)) {
|
||||
await remove(this.WORK_DIR)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { JavaUtil } from './JavaUtil.js'
|
||||
import { Logger } from 'winston'
|
||||
import { spawn } from 'child_process'
|
||||
import { LoggerUtil } from '../LoggerUtil.js'
|
||||
|
||||
export abstract class JarExecutor<T> {
|
||||
|
||||
protected readonly logger: Logger
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected stdoutListeners: ((chunk: any) => void)[] = []
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected stderrListeners: ((chunk: any) => void)[] = []
|
||||
protected onCloseListeners: ((code: number | null) => Promise<void>)[] = []
|
||||
|
||||
protected lastExecutionResult!: T
|
||||
|
||||
protected constructor(loggerName: string) {
|
||||
this.logger = LoggerUtil.getLogger(loggerName)
|
||||
}
|
||||
|
||||
protected abstract getJarPath(): string
|
||||
|
||||
protected executeJar(vmOptions: string[], ...args: string[]): Promise<T> {
|
||||
this.lastExecutionResult = undefined!
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(JavaUtil.getJavaExecutable(), [
|
||||
...vmOptions,
|
||||
'-jar',
|
||||
this.getJarPath(),
|
||||
...args
|
||||
])
|
||||
child.stdout.on('data', (data) => this.logger.info(data.toString('utf8').trim()))
|
||||
this.stdoutListeners.forEach(l => child.stdout.on('data', l))
|
||||
|
||||
child.stderr.on('data', (data) => this.logger.error(data.toString('utf8').trim()))
|
||||
this.stderrListeners.forEach(l => child.stderr.on('data', l))
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
child.on('close', async code => {
|
||||
this.logger.info('Exited with code', code)
|
||||
for(const l of this.onCloseListeners) {
|
||||
await l(code)
|
||||
}
|
||||
resolve(this.lastExecutionResult)
|
||||
})
|
||||
|
||||
child.on('error', (err) => {
|
||||
this.logger.info('Error during process execution', err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class JavaUtil {
|
||||
|
||||
public static getJavaExecutable(): string {
|
||||
return process.env.JAVA_EXECUTABLE!
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { join } from 'path'
|
||||
import { JarExecutor } from './JarExecutor.js'
|
||||
|
||||
export class PackXZExtractWrapper extends JarExecutor<void> {
|
||||
|
||||
constructor() {
|
||||
super('PackXZExtract')
|
||||
}
|
||||
|
||||
protected getJarPath(): string {
|
||||
return join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar')
|
||||
}
|
||||
|
||||
protected execute(command: string, paths: string[]): Promise<void> {
|
||||
return super.executeJar([], command, paths.join(','))
|
||||
}
|
||||
|
||||
public extractUnpack(paths: string[]): Promise<void> {
|
||||
return this.execute('-packxz', paths)
|
||||
}
|
||||
|
||||
public extract(paths: string[]): Promise<void> {
|
||||
return this.execute('-xz', paths)
|
||||
}
|
||||
|
||||
public unpack(paths: string[]): Promise<void> {
|
||||
return this.execute('-pack', paths)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user