From 0fba8811dfd70f03c4e0357109794705d3e3b9cc Mon Sep 17 00:00:00 2001 From: BellCube Dev <33764825+BellCubeDev@users.noreply.github.com> Date: Sun, 31 Dec 2023 09:26:38 -0500 Subject: [PATCH] Update Readme & Rename DefaultFomodAsElementConfig to DefaultFomodDocumentConfig --- README.md | 72 +++++++++++++--------- package.json | 2 +- src/definitions/Metadata.ts | 4 +- src/definitions/lib/FomodDocumentConfig.ts | 2 +- src/definitions/lib/ParseOptionFlags.ts | 6 +- src/definitions/module/Fomod.ts | 10 +-- src/definitions/module/Group.ts | 4 +- src/definitions/module/Option.ts | 4 +- src/definitions/module/Step.ts | 4 +- 9 files changed, 62 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index c1238f7..6031615 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ A JavaScript/TypeScript library for working with FOMOD installers both in the br

What Is a FOMOD

-FOMOD, or `F`all`O`ut `MOD`, is a confusingly-named file installer format pioneered by Fallout Mod Manager (FOMM). The goal of the format is to present users with options to be taken into account when installing the mod. It's primarily used in the Bethesda modding scene, though it's typically supported for use with any game a given mod manager supports. While FOMM, Nexus Mod Manager, and Vortex all supported writing a .NET installer (called a "scripted installer"), it's very rarely observed in use in the wild. With that and the security threat that comes with arbitrary code execution, most mod managers forego its support (Nexus Mod Manager and Vortex have both supported it, though). +FOMOD, which originally stood for `F`all`O`ut `MOD`, is a confusingly-named file installer format pioneered by Fallout Mod Manager (FOMM). The goal of the format is to present users with options to be taken into account when installing the mod. It's primarily used in the Bethesda modding scene, though it's typically supported for use with any game a given mod manager supports. While FOMM, Nexus Mod Manager, and Vortex all supported writing a .NET installer (called a "scripted installer"), it's very rarely observed in use in the wild. With that and the security threat that comes with arbitrary code execution, most mod managers forego its support. FOMOD installers are therefore nearly always written in the alternative, a [schema](https://qconsulting.ca/fo3/ModConfig5.0.xsd)-compliant XML file. This XML format is a little burdensome and a lot XML, so tools have popped up over the years to simplify their creation. Namely, GandaG's [FOMOD Designer](https://github.com/GandaG/fomod-designer/)—a direct 1-to-1 editor and representation of the XML tree—and the [FOMOD Creation Tool](https://www.nexusmods.com/fallout4/mods/6821/), a more abstract and arguably more user-friendly representation of the installer format. In development is the [Fomod Builder](https://github.com/BellCubeDev/fomod-builder), an attempt at meeting both in the middle by providing full schema-allowed control, helpful tooltips, a dark-mode UI, built-in mod manager previews & editor styles, and encouraging users to poke around in the XML as they use the tool. @@ -37,12 +37,14 @@ FOMOD installers are therefore nearly always written in the alternative, a [sche

Quick terminology breakdown

(I chose better names)
-| Term Used | Canonical Name | What It Refers To | -| :-: | :-: | :-- | -| Install | File | Files and folders that might be installed by the FOMOD | -| Step | Install Step | A bundle of Groups presented as a single page | -| Groups | Group | A bundle of checkboxes or radio buttons presented as a section with a header | -| Options | Plugins | A single checkbox or radio button | +| Term Used | Canonical Name | What It Refers To | +| :-: | :-: | :-- | +| Install | File/Folder | Files and folders that might be installed by the FOMOD | +| Step | Install Step | A bundle of Groups presented as a single page | +| Group | Group | A bundle of checkboxes or radio buttons presented as a section with a header | +| Option | Plugin | A single checkbox or radio button | +| FOMM Dependency | Mod Manager Version Dependency | A dependency on a specific version of a mod manager | +| FOSE Dependency | Script Extender Version Dependency | A dependency on a specific version of a script extender |
@@ -52,9 +54,8 @@ FOMOD installers are therefore nearly always written in the alternative, a [sche `fomod` is a library to parse, create, and edit FOMOD installers. It includes: - * Full support for the FOMOD specification -* Bundled type declarations +* Bundled type declarations, source maps, and source code * Written in TypeScript and thoroughly unit-tested * Helpful JSDoc comments detailing: * Usage @@ -69,16 +70,6 @@ FOMOD installers are therefore nearly always written in the alternative, a [sche > Documents will need to be explicitly decommissioned to prevent memory leaks when using large numbers of documents or allowing users to arbitrarily create them * Dependencies on Options (via flags) * Options can be used as dependency within the codebase and are converted to a flag dependency when an XML document is produced -* Automatic Install Optimization (required for technical reasons) - -### Wait, what's this "Automatic Install Optimization" business? - -As someone who used to use Vortex on a slower machine, I've noticed significant performance issues with installers which specified their installs (files and folders to install) directly in the option. As such, I've designed a way around this—using conditional installs. In combination with dependencies on options, this will have no impact on the functionality of your existing FOMODs and is entirely safe to use. The only noteworthy downside is the readability of the resulting XML. - -Unfortunately, due to complications with how installs are represented on a technical level, there is currently no option to use the Files option directly. This likely will be addressed in a future release of the library. - -> [!NOTE] -> This feature is not yet available. This will be available before the first stable release.
@@ -94,14 +85,33 @@ If you're looking to use this library, this section will be your best friend. At its core, each data structure (steps, options, installs, dependencies, etc.) is represented by a class. -If you're already familiar with the XML structure, each class generally represents one or two levels of element. For instance, the Option class represents the `` container and the `` tag. This is done to reduce the amount of boilerplate you as a developer need to write. +If you're already familiar with the XML structure, each class generally represents one or two levels of element. For instance, the Option class represents the `` element and the Group class represents `` and ``. This is done to reduce the amount of boilerplate you as a developer need to write while still giving you complete control over your installer. -### +### The XmlRepresentation Class This associates the element with this document! For most classes, this is free of side effects. For most classes, the element-document map does not restrict garbage collection. However, with certain classes (e.g. FlagInstance), this can lead to memory leaks. To prevent this, you can call the decommission method on the class. The easiest way to make sure you're covered, especially between updates, is to always decommission the document (or class) when you're done with it. The `decommission` method is recursive; therefore, you should call it on the highest level class(es) you have access to. Typically, these will be `fomod` and `fomodInfo`. +### Option Dependencies + +This library provides a way to use an Option directly as the key for a FlagDependency. We'll handle the flag name and value for you, cutting out the flag middle-man from the developer's perspective. + +## Parsing/Serialization Configuration + +The library includes a number of options to control how the XML is parsed and serialized. These can be passed into any XmlRepresentation subclass's `XmlRepresentation.prototype.asElement()` and `XmlRepresentation.parse()` methods as well as the `parseModuleDoc()` and `parseInfoDoc()` functions. Options: + +| Option | Type | Default | Description | +| --: | :-: | :-: | :-- | +| `includeInfoSchema` | `boolean` or `string` | `true` | Whether or to include a third-party schema for Info.xml. If a string is provided, we'll use that string as the schema location. Otherwise, we'll use the library's default. | +| `flattenConditionalInstalls` | `boolean` | `false` | Whether to move all conditional installs with only a dependency on a single option to the tag of that option. Note that this may cause slight performance issues with Vortex on slower machines. | +| `flattenConditionalInstallsNoDependencies` | `boolean` | `false` | Whether to reorganize all conditional installs with no dependencies into the tag. | +| `removeEmptyConditionalInstalls` | `boolean` | `true` | Whether to remove conditional installs with no dependencies and no files (has no effect when `flattenConditionalInstallsNoDependencies` is `true`). | +| `optionSelectedValue` | `string` | `'OPTION_SELECTED'` | String used for the flag value of option dependencies. | +| `parseOptionFlags` | `boolean` or `'loose'` | `true` | Whether to attempt to determine if a flag is an option flag to the best of our knowledge. If `'loose'` is provided, we'll accept any flag name or value so long as it's only set by one option. | + + + ### Examples #### Parsing an Existing Installer @@ -114,17 +124,20 @@ import { parseInfoDoc, parseModuleDoc } from 'fomod'; import { JSDOM } from 'jsdom'; import fs from 'fs/promises'; +// You can use whatever config you'd like +declare const config: FomodDocumentConfig; + // ModuleConfig.xml const moduleText = await fs.readFile('path/to/ModuleConfig.xml'); const moduleDoc = new JSDOM(moduleText, {contentType: 'text/xml'}); -const installer = parseModuleDoc(moduleDoc.window.document) +const installer = parseModuleDoc(moduleDoc.window.document, config) // Info.xml const infoText = await fs.readFile('path/to/Info.xml'); const infoDoc = new JSDOM(infoText, {contentType: 'text/xml'}); -const metadata = parseModuleDoc(infoDoc.window.document) +const metadata = parseInfoDoc(infoDoc.window.document, config) ``` Or, for a more optimized example: @@ -132,9 +145,11 @@ Or, for a more optimized example: import { parseInfoDoc, parseModuleDoc } from 'fomod'; import { JSDOM } from 'jsdom'; +declare const config: FomodDocumentConfig; + const [installer, metadata] = Promise.all([ - JSDOM.fromFile('path/to/ModuleConfig.xml').then((dom) => parseModuleDoc(dom.window.document)), - JSDOM.fromFile('path/to/Info.xml').then((dom) => parseInfoDoc(dom.window.document)), + JSDOM.fromFile('path/to/ModuleConfig.xml').then((dom) => parseModuleDoc(dom.window.document, config)), + JSDOM.fromFile('path/to/Info.xml').then((dom) => parseInfoDoc(dom.window.document, config)), ]); ``` @@ -201,15 +216,16 @@ const info = new FomodInfo({ import { Fomod } from 'fomod'; // you can refer to the previous examples for how you might get a Fomod instance -declare const module: Fomod; +declare const moduleConfig: Fomod; +declare const config: FomodDocumentConfig; const thatOneDocument = document.implementation.createDocument(null, null, null); // Associate the document with the Fomod instance -console.log(module.asElement(thatOneDocument)); +console.log(moduleConfig.asElement(thatOneDocument, config)); // We're done with the document, so let's clean it up -module.decommission(thatOneDocument); +moduleConfig.decommission(thatOneDocument); ``` diff --git a/package.json b/package.json index 0ded944..dd1b77f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "fomod", "description": "A library for creating, parsing, editing, and validating XML-based Fomod installers, widely popularized in the Bethesda modding scene", - "version": "0.2.0", + "version": "0.2.1", "main": "dist/index.js", "repository": "https://github.com/BellCubeDev/fomod-js/", "bugs": { diff --git a/src/definitions/Metadata.ts b/src/definitions/Metadata.ts index a0b6715..2589d34 100644 --- a/src/definitions/Metadata.ts +++ b/src/definitions/Metadata.ts @@ -1,6 +1,6 @@ import { XmlNamespaces } from "../DomUtils"; import { TagName } from "./Enums"; -import { DefaultFomodAsElementConfig, FomodDocumentConfig } from "./lib/FomodDocumentConfig"; +import { DefaultFomodDocumentConfig, FomodDocumentConfig } from "./lib/FomodDocumentConfig"; import { ElementObjectMap, XmlRepresentation } from "./lib/XmlRepresentation"; export interface FomodInfoData { @@ -62,7 +62,7 @@ export class FomodInfo extends XmlRepresentation { element.setAttributeNS(XmlNamespaces.XMLNS, 'xmlns:xsi', XmlNamespaces.XSI); const currentSchema = element.getAttributeNS(XmlNamespaces.XSI, 'noNamespaceSchemaLocation'); - if (includeInfoSchema ?? DefaultFomodAsElementConfig.includeInfoSchema) { + if (includeInfoSchema ?? DefaultFomodDocumentConfig.includeInfoSchema) { if (typeof includeInfoSchema === 'string') element.setAttributeNS(XmlNamespaces.XSI, 'noNamespaceSchemaLocation', includeInfoSchema); else if (currentSchema === null) element.setAttributeNS(XmlNamespaces.XSI, 'noNamespaceSchemaLocation', DefaultInfoSchema); diff --git a/src/definitions/lib/FomodDocumentConfig.ts b/src/definitions/lib/FomodDocumentConfig.ts index 85856dd..e77641f 100644 --- a/src/definitions/lib/FomodDocumentConfig.ts +++ b/src/definitions/lib/FomodDocumentConfig.ts @@ -43,7 +43,7 @@ export interface FomodDocumentConfig { parseOptionFlags?: boolean | 'loose'; } -export const DefaultFomodAsElementConfig = { +export const DefaultFomodDocumentConfig = { includeInfoSchema: true, flattenConditionalInstalls: false, flattenConditionalInstallsNoDependencies: false, diff --git a/src/definitions/lib/ParseOptionFlags.ts b/src/definitions/lib/ParseOptionFlags.ts index 5218a43..026198b 100644 --- a/src/definitions/lib/ParseOptionFlags.ts +++ b/src/definitions/lib/ParseOptionFlags.ts @@ -1,12 +1,12 @@ import { FlagDependency, FlagSetter, Install, InstallPattern, Option } from "../module"; -import { DefaultFomodAsElementConfig, FomodDocumentConfig } from "./FomodDocumentConfig"; +import { DefaultFomodDocumentConfig, FomodDocumentConfig } from "./FomodDocumentConfig"; export function parseOptionFlags(options: Option[], document: Document, config: FomodDocumentConfig = {}, dependencies: FlagDependency[] = []) { const uniqueFlags = new Map, deps: FlagDependency[]]>(); const flagsFound = new Set(); - const loose = (config.parseOptionFlags ?? DefaultFomodAsElementConfig.parseOptionFlags) === 'loose'; - const strictValue = config.optionSelectedValue ?? DefaultFomodAsElementConfig.optionSelectedValue; + const loose = (config.parseOptionFlags ?? DefaultFomodDocumentConfig.parseOptionFlags) === 'loose'; + const strictValue = config.optionSelectedValue ?? DefaultFomodDocumentConfig.optionSelectedValue; for (const option of options) { for (const flag of option.flagsToSet) { diff --git a/src/definitions/module/Fomod.ts b/src/definitions/module/Fomod.ts index 82f3e9f..5427c17 100644 --- a/src/definitions/module/Fomod.ts +++ b/src/definitions/module/Fomod.ts @@ -7,7 +7,7 @@ import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/XmlRepre import { AttributeName, BooleanString, ModuleNamePosition, SortingOrder, TagName } from "../Enums"; import { Option } from "./Option"; import { gatherDependedUponOptions, gatherFlagDependencies } from "../lib/utils"; -import { DefaultFomodAsElementConfig, FomodDocumentConfig } from "../lib/FomodDocumentConfig"; +import { DefaultFomodDocumentConfig, FomodDocumentConfig } from "../lib/FomodDocumentConfig"; import { parseOptionFlags } from "../lib/ParseOptionFlags"; export interface ModuleImageMetadata { @@ -174,7 +174,7 @@ export class Fomod extends XmlRepresentation { const el = installOrPattern.asElement(document, config); - if (config.flattenConditionalInstalls ?? DefaultFomodAsElementConfig.flattenConditionalInstalls) { + if (config.flattenConditionalInstalls ?? DefaultFomodDocumentConfig.flattenConditionalInstalls) { const optionDependencies = gatherDependedUponOptions(installOrPattern.dependencies); if (optionDependencies.size === 1) { const option = optionDependencies.values().next().value as Option; @@ -187,10 +187,10 @@ export class Fomod extends XmlRepresentation { if (installOrPattern.dependencies.dependencies.size === 0) { - if ((config.removeEmptyConditionalInstalls ?? DefaultFomodAsElementConfig.removeEmptyConditionalInstalls) && installOrPattern.filesWrapper.installs.size === 0) { + if ((config.removeEmptyConditionalInstalls ?? DefaultFomodDocumentConfig.removeEmptyConditionalInstalls) && installOrPattern.filesWrapper.installs.size === 0) { el.remove(); continue; - } else if (config.flattenConditionalInstallsNoDependencies ?? DefaultFomodAsElementConfig.flattenConditionalInstallsNoDependencies) { + } else if (config.flattenConditionalInstallsNoDependencies ?? DefaultFomodDocumentConfig.flattenConditionalInstallsNoDependencies) { installOrPattern.filesWrapper.installs.forEach(install => requiredInstallContainer.appendChild(install.asElement(document, config))); el.remove(); @@ -267,7 +267,7 @@ export class Fomod extends XmlRepresentation { if (parsed) fomod.steps.add(parsed); } - if (config.parseOptionFlags ?? DefaultFomodAsElementConfig.parseOptionFlags) { + if (config.parseOptionFlags ?? DefaultFomodDocumentConfig.parseOptionFlags) { const dependencies = Array.from(gatherFlagDependencies(fomod.moduleDependencies)); for (const install of fomod.installs) if (install instanceof InstallPattern) dependencies.push(...gatherFlagDependencies(install.dependencies)); diff --git a/src/definitions/module/Group.ts b/src/definitions/module/Group.ts index e584c89..9c334b6 100644 --- a/src/definitions/module/Group.ts +++ b/src/definitions/module/Group.ts @@ -3,7 +3,7 @@ import { InvalidityReason, InvalidityReport } from "../lib/InvalidityReporting"; import { Option } from "./Option"; import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/XmlRepresentation"; import { AttributeName, GroupBehaviorType, SortingOrder, TagName } from "../Enums"; -import { DefaultFomodAsElementConfig, FomodDocumentConfig } from "../lib/FomodDocumentConfig"; +import { DefaultFomodDocumentConfig, FomodDocumentConfig } from "../lib/FomodDocumentConfig"; import { parseOptionFlags } from "../lib/ParseOptionFlags"; import { gatherFlagDependencies } from "../lib/utils"; @@ -87,7 +87,7 @@ export class Group extends XmlRepresentation { if (option !== null) group.options.add(option); } - if (config.parseOptionFlags ?? DefaultFomodAsElementConfig.parseOptionFlags) { + if (config.parseOptionFlags ?? DefaultFomodDocumentConfig.parseOptionFlags) { const dependencies = []; const options = group.gatherOptions(); diff --git a/src/definitions/module/Option.ts b/src/definitions/module/Option.ts index 58a7f72..cf46abf 100644 --- a/src/definitions/module/Option.ts +++ b/src/definitions/module/Option.ts @@ -5,7 +5,7 @@ import { Install, InstallPattern } from "./Install"; import { InvalidityReason, InvalidityReport } from "../lib/InvalidityReporting"; import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/XmlRepresentation"; import { AttributeName, OptionType, TagName } from "../Enums"; -import { DefaultFomodAsElementConfig, FomodDocumentConfig } from "../lib/FomodDocumentConfig"; +import { DefaultFomodDocumentConfig, FomodDocumentConfig } from "../lib/FomodDocumentConfig"; /*** * $$$$$$\ $$\ $$\ $$$$$$$\ $$\ @@ -112,7 +112,7 @@ export class Option extends XmlRepresentation const name = baseName + `--${suffix}`; if (!existingFlagNames.has(name)) { - const setter = new FlagSetter(new FlagInstance(name, config.optionSelectedValue ?? DefaultFomodAsElementConfig.optionSelectedValue, true)); + const setter = new FlagSetter(new FlagInstance(name, config.optionSelectedValue ?? DefaultFomodDocumentConfig.optionSelectedValue, true)); thisObj._existingOptionFlagSetterByDocument.set(document, setter); return setter; } diff --git a/src/definitions/module/Step.ts b/src/definitions/module/Step.ts index 17e85c2..89c0b4b 100644 --- a/src/definitions/module/Step.ts +++ b/src/definitions/module/Step.ts @@ -3,7 +3,7 @@ import { Group } from "./Group"; import { InvalidityReason, InvalidityReport } from "../lib/InvalidityReporting"; import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/XmlRepresentation"; import { AttributeName, SortingOrder, TagName } from "../Enums"; -import { DefaultFomodAsElementConfig, FomodDocumentConfig } from "../lib/FomodDocumentConfig"; +import { DefaultFomodDocumentConfig, FomodDocumentConfig } from "../lib/FomodDocumentConfig"; import { Option } from "./Option"; import { parseOptionFlags } from "../lib/ParseOptionFlags"; import { gatherFlagDependencies } from "../lib/utils"; @@ -95,7 +95,7 @@ export class Step extends XmlRepresentation { if (group !== null) step.groups.add(group); } - if (config.parseOptionFlags ?? DefaultFomodAsElementConfig.parseOptionFlags) { + if (config.parseOptionFlags ?? DefaultFomodDocumentConfig.parseOptionFlags) { const dependencies = Array.from(gatherFlagDependencies(step.visibilityDeps)); const options = step.gatherOptions();