Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Fabric #66

Merged
merged 10 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ Options:
* OPTIONAL (default: null)
* If not provided forge will not be enabled.
* You can provide either `latest` or `recommended` to use the latest/recommended version of forge.
* `--fabric <string>` Specify fabric loader version
* OPTIONAL (default: null)
* If not provided fabric will not be enabled.
* You can provide either `latest` or `recommended` to use the latest/recommended version of fabric.

>
> Example Usage
Expand Down Expand Up @@ -227,6 +231,7 @@ Ex.
* `files` All modules of type `File`.
* `libraries` All modules of type `Library`
* `forgemods` All modules of type `ForgeMod`.
* `fabricmods` All modules of type `FabricMod`.
* This is a directory of toggleable modules. See the note below.
* `TestServer-1.12.2.png` Server icon file.

Expand Down
25 changes: 21 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,16 +174,22 @@ const generateServerCommand: CommandModule = {
})
.option('forge', {
describe: 'Forge version.',
type: 'string',
default: null
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└ Forge version: ${argv.forge}`,
`\n\t└ Fabric version: ${argv.fabric}`
)

const minecraftVersion = new MinecraftVersion(argv.version as string)

Expand All @@ -196,12 +202,22 @@ const generateServerCommand: CommandModule = {
}
}

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
forgeVersion: argv.forge as string,
fabricVersion: argv.fabric as string
}
)
}
Expand Down Expand Up @@ -234,6 +250,7 @@ const generateServerCurseForgeCommand: CommandModule = {
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}`)
Expand Down
49 changes: 49 additions & 0 deletions src/model/fabric/FabricMeta.ts
Original file line number Diff line number Diff line change
@@ -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
}[]
}
13 changes: 13 additions & 0 deletions src/model/fabric/FabricModJson.ts
Original file line number Diff line number Diff line change
@@ -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[] }

}
19 changes: 19 additions & 0 deletions src/model/nebula/ServerMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface UntrackedFilesOption {
export interface ServerMetaOptions {
version?: string
forgeVersion?: string
fabricVersion?: string
}

export function getDefaultServerMeta(id: string, version: string, options?: ServerMetaOptions): ServerMeta {
Expand Down Expand Up @@ -43,6 +44,13 @@ export function getDefaultServerMeta(id: string, version: string, options?: Serv
}
}

if(options?.fabricVersion) {
servMeta.meta.description = `${servMeta.meta.description} (Fabric v${options.fabricVersion})`
servMeta.fabric = {
version: options.fabricVersion
}
}

// Add empty untracked files.
servMeta.untrackedFiles = []

Expand Down Expand Up @@ -77,6 +85,17 @@ export interface ServerMeta {
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.
*/
Expand Down
6 changes: 3 additions & 3 deletions src/parser/CurseForgeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ export class CurseForgeParser {
await zip.close()
}

if(createServerResult.forgeModContainer) {
const requiredPath = resolve(createServerResult.forgeModContainer, ToggleableNamespace.REQUIRED)
const optionalPath = resolve(createServerResult.forgeModContainer, ToggleableNamespace.OPTIONAL_ON)
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 }[] = []

Expand Down
12 changes: 11 additions & 1 deletion src/resolver/BaseResolver.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Module } from 'helios-distribution-types'
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 {

Expand All @@ -14,4 +16,12 @@ export abstract class BaseResolver implements Resolver, VersionSegmented {
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
}
}

}
123 changes: 123 additions & 0 deletions src/resolver/fabric/Fabric.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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

// eslint-disable-next-line @typescript-eslint/no-unused-vars
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

}

}
13 changes: 1 addition & 12 deletions src/resolver/forge/Forge.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import StreamZip from 'node-stream-zip'
import { createHash } from 'crypto'
import { Stats } from 'fs'
import { Artifact } from 'helios-distribution-types'
import { RepoStructure } from '../../structure/repo/Repo.struct.js'
import { BaseResolver } from '../BaseResolver.js'
import { MinecraftVersion } from '../../util/MinecraftVersion.js'
Expand All @@ -26,7 +23,7 @@ export abstract class ForgeResolver extends BaseResolver {
protected invalidateCache: boolean
) {
super(absoluteRoot, relativeRoot, baseUrl)
this.repoStructure = new RepoStructure(absoluteRoot, relativeRoot)
this.repoStructure = new RepoStructure(absoluteRoot, relativeRoot, 'forge')
this.artifactVersion = this.inferArtifactVersion()
this.checkSecurity()
}
Expand Down Expand Up @@ -133,14 +130,6 @@ export abstract class ForgeResolver extends BaseResolver {
return version
}

protected generateArtifact(buf: Buffer, stats: Stats, url: string): Artifact {
return {
size: stats.size,
MD5: createHash('md5').update(buf).digest('hex'),
url
}
}

protected async getVersionManifestFromJar(jarPath: string): Promise<Buffer>{
return new Promise((resolve, reject) => {
const zip = new StreamZip({
Expand Down
2 changes: 1 addition & 1 deletion src/structure/FileStructure.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface FileStructure {

init(): void
init(): Promise<void>

}
5 changes: 3 additions & 2 deletions src/structure/repo/Repo.struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ export class RepoStructure extends BaseFileStructure {

constructor(
absoluteRoot: string,
relativeRoot: 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)
this.versionRepoStruct = new VersionRepoStructure(this.containerDirectory, this.relativeRoot, name)
}

public getLoggerName(): string {
Expand Down
Loading