FabricModMetadata refactoring

Fixes #36
This commit is contained in:
Kir_Antipov 2022-11-25 01:32:23 +03:00
parent 5d39b55de2
commit 9570097d5b
6 changed files with 223 additions and 120 deletions

View file

@ -167,7 +167,7 @@ Can be automatically retrieved from the config file of your mod:
- `fabric.mod.json` (Fabric) - `fabric.mod.json` (Fabric)
- Custom `mc-publish` field *(recommended)*: - Custom `mc-publish` field:
```json ```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 ```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) - `mods.toml` (Forge)
- Custom `mc-publish` field *(recommended)*: - Custom `mc-publish` field *(recommended)*:
@ -307,7 +285,7 @@ Can be automatically retrieved from the config file of your mod:
- `fabric.mod.json` (Fabric) - `fabric.mod.json` (Fabric)
- Custom `mc-publish` field *(recommended)*: - Custom `mc-publish` field:
```json ```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 ```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) - `mods.toml` (Forge)
- Custom `mc-publish` field *(recommended)*: - Custom `mc-publish` field *(recommended)*:

View file

@ -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;

View file

@ -1,17 +1,20 @@
import ModMetadata from "../../metadata/mod-metadata"; import ModMetadata from "../mod-metadata";
import ZippedModMetadataReader from "../../metadata/zipped-mod-metadata-reader"; import ZippedModMetadataReader from "../zipped-mod-metadata-reader";
import FabricModConfig from "./fabric-mod-config";
import FabricModMetadata from "./fabric-mod-metadata"; import FabricModMetadata from "./fabric-mod-metadata";
export default class FabricModMetadataReader extends ZippedModMetadataReader { class FabricModMetadataReader extends ZippedModMetadataReader<FabricModConfig> {
constructor() { 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")); return JSON.parse(buffer.toString("utf8"));
} }
protected createMetadataFromConfig(config: Record<string, unknown>): ModMetadata { protected createMetadataFromConfig(config: FabricModConfig): ModMetadata {
return new FabricModMetadata(config); return new FabricModMetadata(config);
} }
} }
export default FabricModMetadataReader;

View file

@ -1,76 +1,158 @@
import action from "../../../package.json"; import action from "../../../package.json";
import ModConfig from "../../metadata/mod-config"; import Dependency from "../dependency";
import ModConfigDependency from "../../metadata/mod-config-dependency"; import DependencyKind from "../dependency-kind";
import Dependency from "../../metadata/dependency";
import DependencyKind from "../../metadata/dependency-kind";
import PublisherTarget from "../../publishing/publisher-target"; import PublisherTarget from "../../publishing/publisher-target";
import FabricModConfig from "./fabric-mod-config";
import ModMetadata from "../mod-metadata";
const ignoredByDefault = ["minecraft", "java", "fabricloader"]; type Aliases = Map<PublisherTarget, string>;
const aliases = new Map([
["fabric", "fabric-api"] type FabricDependency = FabricModConfig["depends"][string];
]);
function getDependenciesByKind(config: any, kind: DependencyKind): Dependency[] { function getDependencies(config: FabricModConfig): Dependency[] {
const kindName = DependencyKind.toString(kind).toLowerCase(); 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>(); const dependencies = new Array<Dependency>();
for (const [id, value] of Object.entries(config[kindName] || {})) { for (const [id, value] of Object.entries(config[kindName] || {})) {
const ignore = ignoredByDefault.includes(id); const ignore = isDependencyIgnoredByDefault(id);
if (typeof value === "string") { const aliases = getDefaultDependencyAliases(id);
const dependencyAliases = aliases.has(id) ? new Map(PublisherTarget.getValues().map(x => [x, aliases.get(id)])) : null; const dependency = parseDependency(id, kind, value, ignore, aliases, config);
dependencies.push(Dependency.create({ id, kind, version: value, ignore, aliases: dependencyAliases }));
} else { dependencies.push(dependency);
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));
}
} }
return dependencies; return dependencies;
} }
function getLoaders(config: any): string[] { function parseDependency(id: string, kind: DependencyKind, body: FabricDependency, ignore: boolean, aliases: Aliases, config: FabricModConfig): Dependency {
if (config[action.name]?.quilt ?? config.custom?.[action.name]?.quilt) { 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", "quilt"];
} }
return ["fabric"]; return ["fabric"];
} }
export default class FabricModMetadata extends ModConfig { function getProjects(config: FabricModConfig): Map<PublisherTarget, string> {
public readonly id: string; const projects = new Map();
public readonly name: string; for (const target of PublisherTarget.getValues()) {
public readonly version: string; const targetName = PublisherTarget.toString(target).toLowerCase();
public readonly loaders: string[]; const projectId = config.custom?.[action.name]?.[targetName]
public readonly dependencies: Dependency[]; ?? config.custom?.modmanager?.[targetName]
?? config.custom?.projects?.[targetName];
constructor(config: Record<string, unknown>) { if (projectId) {
super(config); projects.set(target, String(projectId));
this.id = String(this.config.id ?? ""); }
this.name = String(this.config.name ?? this.id); }
this.version = String(this.config.version ?? "*"); return projects;
this.loaders = getLoaders(this.config); }
this.dependencies = DependencyKind.getValues().flatMap(x => getDependenciesByKind(this.config, x));
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 { getProjectId(project: PublisherTarget): string | undefined {
const projectId = super.getProjectId(project); return this._projects.get(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);
} }
} }
export default FabricModMetadata;

View file

@ -23,10 +23,10 @@
"example-mod.mixins.json" "example-mod.mixins.json"
], ],
"projects": { "custom": {
"modmanager": {
"modrinth": "AANobbMI" "modrinth": "AANobbMI"
}, },
"custom": {
"projects": { "projects": {
"curseforge": 394468 "curseforge": 394468
}, },
@ -38,20 +38,19 @@
"depends": { "depends": {
"fabricloader": ">=0.11.3", "fabricloader": ">=0.11.3",
"fabric": ">=0.40.0", "fabric": ">=0.40.0",
"minecraft": "1.17.x", "minecraft": [
"1.17",
"1.17.1"
],
"java": ">=16" "java": ">=16"
}, },
"recommends": { "recommends": {
"recommended-mod": { "recommended-mod": {
"version": "0.2.0", "version": "0.2.0",
"projects": {
"modrinth": "AAAA"
},
"custom": { "custom": {
"projects": {
"curseforge": 42
},
"mc-publish": { "mc-publish": {
"modrinth": "AAAA",
"curseforge": 42,
"github": "v0.2.0", "github": "v0.2.0",
"ignore": true "ignore": true
} }

View file

@ -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 () => { test("custom metadata can be attached to dependency entry", async () => {
const metadata = (await ModMetadataReader.readMetadata("example-mod.fabric.jar"))!; const metadata = (await ModMetadataReader.readMetadata("example-mod.fabric.jar"))!;
const recommended = metadata.dependencies.find(x => x.id === "recommended-mod")!; const recommended = metadata.dependencies.find(x => x.id === "recommended-mod")!;