SurviePote 2/4: atlas Nebula (serveur SurviePote-26.3, distribution.json, sans binaires)
This commit is contained in:
@@ -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]
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user