Skip to content

Commit

Permalink
feat: support Fabric (#66)
Browse files Browse the repository at this point in the history
* Initial scaffolding for Fabric.

* refactor: extract common ModStructure

* feat: add FabricModStructure

* refactor: add name field to VersionRepoStructure

* feat: FabricResolver

* feat: support Fabric

* docs: update README

* Small changes.

* Add additional note.

* Upgrade helios-distribution-types.

---------

Co-authored-by: Daniel Scalzi <d_scalzi@yahoo.com>
  • Loading branch information
jebibot and dscalzi authored Dec 3, 2023
1 parent 8b52895 commit 76ab09a
Show file tree
Hide file tree
Showing 22 changed files with 564 additions and 181 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ 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.

> [!NOTE]
> Forge and fabric cannot be used together on the same server. This command will fail if both are provided.
>
> Example Usage
Expand Down Expand Up @@ -227,6 +234,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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"dotenv": "^16.3.1",
"fs-extra": "^11.1.1",
"got": "^13.0.0",
"helios-distribution-types": "^1.2.0",
"helios-distribution-types": "^1.3.0",
"luxon": "^3.4.3",
"minimatch": "^9.0.3",
"node-stream-zip": "^1.15.0",
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

}

}
Loading

0 comments on commit 76ab09a

Please sign in to comment.