SurviePote 2/4: atlas Nebula (serveur SurviePote-26.3, distribution.json, sans binaires)

This commit is contained in:
kisakay
2026-10-18 18:44:09 +02:00
parent 14355ac2ad
commit ccaee11472
69 changed files with 8289 additions and 0 deletions
+48
View File
@@ -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()
]
})
}
}
+100
View File
@@ -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))
}
}
+21
View File
@@ -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('.')
}
}
+51
View File
@@ -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
}
}
+77
View File
@@ -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}`)
}
}
+15
View File
@@ -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
}
}
+7
View File
@@ -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}!`)
}
}
+139
View File
@@ -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
}
}
+70
View File
@@ -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)
}
}
}
+55
View File
@@ -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)
})
})
}
}
+7
View File
@@ -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)
}
}