Files
2025-01-04 00:34:03 +01:00

366 lines
13 KiB
JavaScript

/**
* @typedef {Object} PackageCompatibilityBadge
* @property {string} type A type in "safe", "unsafe", "warning", "neutral" applied as a CSS class
* @property {string} tooltip A tooltip string displayed when hovering over the badge
* @property {string} [label] An optional text label displayed in the badge
* @property {string} [icon] An optional icon displayed in the badge
*/
/**
* A client-side mixin used for all Package types.
* @param {typeof BasePackage} BasePackage The parent BasePackage class being mixed
* @returns {typeof ClientPackage} A BasePackage subclass mixed with ClientPackage features
* @category - Mixins
*/
function ClientPackageMixin(BasePackage) {
class ClientPackage extends BasePackage {
/**
* Is this package marked as a favorite?
* This boolean is currently only populated as true in the /setup view of the software.
* @type {boolean}
*/
favorite = false;
/**
* Associate package availability with certain badge for client-side display.
* @returns {PackageCompatibilityBadge|null}
*/
getVersionBadge() {
return this.constructor.getVersionBadge(this.availability, this);
}
/* -------------------------------------------- */
/**
* Determine a version badge for the provided compatibility data.
* @param {number} availability The availability level.
* @param {Partial<PackageManifestData>} data The compatibility data.
* @param {object} [options]
* @param {Collection<string, Module>} [options.modules] A specific collection of modules to test availability
* against. Tests against the currently installed modules by
* default.
* @param {Collection<string, System>} [options.systems] A specific collection of systems to test availability
* against. Tests against the currently installed systems by
* default.
* @returns {PackageCompatibilityBadge|null}
*/
static getVersionBadge(availability, data, { modules, systems }={}) {
modules ??= game.modules;
systems ??= game.systems;
const codes = CONST.PACKAGE_AVAILABILITY_CODES;
const { compatibility, version, relationships } = data;
switch ( availability ) {
// Unsafe
case codes.UNKNOWN:
case codes.REQUIRES_CORE_DOWNGRADE:
case codes.REQUIRES_CORE_UPGRADE_STABLE:
case codes.REQUIRES_CORE_UPGRADE_UNSTABLE:
const labels = {
[codes.UNKNOWN]: "SETUP.CompatibilityUnknown",
[codes.REQUIRES_CORE_DOWNGRADE]: "SETUP.RequireCoreDowngrade",
[codes.REQUIRES_CORE_UPGRADE_STABLE]: "SETUP.RequireCoreUpgrade",
[codes.REQUIRES_CORE_UPGRADE_UNSTABLE]: "SETUP.RequireCoreUnstable"
};
return {
type: "error",
tooltip: game.i18n.localize(labels[availability]),
label: version,
icon: "fa fa-file-slash"
};
case codes.MISSING_SYSTEM:
return {
type: "error",
tooltip: game.i18n.format("SETUP.RequireDep", { dependencies: data.system }),
label: version,
icon: "fa fa-file-slash"
};
case codes.MISSING_DEPENDENCY:
case codes.REQUIRES_DEPENDENCY_UPDATE:
return {
type: "error",
label: version,
icon: "fa fa-file-slash",
tooltip: this._formatBadDependenciesTooltip(availability, data, relationships.requires, {
modules, systems
})
};
// Warning
case codes.UNVERIFIED_GENERATION:
return {
type: "warning",
tooltip: game.i18n.format("SETUP.CompatibilityRiskWithVersion", { version: compatibility.verified }),
label: version,
icon: "fas fa-exclamation-triangle"
};
case codes.UNVERIFIED_SYSTEM:
return {
type: "warning",
label: version,
icon: "fas fa-exclamation-triangle",
tooltip: this._formatIncompatibleSystemsTooltip(data, relationships.systems, { systems })
};
// Neutral
case codes.UNVERIFIED_BUILD:
return {
type: "neutral",
tooltip: game.i18n.format("SETUP.CompatibilityRiskWithVersion", { version: compatibility.verified }),
label: version,
icon: "fas fa-code-branch"
};
// Safe
case codes.VERIFIED:
return {
type: "success",
tooltip: game.i18n.localize("SETUP.Verified"),
label: version,
icon: "fas fa-code-branch"
};
}
return null;
}
/* -------------------------------------------- */
/**
* List missing dependencies and format them for display.
* @param {number} availability The availability value.
* @param {Partial<PackageManifestData>} data The compatibility data.
* @param {Iterable<RelatedPackage>} deps The dependencies to format.
* @param {object} [options]
* @param {Collection<string, Module>} [options.modules] A specific collection of modules to test availability
* against. Tests against the currently installed modules by
* default.
* @param {Collection<string, System>} [options.systems] A specific collection of systems to test availability
* against. Tests against the currently installed systems by
* default.
* @returns {string}
* @protected
*/
static _formatBadDependenciesTooltip(availability, data, deps, { modules, systems }={}) {
modules ??= game.modules;
systems ??= game.systems;
const codes = CONST.PACKAGE_AVAILABILITY_CODES;
const checked = new Set();
const bad = [];
for ( const dep of deps ) {
if ( (dep.type !== "module") || checked.has(dep.id) ) continue;
if ( !modules.has(dep.id) ) bad.push(dep.id);
else if ( availability === codes.REQUIRES_DEPENDENCY_UPDATE ) {
const module = modules.get(dep.id);
if ( module.availability !== codes.VERIFIED ) bad.push(dep.id);
}
checked.add(dep.id);
}
const label = availability === codes.MISSING_DEPENDENCY ? "SETUP.RequireDep" : "SETUP.IncompatibleDep";
const formatter = game.i18n.getListFormatter({ style: "short", type: "unit" });
return game.i18n.format(label, { dependencies: formatter.format(bad) });
}
/* -------------------------------------------- */
/**
* List any installed systems that are incompatible with this module's systems relationship, and format them for
* display.
* @param {Partial<PackageManifestData>} data The compatibility data.
* @param {Iterable<RelatedPackage>} relationships The system relationships.
* @param {object} [options]
* @param {Collection<string, System>} [options.systems] A specific collection of systems to test against. Tests
* against the currently installed systems by default.
* @returns {string}
* @protected
*/
static _formatIncompatibleSystemsTooltip(data, relationships, { systems }={}) {
systems ??= game.systems;
const incompatible = [];
for ( const { id, compatibility } of relationships ) {
const system = systems.get(id);
if ( !system ) continue;
if ( !this.testDependencyCompatibility(compatibility, system) || system.unavailable ) incompatible.push(id);
}
const label = incompatible.length ? "SETUP.IncompatibleSystems" : "SETUP.NoSupportedSystem";
const formatter = game.i18n.getListFormatter({ style: "short", type: "unit" });
return game.i18n.format(label, { systems: formatter.format(incompatible) });
}
/* ----------------------------------------- */
/**
* When a package has been installed, add it to the local game data.
*/
install() {
const collection = this.constructor.collection;
game.data[collection].push(this.toObject());
game[collection].set(this.id, this);
}
/* ----------------------------------------- */
/**
* When a package has been uninstalled, remove it from the local game data.
*/
uninstall() {
this.constructor.uninstall(this.id);
}
/* -------------------------------------------- */
/**
* Remove a package from the local game data when it has been uninstalled.
* @param {string} id The package ID.
*/
static uninstall(id) {
game.data[this.collection].findSplice(p => p.id === id);
game[this.collection].delete(id);
}
/* -------------------------------------------- */
/**
* Retrieve the latest Package manifest from a provided remote location.
* @param {string} manifest A remote manifest URL to load
* @param {object} options Additional options which affect package construction
* @param {boolean} [options.strict=true] Whether to construct the remote package strictly
* @returns {Promise<ClientPackage|null>} A Promise which resolves to a constructed ServerPackage instance
* @throws An error if the retrieved manifest data is invalid
*/
static async fromRemoteManifest(manifest, {strict=false}={}) {
try {
const data = await Setup.post({action: "getPackageFromRemoteManifest", type: this.type, manifest});
return new this(data, {installed: false, strict: strict});
}
catch(e) {
return null;
}
}
}
return ClientPackage;
}
/**
* @extends foundry.packages.BaseModule
* @mixes ClientPackageMixin
* @category - Packages
*/
class Module extends ClientPackageMixin(foundry.packages.BaseModule) {
constructor(data, options = {}) {
const {active} = data;
super(data, options);
/**
* Is this package currently active?
* @type {boolean}
*/
Object.defineProperty(this, "active", {value: active, writable: false});
}
}
/* ---------------------------------------- */
/**
* @extends foundry.packages.BaseSystem
* @mixes ClientPackageMixin
* @category - Packages
*/
class System extends ClientPackageMixin(foundry.packages.BaseSystem) {
constructor(data, options={}) {
options.strictDataCleaning = data.strictDataCleaning;
super(data, options);
}
/** @inheritDoc */
_configure(options) {
super._configure(options);
this.strictDataCleaning = !!options.strictDataCleaning;
}
/**
* @deprecated since v12
* @ignore
*/
get template() {
foundry.utils.logCompatibilityWarning("System#template is deprecated in favor of System#documentTypes",
{since: 12, until: 14});
return game.model;
}
}
/* ---------------------------------------- */
/**
* @extends foundry.packages.BaseWorld
* @mixes ClientPackageMixin
* @category - Packages
*/
class World extends ClientPackageMixin(foundry.packages.BaseWorld) {
/** @inheritDoc */
static getVersionBadge(availability, data, { modules, systems }={}) {
modules ??= game.modules;
systems ??= game.systems;
const badge = super.getVersionBadge(availability, data, { modules, systems });
if ( !badge ) return badge;
const codes = CONST.PACKAGE_AVAILABILITY_CODES;
if ( availability === codes.VERIFIED ) {
const system = systems.get(data.system);
if ( system.availability !== codes.VERIFIED ) badge.type = "neutral";
}
if ( !data.manifest ) badge.label = "";
return badge;
}
/* -------------------------------------------- */
/**
* Provide data for a system badge displayed for the world which reflects the system ID and its availability
* @param {System} [system] A specific system to use, otherwise use the installed system.
* @returns {PackageCompatibilityBadge|null}
*/
getSystemBadge(system) {
system ??= game.systems.get(this.system);
if ( !system ) return {
type: "error",
tooltip: game.i18n.format("SETUP.RequireSystem", { system: this.system }),
label: this.system,
icon: "fa fa-file-slash"
};
const badge = system.getVersionBadge();
if ( badge.type === "safe" ) {
badge.type = "neutral";
badge.icon = null;
}
badge.tooltip = `<p>${system.title}</p><p>${badge.tooltip}</p>`;
badge.label = system.id;
return badge;
}
/* -------------------------------------------- */
/** @inheritdoc */
static _formatBadDependenciesTooltip(availability, data, deps) {
const system = game.systems.get(data.system);
if ( system ) deps ??= [...data.relationships.requires.values(), ...system.relationships.requires.values()];
return super._formatBadDependenciesTooltip(availability, data, deps);
}
}
/* ---------------------------------------- */
/**
* A mapping of allowed package types and the classes which implement them.
* @type {{world: World, system: System, module: Module}}
*/
const PACKAGE_TYPES = {
world: World,
system: System,
module: Module
};