366 lines
13 KiB
JavaScript
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
|
|
};
|