diff --git a/README.md b/README.md index b89876e..084bdf0 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ Can be automatically retrieved from the config file of your mod: - `fabric.mod.json` (Fabric) - - Custom `mc-publish` field *(recommended)*: + - Custom `mc-publish` field: ```json { // ... @@ -179,7 +179,7 @@ Can be automatically retrieved from the config file of your mod: } ``` - - Custom [`modmanager`](https://github.com/DeathsGun/ModManager) field *(recommended)*: + - Custom [`modmanager`](https://github.com/DeathsGun/ModManager) field: ```json { // ... @@ -191,28 +191,6 @@ Can be automatically retrieved from the config file of your mod: } ``` - - Custom `projects` field: - ```json - { - // ... - "custom": { - "projects": { - "modrinth": "AANobbMI" - } - }, - } - ``` - - - `projects` field: - ```json - { - // ... - "projects": { - "modrinth": "AANobbMI" - }, - } - ``` - - `mods.toml` (Forge) - Custom `mc-publish` field *(recommended)*: @@ -307,7 +285,7 @@ Can be automatically retrieved from the config file of your mod: - `fabric.mod.json` (Fabric) - - Custom `mc-publish` field *(recommended)*: + - Custom `mc-publish` field: ```json { // ... @@ -319,7 +297,7 @@ Can be automatically retrieved from the config file of your mod: } ``` - - Custom [`modmanager`](https://github.com/DeathsGun/ModManager) field *(recommended)*: + - Custom [`modmanager`](https://github.com/DeathsGun/ModManager) field: ```json { // ... @@ -331,28 +309,6 @@ Can be automatically retrieved from the config file of your mod: } ``` - - Custom `projects` field: - ```json - { - // ... - "custom": { - "projects": { - "curseforge": 394468 - } - }, - } - ``` - - - `projects` field: - ```json - { - // ... - "projects": { - "curseforge": 394468 - }, - } - ``` - - `mods.toml` (Forge) - Custom `mc-publish` field *(recommended)*: diff --git a/src/metadata/fabric/fabric-mod-config.ts b/src/metadata/fabric/fabric-mod-config.ts new file mode 100644 index 0000000..29600f8 --- /dev/null +++ b/src/metadata/fabric/fabric-mod-config.ts @@ -0,0 +1,57 @@ +import type DependencyKind from "../dependency-kind"; + +type Environment = "client" | "server" | "*"; + +type Person = string | { name: string; contact?: string; }; + +type Dependency = string | string[] | { + version?: string | string[]; + versions?: string | string[]; + custom?: Record<string, any>; +}; + +type DependencyContainer = { + // Dependency resolution + [Kind in keyof typeof DependencyKind as Lowercase<Kind>]?: Record<string, Dependency>; +}; + +// https://fabricmc.net/wiki/documentation:fabric_mod_json +type FabricModConfig = { + // Mandatory fields + schemaVersion: 1; + id: string; + version: string; + + // Mod loading + provides?: string; + environment?: Environment; + entrypoints?: Record<string, string[]>; + jars?: { file: string }[]; + languageAdapters?: Record<string, string>; + mixins?: (string | { config: string, environment: Environment })[]; + + // Metadata + name?: string; + description?: string; + contact?: { + email?: string; + irc?: string; + homepage?: string; + issues?: string; + sources?: string; + [key: string]: string; + }; + authors?: Person[]; + contributors?: Person[]; + license?: string | string[]; + icon?: string | Record<string, string>; + + // Custom fields + custom?: Record<string, any>; +} & DependencyContainer; + +namespace FabricModConfig { + export const FILENAME = "fabric.mod.json"; +} + +export default FabricModConfig; diff --git a/src/metadata/fabric/fabric-mod-metadata-reader.ts b/src/metadata/fabric/fabric-mod-metadata-reader.ts index fbe4a6a..147847d 100644 --- a/src/metadata/fabric/fabric-mod-metadata-reader.ts +++ b/src/metadata/fabric/fabric-mod-metadata-reader.ts @@ -1,17 +1,20 @@ -import ModMetadata from "../../metadata/mod-metadata"; -import ZippedModMetadataReader from "../../metadata/zipped-mod-metadata-reader"; +import ModMetadata from "../mod-metadata"; +import ZippedModMetadataReader from "../zipped-mod-metadata-reader"; +import FabricModConfig from "./fabric-mod-config"; import FabricModMetadata from "./fabric-mod-metadata"; -export default class FabricModMetadataReader extends ZippedModMetadataReader { +class FabricModMetadataReader extends ZippedModMetadataReader<FabricModConfig> { constructor() { - super("fabric.mod.json"); + super(FabricModConfig.FILENAME); } - protected loadConfig(buffer: Buffer): Record<string, unknown> { + protected loadConfig(buffer: Buffer): FabricModConfig { return JSON.parse(buffer.toString("utf8")); } - protected createMetadataFromConfig(config: Record<string, unknown>): ModMetadata { + protected createMetadataFromConfig(config: FabricModConfig): ModMetadata { return new FabricModMetadata(config); } } + +export default FabricModMetadataReader; diff --git a/src/metadata/fabric/fabric-mod-metadata.ts b/src/metadata/fabric/fabric-mod-metadata.ts index c411171..9cc4da5 100644 --- a/src/metadata/fabric/fabric-mod-metadata.ts +++ b/src/metadata/fabric/fabric-mod-metadata.ts @@ -1,76 +1,158 @@ import action from "../../../package.json"; -import ModConfig from "../../metadata/mod-config"; -import ModConfigDependency from "../../metadata/mod-config-dependency"; -import Dependency from "../../metadata/dependency"; -import DependencyKind from "../../metadata/dependency-kind"; +import Dependency from "../dependency"; +import DependencyKind from "../dependency-kind"; import PublisherTarget from "../../publishing/publisher-target"; +import FabricModConfig from "./fabric-mod-config"; +import ModMetadata from "../mod-metadata"; -const ignoredByDefault = ["minecraft", "java", "fabricloader"]; -const aliases = new Map([ - ["fabric", "fabric-api"] -]); -function getDependenciesByKind(config: any, kind: DependencyKind): Dependency[] { - const kindName = DependencyKind.toString(kind).toLowerCase(); +type Aliases = Map<PublisherTarget, string>; + +type FabricDependency = FabricModConfig["depends"][string]; + +function getDependencies(config: FabricModConfig): Dependency[] { + return DependencyKind.getValues().flatMap(x => getDependenciesByKind(config, x)); +} + +function getDependenciesByKind(config: FabricModConfig, kind: DependencyKind): Dependency[] { + const kindName = DependencyKind.toString(kind).toLowerCase() as Lowercase<keyof typeof DependencyKind>; const dependencies = new Array<Dependency>(); for (const [id, value] of Object.entries(config[kindName] || {})) { - const ignore = ignoredByDefault.includes(id); - if (typeof value === "string") { - const dependencyAliases = aliases.has(id) ? new Map(PublisherTarget.getValues().map(x => [x, aliases.get(id)])) : null; - dependencies.push(Dependency.create({ id, kind, version: value, ignore, aliases: dependencyAliases })); - } else { - const dependencyMetadata = { ignore, ...<any>value, id, kind }; - if (aliases.has(id)) { - if (!dependencyMetadata.custom) { - dependencyMetadata.custom = {}; - } - if (!dependencyMetadata.custom[action.name]) { - dependencyMetadata.custom[action.name] = {}; - } - for (const target of PublisherTarget.getValues()) { - const targetName = PublisherTarget.toString(target).toLowerCase(); - if (typeof dependencyMetadata.custom[action.name][targetName] !== "string") { - dependencyMetadata.custom[action.name][targetName] = aliases.get(id); - } - } - } - dependencies.push(new ModConfigDependency(dependencyMetadata)); - } + const ignore = isDependencyIgnoredByDefault(id); + const aliases = getDefaultDependencyAliases(id); + const dependency = parseDependency(id, kind, value, ignore, aliases, config); + + dependencies.push(dependency); } return dependencies; } -function getLoaders(config: any): string[] { - if (config[action.name]?.quilt ?? config.custom?.[action.name]?.quilt) { +function parseDependency(id: string, kind: DependencyKind, body: FabricDependency, ignore: boolean, aliases: Aliases, config: FabricModConfig): Dependency { + if (typeof body === "string" || Array.isArray(body)) { + return parseSimpleDependency(id, kind, body, ignore, aliases, config); + } + + let version = body.version || body.versions || "*"; + if (Array.isArray(version)) { + version = version.join(" || "); + } + + kind = getDependencyKind(id, config) ?? kind; + ignore = isDependencyIgnoredInConfig(id, config) ?? body.custom?.[action.name]?.ignore ?? ignore; + aliases = new Map([ ...(aliases || []), ...(getDependencyAliases(id, config) || []) ]); + for (const target of PublisherTarget.getValues()) { + const targetName = PublisherTarget.toString(target).toLowerCase(); + const alias = body.custom?.[action.name]?.[targetName]; + if (alias) { + aliases.set(target, String(alias)); + } + } + + return Dependency.create({ id, kind, version, ignore, aliases }); +} + +function parseSimpleDependency(id: string, kind: DependencyKind, version: string | string[], ignore: boolean, aliases: Aliases, config: FabricModConfig): Dependency { + if (Array.isArray(version)) { + version = version.join(" || "); + } + + kind = getDependencyKind(id, config) ?? kind; + ignore = isDependencyIgnoredInConfig(id, config) ?? ignore; + aliases = new Map([ ...(aliases || []), ...(getDependencyAliases(id, config) || []) ]); + return Dependency.create({ id, kind, version, ignore, aliases }); +} + +const ignoredByDefault = [ + "minecraft", + "java", + "fabricloader", +]; +function isDependencyIgnoredByDefault(id: string): boolean { + return ignoredByDefault.includes(id); +} + +function isDependencyIgnoredInConfig(id: string, config: FabricModConfig): boolean | null { + return config.custom?.[action.name]?.dependencies?.[id]?.ignore; +} + +const defaultAliases = new Map<string, string | Aliases>([ + ["fabric", "fabric-api"], +]); +function getDefaultDependencyAliases(id: string): Aliases | null { + if (!defaultAliases.has(id)) { + return null; + } + + const aliases = defaultAliases.get(id); + if (typeof aliases !== "string") { + return new Map([...aliases]); + } + + return new Map(PublisherTarget.getValues().map(x => [x, aliases])); +} + +function getDependencyAliases(id: string, config: FabricModConfig): Aliases | null { + const metadata = config.custom?.[action.name]?.dependencies?.[id]; + if (!metadata) { + return null; + } + + const aliases = new Map() as Aliases; + for (const target of PublisherTarget.getValues()) { + const targetName = PublisherTarget.toString(target).toLowerCase(); + const alias = metadata[targetName] ?? id; + aliases.set(target, String(alias)); + } + return aliases; +} + +function getDependencyKind(id: string, config: FabricModConfig): DependencyKind | null { + const kind = config.custom?.[action.name]?.dependencies?.[id]?.kind; + return kind ? DependencyKind.parse(kind) : null; +} + +function getLoaders(config: FabricModConfig): string[] { + if (config.custom?.[action.name]?.quilt) { return ["fabric", "quilt"]; } return ["fabric"]; } -export default class FabricModMetadata extends ModConfig { - public readonly id: string; - public readonly name: string; - public readonly version: string; - public readonly loaders: string[]; - public readonly dependencies: Dependency[]; +function getProjects(config: FabricModConfig): Map<PublisherTarget, string> { + const projects = new Map(); + for (const target of PublisherTarget.getValues()) { + const targetName = PublisherTarget.toString(target).toLowerCase(); + const projectId = config.custom?.[action.name]?.[targetName] + ?? config.custom?.modmanager?.[targetName] + ?? config.custom?.projects?.[targetName]; - constructor(config: Record<string, unknown>) { - super(config); - this.id = String(this.config.id ?? ""); - this.name = String(this.config.name ?? this.id); - this.version = String(this.config.version ?? "*"); - this.loaders = getLoaders(this.config); - this.dependencies = DependencyKind.getValues().flatMap(x => getDependenciesByKind(this.config, x)); + if (projectId) { + projects.set(target, String(projectId)); + } + } + return projects; +} + +class FabricModMetadata implements ModMetadata { + readonly id: string; + readonly name: string; + readonly version: string; + readonly loaders: string[]; + readonly dependencies: Dependency[]; + + private readonly _projects: Map<PublisherTarget, string>; + + constructor(config: FabricModConfig) { + this.id = String(config.id ?? ""); + this.name = String(config.name ?? this.id); + this.version = String(config.version ?? "*"); + this.loaders = getLoaders(config); + this.dependencies = getDependencies(config); + this._projects = getProjects(config); } getProjectId(project: PublisherTarget): string | undefined { - const projectId = super.getProjectId(project); - if (projectId) { - return projectId; - } - - const projectName = PublisherTarget.toString(project).toLowerCase(); - const custom = <any>this.config.custom; - const modManagerProjectId = custom?.modmanager?.[projectName]?.id ?? custom?.modmanager?.[projectName]; - return modManagerProjectId === undefined ? modManagerProjectId : String(modManagerProjectId); + return this._projects.get(project); } } + +export default FabricModMetadata; diff --git a/test/content/fabric/fabric.mod.json b/test/content/fabric/fabric.mod.json index 7659725..197eef5 100644 --- a/test/content/fabric/fabric.mod.json +++ b/test/content/fabric/fabric.mod.json @@ -23,10 +23,10 @@ "example-mod.mixins.json" ], - "projects": { - "modrinth": "AANobbMI" - }, "custom": { + "modmanager": { + "modrinth": "AANobbMI" + }, "projects": { "curseforge": 394468 }, @@ -38,20 +38,19 @@ "depends": { "fabricloader": ">=0.11.3", "fabric": ">=0.40.0", - "minecraft": "1.17.x", + "minecraft": [ + "1.17", + "1.17.1" + ], "java": ">=16" }, "recommends": { "recommended-mod": { "version": "0.2.0", - "projects": { - "modrinth": "AAAA" - }, "custom": { - "projects": { - "curseforge": 42 - }, "mc-publish": { + "modrinth": "AAAA", + "curseforge": 42, "github": "v0.2.0", "ignore": true } diff --git a/test/unit-tests/metadata/mod-metadata-reader.test.ts b/test/unit-tests/metadata/mod-metadata-reader.test.ts index 0fcb720..56add9d 100644 --- a/test/unit-tests/metadata/mod-metadata-reader.test.ts +++ b/test/unit-tests/metadata/mod-metadata-reader.test.ts @@ -65,6 +65,12 @@ describe("ModMetadataReader.readMetadata", () => { } }); + test("version array is supported", async () => { + const metadata = (await ModMetadataReader.readMetadata("example-mod.fabric.jar"))!; + const minecraft = metadata.dependencies.find(x => x.id === "minecraft"); + expect(minecraft.version).toStrictEqual("1.17 || 1.17.1"); + }); + test("custom metadata can be attached to dependency entry", async () => { const metadata = (await ModMetadataReader.readMetadata("example-mod.fabric.jar"))!; const recommended = metadata.dependencies.find(x => x.id === "recommended-mod")!;