From 0fcfdc07c1fe370e95e1d50a523ef60fe4c2cecb Mon Sep 17 00:00:00 2001 From: Kir_Antipov Date: Sun, 5 Jun 2022 19:37:01 +0300 Subject: [PATCH] Implemented Quilt support Closes #5 --- src/metadata/mod-config-dependency.ts | 6 +- src/metadata/mod-config.ts | 6 +- src/metadata/mod-loader-type.ts | 1 + src/metadata/mod-metadata-reader-factory.ts | 4 + .../quilt/quilt-mod-metadata-reader.ts | 17 ++++ src/metadata/quilt/quilt-mod-metadata.ts | 97 +++++++++++++++++++ test/content/quilt.mod.json | 89 +++++++++++++++++ test/curseforge-utils.test.ts | 1 + test/mod-metadata-reader.test.ts | 86 ++++++++++++++++ 9 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 src/metadata/quilt/quilt-mod-metadata-reader.ts create mode 100644 src/metadata/quilt/quilt-mod-metadata.ts create mode 100644 test/content/quilt.mod.json diff --git a/src/metadata/mod-config-dependency.ts b/src/metadata/mod-config-dependency.ts index a957a70..12326f1 100644 --- a/src/metadata/mod-config-dependency.ts +++ b/src/metadata/mod-config-dependency.ts @@ -27,9 +27,11 @@ export default class ModConfigDependency> imple getProjectId(project: PublisherTarget): string | undefined { const projectName = PublisherTarget.toString(project).toLowerCase(); - const custom = this.config["custom"]; - const projects = this.config["projects"]; + const config = this.config; + const custom = config["custom"]; + const projects = config["projects"]; const projectId = ( + config[action.name]?.[projectName]?.id ?? config[action.name]?.[projectName] ?? custom?.[action.name]?.[projectName]?.id ?? custom?.[action.name]?.[projectName] ?? projects?.[projectName]?.id ?? projects?.[projectName] ?? custom?.projects?.[projectName]?.id ?? custom?.projects?.[projectName] diff --git a/src/metadata/mod-loader-type.ts b/src/metadata/mod-loader-type.ts index 9215497..c51e60f 100644 --- a/src/metadata/mod-loader-type.ts +++ b/src/metadata/mod-loader-type.ts @@ -1,6 +1,7 @@ enum ModLoaderType { Fabric = 1, Forge, + Quilt, } namespace ModLoaderType { diff --git a/src/metadata/mod-metadata-reader-factory.ts b/src/metadata/mod-metadata-reader-factory.ts index f8b2921..faef2ba 100644 --- a/src/metadata/mod-metadata-reader-factory.ts +++ b/src/metadata/mod-metadata-reader-factory.ts @@ -1,5 +1,6 @@ import FabricModMetadataReader from "./fabric/fabric-mod-metadata-reader"; import ForgeModMetadataReader from "./forge/forge-mod-metadata-reader"; +import QuiltModMetadataReader from "./quilt/quilt-mod-metadata-reader"; import ModLoaderType from "./mod-loader-type"; import ModMetadataReader from "./mod-metadata-reader"; @@ -12,6 +13,9 @@ export default class ModMetadataReaderFactory { case ModLoaderType.Forge: return new ForgeModMetadataReader(); + case ModLoaderType.Quilt: + return new QuiltModMetadataReader(); + default: throw new Error(`Unknown mod loader "${ModLoaderType.toString(loaderType)}"`); } diff --git a/src/metadata/quilt/quilt-mod-metadata-reader.ts b/src/metadata/quilt/quilt-mod-metadata-reader.ts new file mode 100644 index 0000000..caac99a --- /dev/null +++ b/src/metadata/quilt/quilt-mod-metadata-reader.ts @@ -0,0 +1,17 @@ +import ModMetadata from "../../metadata/mod-metadata"; +import ZippedModMetadataReader from "../../metadata/zipped-mod-metadata-reader"; +import QuiltModMetadata from "./quilt-mod-metadata"; + +export default class QuiltModMetadataReader extends ZippedModMetadataReader { + constructor() { + super("quilt.mod.json"); + } + + protected loadConfig(buffer: Buffer): Record { + return JSON.parse(buffer.toString("utf8")); + } + + protected createMetadataFromConfig(config: Record): ModMetadata { + return new QuiltModMetadata(config); + } +} diff --git a/src/metadata/quilt/quilt-mod-metadata.ts b/src/metadata/quilt/quilt-mod-metadata.ts new file mode 100644 index 0000000..92dd098 --- /dev/null +++ b/src/metadata/quilt/quilt-mod-metadata.ts @@ -0,0 +1,97 @@ +import action from "../../../package.json"; +import Dependency from "../../metadata/dependency"; +import DependencyKind from "../../metadata/dependency-kind"; +import ModConfig from "../../metadata/mod-config"; +import ModConfigDependency from "../../metadata/mod-config-dependency"; +import PublisherTarget from "../../publishing/publisher-target"; + +function extractId(id?: string): string | null { + if (!id) { + return id ?? null; + } + + const separatorIndex = id.indexOf(":"); + if (separatorIndex !== -1) { + id = id.substring(separatorIndex + 1); + } + + return id; +} + +function getDependencyEntries(container: any, transformer?: (x: any) => void): any[] { + if (!Array.isArray(container)) { + return []; + } + + if (transformer) { + container = container.map(x => typeof x === "string" ? ({ id: x }) : ({ ...x })); + container.forEach(transformer); + } + return container; +} + +const ignoredByDefault = ["minecraft", "java", "quilt_loader"]; +const aliases = new Map([ + ["fabric", "fabric-api"], + ["quilted_fabric_api", "qsl"], +]); +function createDependency(body: any): Dependency { + const id = extractId(typeof body === "string" ? body : String(body.id ?? "")); + const ignore = ignoredByDefault.includes(id); + if (id.startsWith("quilted_") || id.startsWith("quilt_")) { + aliases.set(id, "qsl"); + } + + if (typeof body === "string") { + const dependencyAliases = aliases.has(id) ? new Map(PublisherTarget.getValues().map(x => [x, aliases.get(id)])) : null; + return Dependency.create({ id, ignore, aliases: dependencyAliases }); + } + + const dependencyMetadata = { + ignore, + ...body, + id, + version: body.version ?? String(Array.isArray(body.versions) ? body.versions[0] : body.versions || "*"), + kind: ( + body.incompatible && body.unless && DependencyKind.Conflicts || + body.incompatible && DependencyKind.Breaks || + body.embedded && DependencyKind.Includes || + body.optional && DependencyKind.Recommends || + DependencyKind.Depends + ) + }; + if (aliases.has(id)) { + if (!dependencyMetadata[action.name]) { + dependencyMetadata[action.name] = {}; + } + for (const target of PublisherTarget.getValues()) { + const targetName = PublisherTarget.toString(target).toLowerCase(); + if (typeof dependencyMetadata[action.name][targetName] !== "string") { + dependencyMetadata[action.name][targetName] = aliases.get(id); + } + } + } + return new ModConfigDependency(dependencyMetadata); +} + +export default class QuiltModMetadata extends ModConfig { + public readonly id: string; + public readonly name: string; + public readonly version: string; + public readonly loaders: string[]; + public readonly dependencies: Dependency[]; + + constructor(config: Record) { + super(config); + const root = >this.config.quilt_loader ?? {}; + this.id = String(root.id ?? ""); + this.name = String(root.name ?? this.id); + this.version = String(root.version ?? "*"); + this.loaders = ["quilt"]; + this.dependencies = getDependencyEntries(root.depends) + .concat(getDependencyEntries(root.provides, x => x.embedded = true)) + .concat(getDependencyEntries(root.breaks, x => x.incompatible = true)) + .map(createDependency) + .filter((x, i, self) => self.findIndex(y => x.id === y.id && x.kind === y.kind) === i); + } +} \ No newline at end of file diff --git a/test/content/quilt.mod.json b/test/content/quilt.mod.json new file mode 100644 index 0000000..1b3f1e1 --- /dev/null +++ b/test/content/quilt.mod.json @@ -0,0 +1,89 @@ +{ + "schema_version": 1, + "quilt_loader": { + "group": "com.example", + "id": "example-mod", + "version": "0.1.0", + "name": "Example Mod", + "description": "Description", + "authors": [ + "Author" + ], + "contact": { + "homepage": "https://github.com/", + "sources": "https://github.com/", + "issues": "https://github.com/", + "wiki": "https://github.com/" + }, + "license": "MIT", + "icon": "icon.jpg", + "intermediate_mappings": "net.fabricmc:intermediary", + "environment": "*", + "entrypoints": { + "main": [ + "example.ExampleMod" + ] + }, + "depends": [ + { + "id": "quilt_loader", + "version": ">=0.11.3" + }, + { + "id": "quilt_base", + "version": ">=0.40.0" + }, + { + "id": "minecraft", + "version": "1.17.x" + }, + { + "id": "java", + "version": ">=16" + }, + { + "id": "recommended-mod", + "version": "0.2.0", + "optional": true, + "mc-publish": { + "modrinth": "AAAA", + "ignore": true + }, + "projects": { + "curseforge": 42 + }, + "custom": { + "projects": { + "github": "v0.2.0" + } + } + } + ], + "provides": [ + "included:included-mod" + ], + "breaks": [ + "breaking-mod", + { + "id": "conflicting:conflicting-mod", + "version": "<0.40.0", + "unless": "fix-conflicting-mod" + } + ] + }, + "mc-publish": { + "modrinth": "AANobbMI" + }, + "projects": { + "curseforge": 394468 + }, + "custom": { + "projects": { + "github": "mc1.18-0.4.0-alpha5" + } + }, + "mixins": [ + "example-mod.mixins.json" + ], + "access_widener": "example.accesswidener" +} diff --git a/test/curseforge-utils.test.ts b/test/curseforge-utils.test.ts index 7552e8e..1c211a7 100644 --- a/test/curseforge-utils.test.ts +++ b/test/curseforge-utils.test.ts @@ -80,6 +80,7 @@ describe("convertToCurseForgeVersions", () => { loaders: { fabric: 7499, forge: 7498, + quilt: 9153, rift: 7500 }, java: { diff --git a/test/mod-metadata-reader.test.ts b/test/mod-metadata-reader.test.ts index d81c65a..fab477b 100644 --- a/test/mod-metadata-reader.test.ts +++ b/test/mod-metadata-reader.test.ts @@ -170,6 +170,92 @@ describe("ModMetadataReader.readMetadata", () => { }); }); + describe("Quilt", () => { + beforeAll(() => new Promise(resolve => { + const zip = new ZipFile(); + zip.addFile("./test/content/quilt.mod.json", "quilt.mod.json"); + zip.end(); + zip.outputStream.pipe(fs.createWriteStream("example-mod.quilt.jar")).on("close", resolve); + })); + + afterAll(() => new Promise(resolve => fs.unlink("example-mod.quilt.jar", resolve))); + + test("the format can be read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.quilt.jar"); + expect(metadata).toBeTruthy(); + }); + + test("mod info can be read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.quilt.jar"); + expect(metadata.id).toBe("example-mod"); + expect(metadata.name).toBe("Example Mod"); + expect(metadata.version).toBe("0.1.0"); + expect(metadata.loaders).toMatchObject(["quilt"]); + }); + + test("project ids can be specified in the config file", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.quilt.jar"); + expect(metadata.getProjectId(PublisherTarget.Modrinth)).toBe("AANobbMI"); + expect(metadata.getProjectId(PublisherTarget.CurseForge)).toBe("394468"); + expect(metadata.getProjectId(PublisherTarget.GitHub)).toBe("mc1.18-0.4.0-alpha5"); + }); + + test("all dependencies are read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.quilt.jar"); + expect(metadata.dependencies).toHaveLength(8); + const dependencies = metadata.dependencies.reduce((agg, x) => { agg[x.id] = x; return agg; }, >{}); + expect(dependencies["quilt_loader"]?.kind).toBe(DependencyKind.Depends); + expect(dependencies["quilt_base"]?.kind).toBe(DependencyKind.Depends); + expect(dependencies["minecraft"]?.kind).toBe(DependencyKind.Depends); + expect(dependencies["java"]?.kind).toBe(DependencyKind.Depends); + expect(dependencies["recommended-mod"]?.kind).toBe(DependencyKind.Recommends); + expect(dependencies["included-mod"]?.kind).toBe(DependencyKind.Includes); + expect(dependencies["conflicting-mod"]?.kind).toBe(DependencyKind.Conflicts); + expect(dependencies["breaking-mod"]?.kind).toBe(DependencyKind.Breaks); + }); + + test("dependency info can be read", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.quilt.jar"); + const conflicting = metadata.dependencies.find(x => x.id === "conflicting-mod"); + expect(conflicting).toBeTruthy(); + expect(conflicting.id).toBe("conflicting-mod"); + expect(conflicting.kind).toBe(DependencyKind.Conflicts); + expect(conflicting.version).toBe("<0.40.0"); + expect(conflicting.ignore).toBe(false); + for (const project of PublisherTarget.getValues()) { + expect(conflicting.getProjectSlug(project)).toBe(conflicting.id); + } + }); + + test("custom metadata can be attached to dependency entry", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.quilt.jar"); + const recommended = metadata.dependencies.find(x => x.id === "recommended-mod"); + expect(recommended).toBeTruthy(); + expect(recommended.id).toBe("recommended-mod"); + expect(recommended.kind).toBe(DependencyKind.Recommends); + expect(recommended.version).toBe("0.2.0"); + expect(recommended.ignore).toBe(true); + expect(recommended.getProjectSlug(PublisherTarget.Modrinth)).toBe("AAAA"); + expect(recommended.getProjectSlug(PublisherTarget.CurseForge)).toBe("42"); + expect(recommended.getProjectSlug(PublisherTarget.GitHub)).toBe("v0.2.0"); + }); + + test("special case dependencies (minecraft, java and quilt_loader) are ignored by default", async () => { + const metadata = await ModMetadataReader.readMetadata("example-mod.quilt.jar"); + expect(metadata.dependencies.find(x => x.id === "minecraft").ignore).toBe(true); + expect(metadata.dependencies.find(x => x.id === "java").ignore).toBe(true); + expect(metadata.dependencies.find(x => x.id === "quilt_loader").ignore).toBe(true); + }); + + test("special case dependencies (quilted_quilt_api) are replaced with their aliases", async() => { + const metadata = await ModMetadataReader.readMetadata("example-mod.quilt.jar"); + const quilt = metadata.dependencies.find(x => x.id === "quilt_base"); + for (const target of PublisherTarget.getValues()) { + expect(quilt.getProjectSlug(target) === "qsl"); + } + }); + }); + describe("unsupported mod formats", () => { test("null is returned when the format is not supported or specified file does not exist", async () => { const metadata = await ModMetadataReader.readMetadata("example-mod.unknown.jar");