Initial
This commit is contained in:
33
resources/app/common/packages/base-module.mjs
Normal file
33
resources/app/common/packages/base-module.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
import BasePackage from "./base-package.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import AdditionalTypesField from "./sub-types.mjs";
|
||||
|
||||
/**
|
||||
* The data schema used to define Module manifest files.
|
||||
* Extends the basic PackageData schema with some additional module-specific fields.
|
||||
* @property {boolean} [coreTranslation] Does this module provide a translation for the core software?
|
||||
* @property {boolean} [library] A library module provides no user-facing functionality and is solely
|
||||
* for use by other modules. Loaded before any system or module scripts.
|
||||
* @property {Record<string, string[]>} [documentTypes] Additional document subtypes provided by this module.
|
||||
*/
|
||||
export default class BaseModule extends BasePackage {
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const parentSchema = super.defineSchema();
|
||||
return Object.assign({}, parentSchema, {
|
||||
coreTranslation: new fields.BooleanField(),
|
||||
library: new fields.BooleanField(),
|
||||
documentTypes: new AdditionalTypesField()
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static type = "module";
|
||||
|
||||
/**
|
||||
* The default icon used for this type of Package.
|
||||
* @type {string}
|
||||
*/
|
||||
static icon = "fa-plug";
|
||||
}
|
||||
826
resources/app/common/packages/base-package.mjs
Normal file
826
resources/app/common/packages/base-package.mjs
Normal file
@@ -0,0 +1,826 @@
|
||||
import DataModel from "../abstract/data.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import {
|
||||
COMPENDIUM_DOCUMENT_TYPES, DOCUMENT_OWNERSHIP_LEVELS,
|
||||
PACKAGE_AVAILABILITY_CODES,
|
||||
PACKAGE_TYPES,
|
||||
SYSTEM_SPECIFIC_COMPENDIUM_TYPES,
|
||||
USER_ROLES
|
||||
} from "../constants.mjs";
|
||||
import {isNewerVersion, logCompatibilityWarning, mergeObject} from "../utils/module.mjs";
|
||||
import BaseFolder from "../documents/folder.mjs";
|
||||
import {ObjectField} from "../data/fields.mjs";
|
||||
import {DataModelValidationFailure} from "../data/validation-failure.mjs";
|
||||
|
||||
|
||||
/**
|
||||
* A custom SchemaField for defining package compatibility versions.
|
||||
* @property {string} minimum The Package will not function before this version
|
||||
* @property {string} verified Verified compatible up to this version
|
||||
* @property {string} maximum The Package will not function after this version
|
||||
*/
|
||||
export class PackageCompatibility extends fields.SchemaField {
|
||||
constructor(options) {
|
||||
super({
|
||||
minimum: new fields.StringField({required: false, blank: false, initial: undefined}),
|
||||
verified: new fields.StringField({required: false, blank: false, initial: undefined}),
|
||||
maximum: new fields.StringField({required: false, blank: false, initial: undefined})
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A custom SchemaField for defining package relationships.
|
||||
* @property {RelatedPackage[]} systems Systems that this Package supports
|
||||
* @property {RelatedPackage[]} requires Packages that are required for base functionality
|
||||
* @property {RelatedPackage[]} recommends Packages that are recommended for optimal functionality
|
||||
*/
|
||||
export class PackageRelationships extends fields.SchemaField {
|
||||
/** @inheritdoc */
|
||||
constructor(options) {
|
||||
super({
|
||||
systems: new PackageRelationshipField(new RelatedPackage({packageType: "system"})),
|
||||
requires: new PackageRelationshipField(new RelatedPackage()),
|
||||
recommends: new PackageRelationshipField(new RelatedPackage()),
|
||||
conflicts: new PackageRelationshipField(new RelatedPackage()),
|
||||
flags: new fields.ObjectField()
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A SetField with custom casting behavior.
|
||||
*/
|
||||
class PackageRelationshipField extends fields.SetField {
|
||||
/** @override */
|
||||
_cast(value) {
|
||||
return value instanceof Array ? value : [value];
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A custom SchemaField for defining a related Package.
|
||||
* It may be required to be a specific type of package, by passing the packageType option to the constructor.
|
||||
*/
|
||||
export class RelatedPackage extends fields.SchemaField {
|
||||
constructor({packageType, ...options}={}) {
|
||||
let typeOptions = {choices: PACKAGE_TYPES, initial:"module"};
|
||||
if ( packageType ) typeOptions = {choices: [packageType], initial: packageType};
|
||||
super({
|
||||
id: new fields.StringField({required: true, blank: false}),
|
||||
type: new fields.StringField(typeOptions),
|
||||
manifest: new fields.StringField({required: false, blank: false, initial: undefined}),
|
||||
compatibility: new PackageCompatibility(),
|
||||
reason: new fields.StringField({required: false, blank: false, initial: undefined})
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A custom SchemaField for defining the folder structure of the included compendium packs.
|
||||
*/
|
||||
export class PackageCompendiumFolder extends fields.SchemaField {
|
||||
constructor({depth=1, ...options}={}) {
|
||||
const schema = {
|
||||
name: new fields.StringField({required: true, blank: false}),
|
||||
sorting: new fields.StringField({required: false, blank: false, initial: undefined,
|
||||
choices: BaseFolder.SORTING_MODES}),
|
||||
color: new fields.ColorField(),
|
||||
packs: new fields.SetField(new fields.StringField({required: true, blank: false}))
|
||||
};
|
||||
if ( depth < 4 ) schema.folders = new fields.SetField(new PackageCompendiumFolder(
|
||||
{depth: depth+1, options}));
|
||||
super(schema, options);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A special ObjectField which captures a mapping of USER_ROLES to DOCUMENT_OWNERSHIP_LEVELS.
|
||||
*/
|
||||
export class CompendiumOwnershipField extends ObjectField {
|
||||
|
||||
/** @inheritdoc */
|
||||
static get _defaults() {
|
||||
return mergeObject(super._defaults, {
|
||||
initial: {PLAYER: "OBSERVER", ASSISTANT: "OWNER"},
|
||||
validationError: "is not a mapping of USER_ROLES to DOCUMENT_OWNERSHIP_LEVELS"
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_validateType(value, options) {
|
||||
for ( let [k, v] of Object.entries(value) ) {
|
||||
if ( !(k in USER_ROLES) ) throw new Error(`Compendium ownership key "${k}" is not a valid choice in USER_ROLES`);
|
||||
if ( !(v in DOCUMENT_OWNERSHIP_LEVELS) ) throw new Error(`Compendium ownership value "${v}" is not a valid
|
||||
choice in DOCUMENT_OWNERSHIP_LEVELS`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A special SetField which provides additional validation and initialization behavior specific to compendium packs.
|
||||
*/
|
||||
export class PackageCompendiumPacks extends fields.SetField {
|
||||
|
||||
/** @override */
|
||||
_cleanType(value, options) {
|
||||
return value.map(v => {
|
||||
v = this.element.clean(v, options);
|
||||
if ( v.path ) v.path = v.path.replace(/\.db$/, ""); // Strip old NEDB extensions
|
||||
else v.path = `packs/${v.name}`; // Auto-populate a default pack path
|
||||
return v;
|
||||
})
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
initialize(value, model, options={}) {
|
||||
const packs = new Set();
|
||||
const packageName = model._source.id;
|
||||
for ( let v of value ) {
|
||||
try {
|
||||
const pack = this.element.initialize(v, model, options);
|
||||
pack.packageType = model.constructor.type;
|
||||
pack.packageName = packageName;
|
||||
pack.id = `${model.constructor.type === "world" ? "world" : packageName}.${pack.name}`;
|
||||
packs.add(pack);
|
||||
} catch(err) {
|
||||
logger.warn(err.message);
|
||||
}
|
||||
}
|
||||
return packs;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Extend the logic for validating the complete set of packs to ensure uniqueness.
|
||||
* @inheritDoc
|
||||
*/
|
||||
_validateElements(value, options) {
|
||||
const packNames = new Set();
|
||||
const duplicateNames = new Set();
|
||||
const packPaths = new Set();
|
||||
const duplicatePaths = new Set();
|
||||
for ( const pack of value ) {
|
||||
if ( packNames.has(pack.name) ) duplicateNames.add(pack.name);
|
||||
packNames.add(pack.name);
|
||||
if ( pack.path ) {
|
||||
if ( packPaths.has(pack.path) ) duplicatePaths.add(pack.path);
|
||||
packPaths.add(pack.path);
|
||||
}
|
||||
}
|
||||
return super._validateElements(value, {...options, duplicateNames, duplicatePaths});
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate each individual compendium pack, ensuring its name and path are unique.
|
||||
* @inheritDoc
|
||||
*/
|
||||
_validateElement(value, {duplicateNames, duplicatePaths, ...options}={}) {
|
||||
if ( duplicateNames.has(value.name) ) {
|
||||
return new DataModelValidationFailure({
|
||||
invalidValue: value.name,
|
||||
message: `Duplicate Compendium name "${value.name}" already declared by some other pack`,
|
||||
unresolved: true
|
||||
});
|
||||
}
|
||||
if ( duplicatePaths.has(value.path) ) {
|
||||
return new DataModelValidationFailure({
|
||||
invalidValue: value.path,
|
||||
message: `Duplicate Compendium path "${value.path}" already declared by some other pack`,
|
||||
unresolved: true
|
||||
});
|
||||
}
|
||||
return this.element.validate(value, options);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The data schema used to define a Package manifest.
|
||||
* Specific types of packages extend this schema with additional fields.
|
||||
*/
|
||||
export default class BasePackage extends DataModel {
|
||||
/**
|
||||
* @param {PackageManifestData} data Source data for the package
|
||||
* @param {object} [options={}] Options which affect DataModel construction
|
||||
*/
|
||||
constructor(data, options={}) {
|
||||
const {availability, locked, exclusive, owned, tags, hasStorage} = data;
|
||||
super(data, options);
|
||||
|
||||
/**
|
||||
* An availability code in PACKAGE_AVAILABILITY_CODES which defines whether this package can be used.
|
||||
* @type {number}
|
||||
*/
|
||||
this.availability = availability ?? this.constructor.testAvailability(this);
|
||||
|
||||
/**
|
||||
* A flag which tracks whether this package is currently locked.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.locked = locked ?? false;
|
||||
|
||||
/**
|
||||
* A flag which tracks whether this package is a free Exclusive pack
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.exclusive = exclusive ?? false;
|
||||
|
||||
/**
|
||||
* A flag which tracks whether this package is owned, if it is protected.
|
||||
* @type {boolean|null}
|
||||
*/
|
||||
this.owned = owned ?? false;
|
||||
|
||||
/**
|
||||
* A set of Tags that indicate what kind of Package this is, provided by the Website
|
||||
* @type {string[]}
|
||||
*/
|
||||
this.tags = tags ?? [];
|
||||
|
||||
/**
|
||||
* A flag which tracks if this package has files stored in the persistent storage folder
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.hasStorage = hasStorage ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the package type in CONST.PACKAGE_TYPES that this class represents.
|
||||
* Each BasePackage subclass must define this attribute.
|
||||
* @virtual
|
||||
* @type {string}
|
||||
*/
|
||||
static type = "package";
|
||||
|
||||
/**
|
||||
* The type of this package instance. A value in CONST.PACKAGE_TYPES.
|
||||
* @type {string}
|
||||
*/
|
||||
get type() {
|
||||
return this.constructor.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* The canonical identifier for this package
|
||||
* @return {string}
|
||||
* @deprecated
|
||||
*/
|
||||
get name() {
|
||||
logCompatibilityWarning("You are accessing BasePackage#name which is now deprecated in favor of id.",
|
||||
{since: 10, until: 13});
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* A flag which defines whether this package is unavailable to be used.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get unavailable() {
|
||||
return this.availability > PACKAGE_AVAILABILITY_CODES.UNVERIFIED_GENERATION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this Package incompatible with the currently installed core Foundry VTT software version?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get incompatibleWithCoreVersion() {
|
||||
return this.constructor.isIncompatibleWithCoreVersion(this.availability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a given availability is incompatible with the core version.
|
||||
* @param {number} availability The availability value to test.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isIncompatibleWithCoreVersion(availability) {
|
||||
const codes = CONST.PACKAGE_AVAILABILITY_CODES;
|
||||
return (availability >= codes.REQUIRES_CORE_DOWNGRADE) && (availability <= codes.REQUIRES_CORE_UPGRADE_UNSTABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* The named collection to which this package type belongs
|
||||
* @type {string}
|
||||
*/
|
||||
static get collection() {
|
||||
return `${this.type}s`;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
const optionalString = {required: false, blank: false, initial: undefined};
|
||||
return {
|
||||
|
||||
// Package metadata
|
||||
id: new fields.StringField({required: true, blank: false, validate: this.validateId}),
|
||||
title: new fields.StringField({required: true, blank: false}),
|
||||
description: new fields.StringField({required: true}),
|
||||
authors: new fields.SetField(new fields.SchemaField({
|
||||
name: new fields.StringField({required: true, blank: false}),
|
||||
email: new fields.StringField(optionalString),
|
||||
url: new fields.StringField(optionalString),
|
||||
discord: new fields.StringField(optionalString),
|
||||
flags: new fields.ObjectField(),
|
||||
})),
|
||||
url: new fields.StringField(optionalString),
|
||||
license: new fields.StringField(optionalString),
|
||||
readme: new fields.StringField(optionalString),
|
||||
bugs: new fields.StringField(optionalString),
|
||||
changelog: new fields.StringField(optionalString),
|
||||
flags: new fields.ObjectField(),
|
||||
media: new fields.SetField(new fields.SchemaField({
|
||||
type: new fields.StringField(optionalString),
|
||||
url: new fields.StringField(optionalString),
|
||||
caption: new fields.StringField(optionalString),
|
||||
loop: new fields.BooleanField({required: false, blank: false, initial: false}),
|
||||
thumbnail: new fields.StringField(optionalString),
|
||||
flags: new fields.ObjectField(),
|
||||
})),
|
||||
|
||||
// Package versioning
|
||||
version: new fields.StringField({required: true, blank: false, initial: "0"}),
|
||||
compatibility: new PackageCompatibility(),
|
||||
|
||||
// Included content
|
||||
scripts: new fields.SetField(new fields.StringField({required: true, blank: false})),
|
||||
esmodules: new fields.SetField(new fields.StringField({required: true, blank: false})),
|
||||
styles: new fields.SetField(new fields.StringField({required: true, blank: false})),
|
||||
languages: new fields.SetField(new fields.SchemaField({
|
||||
lang: new fields.StringField({required: true, blank: false, validate: Intl.getCanonicalLocales,
|
||||
validationError: "must be supported by the Intl.getCanonicalLocales function"
|
||||
}),
|
||||
name: new fields.StringField({required: false}),
|
||||
path: new fields.StringField({required: true, blank: false}),
|
||||
system: new fields.StringField(optionalString),
|
||||
module: new fields.StringField(optionalString),
|
||||
flags: new fields.ObjectField(),
|
||||
})),
|
||||
packs: new PackageCompendiumPacks(new fields.SchemaField({
|
||||
name: new fields.StringField({required: true, blank: false, validate: this.validateId}),
|
||||
label: new fields.StringField({required: true, blank: false}),
|
||||
banner: new fields.StringField({...optionalString, nullable: true}),
|
||||
path: new fields.StringField({required: false}),
|
||||
type: new fields.StringField({required: true, blank: false, choices: COMPENDIUM_DOCUMENT_TYPES,
|
||||
validationError: "must be a value in CONST.COMPENDIUM_DOCUMENT_TYPES"}),
|
||||
system: new fields.StringField(optionalString),
|
||||
ownership: new CompendiumOwnershipField(),
|
||||
flags: new fields.ObjectField(),
|
||||
}, {validate: BasePackage.#validatePack})),
|
||||
packFolders: new fields.SetField(new PackageCompendiumFolder()),
|
||||
|
||||
// Package relationships
|
||||
relationships: new PackageRelationships(),
|
||||
socket: new fields.BooleanField(),
|
||||
|
||||
// Package downloading
|
||||
manifest: new fields.StringField(),
|
||||
download: new fields.StringField({required: false, blank: false, initial: undefined}),
|
||||
protected: new fields.BooleanField(),
|
||||
exclusive: new fields.BooleanField(),
|
||||
persistentStorage: new fields.BooleanField(),
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Check the given compatibility data against the current installation state and determine its availability.
|
||||
* @param {Partial<PackageManifestData>} data The compatibility data to test.
|
||||
* @param {object} [options]
|
||||
* @param {ReleaseData} [options.release] A specific software release for which to test availability.
|
||||
* Tests against the current release by default.
|
||||
* @returns {number}
|
||||
*/
|
||||
static testAvailability({ compatibility }, { release }={}) {
|
||||
release ??= globalThis.release ?? game.release;
|
||||
const codes = CONST.PACKAGE_AVAILABILITY_CODES;
|
||||
const {minimum, maximum, verified} = compatibility;
|
||||
const isGeneration = version => Number.isInteger(Number(version));
|
||||
|
||||
// Require a certain minimum core version.
|
||||
if ( minimum && isNewerVersion(minimum, release.version) ) {
|
||||
const generation = Number(minimum.split(".").shift());
|
||||
const isStable = generation <= release.maxStableGeneration;
|
||||
const exists = generation <= release.maxGeneration;
|
||||
if ( isStable ) return codes.REQUIRES_CORE_UPGRADE_STABLE;
|
||||
return exists ? codes.REQUIRES_CORE_UPGRADE_UNSTABLE : codes.UNKNOWN;
|
||||
}
|
||||
|
||||
// Require a certain maximum core version.
|
||||
if ( maximum ) {
|
||||
const compatible = isGeneration(maximum)
|
||||
? release.generation <= Number(maximum)
|
||||
: !isNewerVersion(release.version, maximum);
|
||||
if ( !compatible ) return codes.REQUIRES_CORE_DOWNGRADE;
|
||||
}
|
||||
|
||||
// Require a certain compatible core version.
|
||||
if ( verified ) {
|
||||
const compatible = isGeneration(verified)
|
||||
? Number(verified) >= release.generation
|
||||
: !isNewerVersion(release.version, verified);
|
||||
const sameGeneration = release.generation === Number(verified.split(".").shift());
|
||||
if ( compatible ) return codes.VERIFIED;
|
||||
return sameGeneration ? codes.UNVERIFIED_BUILD : codes.UNVERIFIED_GENERATION;
|
||||
}
|
||||
|
||||
// FIXME: Why do we not check if all of this package's dependencies are satisfied?
|
||||
// Proposal: Check all relationships.requires and set MISSING_DEPENDENCY if any dependencies are not VERIFIED,
|
||||
// UNVERIFIED_BUILD, or UNVERIFIED_GENERATION, or if they do not satisfy the given compatibility range for the
|
||||
// relationship.
|
||||
|
||||
// No compatible version is specified.
|
||||
return codes.UNKNOWN;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Test that the dependencies of a package are satisfied as compatible.
|
||||
* This method assumes that all packages in modulesCollection have already had their own availability tested.
|
||||
* @param {Collection<string,Module>} modulesCollection A collection which defines the set of available modules
|
||||
* @returns {Promise<boolean>} Are all required dependencies satisfied?
|
||||
* @internal
|
||||
*/
|
||||
async _testRequiredDependencies(modulesCollection) {
|
||||
const requirements = this.relationships.requires;
|
||||
for ( const {id, type, manifest, compatibility} of requirements ) {
|
||||
if ( type !== "module" ) continue; // Only test modules
|
||||
let pkg;
|
||||
|
||||
// If the requirement specifies an explicit remote manifest URL, we need to load it
|
||||
if ( manifest ) {
|
||||
try {
|
||||
pkg = await this.constructor.fromRemoteManifest(manifest, {strict: true});
|
||||
} catch(err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise the dependency must belong to the known modulesCollection
|
||||
else pkg = modulesCollection.get(id);
|
||||
if ( !pkg ) return false;
|
||||
|
||||
// Ensure that the package matches the required compatibility range
|
||||
if ( !this.constructor.testDependencyCompatibility(compatibility, pkg) ) return false;
|
||||
|
||||
// Test compatibility of the dependency
|
||||
if ( pkg.unavailable ) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Test compatibility of a package's supported systems.
|
||||
* @param {Collection<string, System>} systemCollection A collection which defines the set of available systems.
|
||||
* @returns {Promise<boolean>} True if all supported systems which are currently installed
|
||||
* are compatible or if the package has no supported systems.
|
||||
* Returns false otherwise, or if no supported systems are
|
||||
* installed.
|
||||
* @internal
|
||||
*/
|
||||
async _testSupportedSystems(systemCollection) {
|
||||
const systems = this.relationships.systems;
|
||||
if ( !systems?.size ) return true;
|
||||
let supportedSystem = false;
|
||||
for ( const { id, compatibility } of systems ) {
|
||||
const pkg = systemCollection.get(id);
|
||||
if ( !pkg ) continue;
|
||||
if ( !this.constructor.testDependencyCompatibility(compatibility, pkg) || pkg.unavailable ) return false;
|
||||
supportedSystem = true;
|
||||
}
|
||||
return supportedSystem;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine if a dependency is within the given compatibility range.
|
||||
* @param {PackageCompatibility} compatibility The compatibility range declared for the dependency, if any
|
||||
* @param {BasePackage} dependency The known dependency package
|
||||
* @returns {boolean} Is the dependency compatible with the required range?
|
||||
*/
|
||||
static testDependencyCompatibility(compatibility, dependency) {
|
||||
if ( !compatibility ) return true;
|
||||
const {minimum, maximum} = compatibility;
|
||||
if ( minimum && isNewerVersion(minimum, dependency.version) ) return false;
|
||||
if ( maximum && isNewerVersion(dependency.version, maximum) ) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static cleanData(source={}, { installed, ...options }={}) {
|
||||
|
||||
// Auto-assign language name
|
||||
for ( let l of source.languages || [] ) {
|
||||
l.name = l.name ?? l.lang;
|
||||
}
|
||||
|
||||
// Identify whether this package depends on a single game system
|
||||
let systemId = undefined;
|
||||
if ( this.type === "system" ) systemId = source.id;
|
||||
else if ( this.type === "world" ) systemId = source.system;
|
||||
else if ( source.relationships?.systems?.length === 1 ) systemId = source.relationships.systems[0].id;
|
||||
|
||||
// Auto-configure some package data
|
||||
for ( const pack of source.packs || [] ) {
|
||||
if ( !pack.system && systemId ) pack.system = systemId; // System dependency
|
||||
if ( typeof pack.ownership === "string" ) pack.ownership = {PLAYER: pack.ownership};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean unsupported non-module dependencies in requires or recommends.
|
||||
* @deprecated since v11
|
||||
*/
|
||||
["requires", "recommends"].forEach(rel => {
|
||||
const pkgs = source.relationships?.[rel];
|
||||
if ( !Array.isArray(pkgs) ) return;
|
||||
const clean = [];
|
||||
for ( const pkg of pkgs ) {
|
||||
if ( !pkg.type || (pkg.type === "module") ) clean.push(pkg);
|
||||
}
|
||||
const diff = pkgs.length - clean.length;
|
||||
if ( diff ) {
|
||||
source.relationships[rel] = clean;
|
||||
this._logWarning(
|
||||
source.id,
|
||||
`The ${this.type} "${source.id}" has a ${rel} relationship on a non-module, which is not supported.`,
|
||||
{ since: 11, until: 13, stack: false, installed });
|
||||
}
|
||||
});
|
||||
return super.cleanData(source, options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate that a Package ID is allowed.
|
||||
* @param {string} id The candidate ID
|
||||
* @throws An error if the candidate ID is invalid
|
||||
*/
|
||||
static validateId(id) {
|
||||
const allowed = /^[A-Za-z0-9-_]+$/;
|
||||
if ( !allowed.test(id) ) {
|
||||
throw new Error("Package and compendium pack IDs may only be alphanumeric with hyphens or underscores.");
|
||||
}
|
||||
const prohibited = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
||||
if ( prohibited.test(id) ) throw new Error(`The ID "${id}" uses an operating system prohibited value.`);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate a single compendium pack object
|
||||
* @param {PackageCompendiumData} packData Candidate compendium packs data
|
||||
* @throws An error if the data is invalid
|
||||
*/
|
||||
static #validatePack(packData) {
|
||||
if ( SYSTEM_SPECIFIC_COMPENDIUM_TYPES.includes(packData.type) && !packData.system ) {
|
||||
throw new Error(`The Compendium pack "${packData.name}" of the "${packData.type}" type must declare the "system"`
|
||||
+ " upon which it depends.");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A wrapper around the default compatibility warning logger which handles some package-specific interactions.
|
||||
* @param {string} packageId The package ID being logged
|
||||
* @param {string} message The warning or error being logged
|
||||
* @param {object} options Logging options passed to foundry.utils.logCompatibilityWarning
|
||||
* @param {object} [options.installed] Is the package installed?
|
||||
* @internal
|
||||
*/
|
||||
static _logWarning(packageId, message, { installed, ...options }={}) {
|
||||
logCompatibilityWarning(message, options);
|
||||
if ( installed ) globalThis.packages?.warnings?.add(packageId, {type: this.type, level: "warning", message});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A set of package manifest keys that are migrated.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
static migratedKeys = new Set([
|
||||
/** @deprecated since 10 until 13 */
|
||||
"name", "dependencies", "minimumCoreVersion", "compatibleCoreVersion"
|
||||
]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static migrateData(data, { installed }={}) {
|
||||
this._migrateNameToId(data, {since: 10, until: 13, stack: false, installed});
|
||||
this._migrateDependenciesNameToId(data, {since: 10, until: 13, stack: false, installed});
|
||||
this._migrateToRelationships(data, {since: 10, until: 13, stack: false, installed});
|
||||
this._migrateCompatibility(data, {since: 10, until: 13, stack: false, installed});
|
||||
this._migrateMediaURL(data, {since: 11, until: 13, stack: false, installed});
|
||||
this._migrateOwnership(data, {since: 11, until: 13, stack: false, installed});
|
||||
this._migratePackIDs(data, {since: 12, until: 14, stack: false, installed});
|
||||
this._migratePackEntityToType(data, {since: 9, stack: false, installed});
|
||||
return super.migrateData(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @internal */
|
||||
static _migrateNameToId(data, logOptions) {
|
||||
if ( data.name && !data.id ) {
|
||||
data.id = data.name;
|
||||
delete data.name;
|
||||
if ( this.type !== "world" ) {
|
||||
const warning = `The ${this.type} "${data.id}" is using "name" which is deprecated in favor of "id"`;
|
||||
this._logWarning(data.id, warning, logOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @internal */
|
||||
static _migrateDependenciesNameToId(data, logOptions) {
|
||||
if ( data.relationships ) return;
|
||||
if ( data.dependencies ) {
|
||||
let hasDependencyName = false;
|
||||
for ( const dependency of data.dependencies ) {
|
||||
if ( dependency.name && !dependency.id ) {
|
||||
hasDependencyName = true;
|
||||
dependency.id = dependency.name;
|
||||
delete dependency.name;
|
||||
}
|
||||
}
|
||||
if ( hasDependencyName ) {
|
||||
const msg = `The ${this.type} "${data.id}" contains dependencies using "name" which is deprecated in favor of "id"`;
|
||||
this._logWarning(data.id, msg, logOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @internal */
|
||||
static _migrateToRelationships(data, logOptions) {
|
||||
if ( data.relationships ) return;
|
||||
data.relationships = {
|
||||
requires: [],
|
||||
systems: []
|
||||
};
|
||||
|
||||
// Dependencies -> Relationships.Requires
|
||||
if ( data.dependencies ) {
|
||||
for ( const d of data.dependencies ) {
|
||||
const relationship = {
|
||||
"id": d.id,
|
||||
"type": d.type,
|
||||
"manifest": d.manifest,
|
||||
"compatibility": {
|
||||
"compatible": d.version
|
||||
}
|
||||
};
|
||||
d.type === "system" ? data.relationships.systems.push(relationship) : data.relationships.requires.push(relationship);
|
||||
}
|
||||
const msg = `The ${this.type} "${data.id}" contains "dependencies" which is deprecated in favor of "relationships.requires"`;
|
||||
this._logWarning(data.id, msg, logOptions);
|
||||
delete data.dependencies;
|
||||
}
|
||||
|
||||
// V9: system -> relationships.systems
|
||||
else if ( data.system && (this.type === "module") ) {
|
||||
data.system = data.system instanceof Array ? data.system : [data.system];
|
||||
const newSystems = data.system.map(id => ({id})).filter(s => !data.relationships.systems.find(x => x.id === s.id));
|
||||
data.relationships.systems = data.relationships.systems.concat(newSystems);
|
||||
const msg = `${this.type} "${data.id}" contains "system" which is deprecated in favor of "relationships.systems"`;
|
||||
this._logWarning(data.id, msg, logOptions);
|
||||
delete data.system;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @internal */
|
||||
static _migrateCompatibility(data, logOptions) {
|
||||
if ( !data.compatibility && (data.minimumCoreVersion || data.compatibleCoreVersion) ) {
|
||||
this._logWarning(data.id, `The ${this.type} "${data.id}" is using the old flat core compatibility fields which `
|
||||
+ `are deprecated in favor of the new "compatibility" object`,
|
||||
logOptions);
|
||||
data.compatibility = {
|
||||
minimum: data.minimumCoreVersion,
|
||||
verified: data.compatibleCoreVersion
|
||||
};
|
||||
delete data.minimumCoreVersion;
|
||||
delete data.compatibleCoreVersion;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @internal */
|
||||
static _migrateMediaURL(data, logOptions) {
|
||||
if ( !data.media ) return;
|
||||
let hasMediaLink = false;
|
||||
for ( const media of data.media ) {
|
||||
if ( "link" in media ) {
|
||||
hasMediaLink = true;
|
||||
media.url = media.link;
|
||||
delete media.link;
|
||||
}
|
||||
}
|
||||
if ( hasMediaLink ) {
|
||||
const msg = `${this.type} "${data.id}" declares media.link which is unsupported, media.url should be used`;
|
||||
this._logWarning(data.id, msg, logOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @internal */
|
||||
static _migrateOwnership(data, logOptions) {
|
||||
if ( !data.packs ) return;
|
||||
let hasPrivatePack = false;
|
||||
for ( const pack of data.packs ) {
|
||||
if ( pack.private && !("ownership" in pack) ) {
|
||||
pack.ownership = {PLAYER: "LIMITED", ASSISTANT: "OWNER"};
|
||||
hasPrivatePack = true;
|
||||
}
|
||||
delete pack.private;
|
||||
}
|
||||
if ( hasPrivatePack ) {
|
||||
const msg = `${this.type} "${data.id}" uses pack.private which has been replaced with pack.ownership`;
|
||||
this._logWarning(data.id, msg, logOptions);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @internal */
|
||||
static _migratePackIDs(data, logOptions) {
|
||||
if ( !data.packs ) return;
|
||||
for ( const pack of data.packs ) {
|
||||
const slugified = pack.name.replace(/[^A-Za-z0-9-_]/g, "");
|
||||
if ( pack.name !== slugified ) {
|
||||
const msg = `The ${this.type} "${data.id}" contains a pack with an invalid name "${pack.name}". `
|
||||
+ "Pack names containing any character that is non-alphanumeric or an underscore will cease loading in "
|
||||
+ "version 14 of the software.";
|
||||
pack.name = slugified;
|
||||
this._logWarning(data.id, msg, logOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @internal */
|
||||
static _migratePackEntityToType(data, logOptions) {
|
||||
if ( !data.packs ) return;
|
||||
let hasPackEntity = false;
|
||||
for ( const pack of data.packs ) {
|
||||
if ( ("entity" in pack) && !("type" in pack) ) {
|
||||
pack.type = pack.entity;
|
||||
hasPackEntity = true;
|
||||
}
|
||||
delete pack.entity;
|
||||
}
|
||||
if ( hasPackEntity ) {
|
||||
const msg = `${this.type} "${data.id}" uses pack.entity which has been replaced with pack.type`;
|
||||
this._logWarning(data.id, msg, logOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Retrieve the latest Package manifest from a provided remote location.
|
||||
* @param {string} manifestUrl 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
|
||||
* @return {Promise<ServerPackage>} A Promise which resolves to a constructed ServerPackage instance
|
||||
* @throws An error if the retrieved manifest data is invalid
|
||||
*/
|
||||
static async fromRemoteManifest(manifestUrl, {strict=true}={}) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
150
resources/app/common/packages/base-system.mjs
Normal file
150
resources/app/common/packages/base-system.mjs
Normal file
@@ -0,0 +1,150 @@
|
||||
import BasePackage from "./base-package.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import AdditionalTypesField from "./sub-types.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./sub-types.mjs").DocumentTypesConfiguration} DocumentTypesConfiguration
|
||||
*/
|
||||
|
||||
/**
|
||||
* The data schema used to define System manifest files.
|
||||
* Extends the basic PackageData schema with some additional system-specific fields.
|
||||
* @property {DocumentTypesConfiguration} [documentTypes] Additional document subtypes provided by this system.
|
||||
* @property {string} [background] A web URL or local file path which provides a default background banner for
|
||||
* worlds which are created using this system
|
||||
* @property {string} [initiative] A default initiative formula used for this system
|
||||
* @property {number} [grid] The default grid settings to use for Scenes in this system
|
||||
* @property {number} [grid.type] A default grid type to use for Scenes in this system
|
||||
* @property {number} [grid.distance] A default distance measurement to use for Scenes in this system
|
||||
* @property {string} [grid.units] A default unit of measure to use for distance measurement in this system
|
||||
* @property {number} [grid.diagonals] The default rule used by this system for diagonal measurement on square grids
|
||||
* @property {string} [primaryTokenAttribute] An Actor data attribute path to use for Token primary resource bars
|
||||
* @property {string} [secondaryTokenAttribute] An Actor data attribute path to use for Token secondary resource bars
|
||||
*/
|
||||
export default class BaseSystem extends BasePackage {
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
return Object.assign({}, super.defineSchema(), {
|
||||
documentTypes: new AdditionalTypesField(),
|
||||
background: new fields.StringField({required: false, blank: false}),
|
||||
initiative: new fields.StringField(),
|
||||
grid: new fields.SchemaField({
|
||||
type: new fields.NumberField({required: true, choices: Object.values(CONST.GRID_TYPES),
|
||||
initial: CONST.GRID_TYPES.SQUARE, validationError: "must be a value in CONST.GRID_TYPES"}),
|
||||
distance: new fields.NumberField({required: true, nullable: false, positive: true, initial: 1}),
|
||||
units: new fields.StringField({required: true}),
|
||||
diagonals: new fields.NumberField({required: true, choices: Object.values(CONST.GRID_DIAGONALS),
|
||||
initial: CONST.GRID_DIAGONALS.EQUIDISTANT, validationError: "must be a value in CONST.GRID_DIAGONALS"}),
|
||||
}),
|
||||
primaryTokenAttribute: new fields.StringField(),
|
||||
secondaryTokenAttribute: new fields.StringField()
|
||||
});
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static type = "system";
|
||||
|
||||
/**
|
||||
* The default icon used for this type of Package.
|
||||
* @type {string}
|
||||
*/
|
||||
static icon = "fa-dice";
|
||||
|
||||
/**
|
||||
* Does the system template request strict type checking of data compared to template.json inferred types.
|
||||
* @type {boolean}
|
||||
*/
|
||||
strictDataCleaning = false;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Static initializer block for deprecated properties.
|
||||
*/
|
||||
static {
|
||||
/**
|
||||
* Shim grid distance and units.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
Object.defineProperties(this.prototype, Object.fromEntries(
|
||||
Object.entries({
|
||||
gridDistance: "grid.distance",
|
||||
gridUnits: "grid.units"
|
||||
}).map(([o, n]) => [o, {
|
||||
get() {
|
||||
const msg = `You are accessing BasePackage#${o} which has been migrated to BasePackage#${n}.`;
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||||
return foundry.utils.getProperty(this, n);
|
||||
},
|
||||
set(v) {
|
||||
const msg = `You are accessing BasePackage#${o} which has been migrated to BasePackage#${n}.`;
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||||
return foundry.utils.setProperty(this, n, v);
|
||||
},
|
||||
configurable: true
|
||||
}])
|
||||
));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static migratedKeys = (function() {
|
||||
return BasePackage.migratedKeys.union(new Set([
|
||||
/** @deprecated since 12 until 14 */
|
||||
"gridDistance", "gridUnits"
|
||||
]));
|
||||
})();
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static migrateData(data, options) {
|
||||
/**
|
||||
* Migrate grid distance and units.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
for ( const [oldKey, [newKey, apply]] of Object.entries({
|
||||
gridDistance: ["grid.distance", d => Math.max(d.gridDistance || 0, 1)],
|
||||
gridUnits: ["grid.units", d => d.gridUnits || ""]
|
||||
})) {
|
||||
if ( (oldKey in data) && !foundry.utils.hasProperty(data, newKey) ) {
|
||||
foundry.utils.setProperty(data, newKey, apply(data));
|
||||
delete data[oldKey];
|
||||
const warning = `The ${this.type} "${data.id}" is using "${oldKey}" which is deprecated in favor of "${newKey}".`;
|
||||
this._logWarning(data.id, warning, {since: 12, until: 14, stack: false, installed: options.installed});
|
||||
}
|
||||
}
|
||||
return super.migrateData(data, options);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static shimData(data, options) {
|
||||
/**
|
||||
* Shim grid distance and units.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
for ( const [oldKey, newKey] of Object.entries({
|
||||
gridDistance: "grid.distance",
|
||||
gridUnits: "grid.units"
|
||||
})) {
|
||||
if ( !data.hasOwnProperty(oldKey) && foundry.utils.hasProperty(data, newKey) ) {
|
||||
Object.defineProperty(data, oldKey, {
|
||||
get: () => {
|
||||
const msg = `You are accessing BasePackage#${oldKey} which has been migrated to BasePackage#${newKey}.`;
|
||||
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||||
return foundry.utils.getProperty(data, newKey);
|
||||
},
|
||||
set: value => foundry.utils.setProperty(data, newKey, value),
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
return super.shimData(data, options);
|
||||
}
|
||||
}
|
||||
119
resources/app/common/packages/base-world.mjs
Normal file
119
resources/app/common/packages/base-world.mjs
Normal file
@@ -0,0 +1,119 @@
|
||||
import BasePackage from "./base-package.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import {WORLD_JOIN_THEMES} from "../constants.mjs";
|
||||
|
||||
/**
|
||||
* The data schema used to define World manifest files.
|
||||
* Extends the basic PackageData schema with some additional world-specific fields.
|
||||
* @property {string} system The game system name which this world relies upon
|
||||
* @property {string} coreVersion The version of the core software for which this world has been migrated
|
||||
* @property {string} systemVersion The version of the game system for which this world has been migrated
|
||||
* @property {string} [background] A web URL or local file path which provides a background banner image
|
||||
* @property {string} [nextSession] An ISO datetime string when the next game session is scheduled to occur
|
||||
* @property {boolean} [resetKeys] Should user access keys be reset as part of the next launch?
|
||||
* @property {boolean} [safeMode] Should the world launch in safe mode?
|
||||
* @property {string} [joinTheme] The theme to use for this world's join page.
|
||||
*/
|
||||
export default class BaseWorld extends BasePackage {
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
return Object.assign({}, super.defineSchema(), {
|
||||
system: new fields.StringField({required: true, blank: false}),
|
||||
background: new fields.StringField({required: false, blank: false}),
|
||||
joinTheme: new fields.StringField({
|
||||
required: false, initial: undefined, nullable: false, blank: false, choices: WORLD_JOIN_THEMES
|
||||
}),
|
||||
coreVersion: new fields.StringField({required: true, blank: false}),
|
||||
systemVersion: new fields.StringField({required: true, blank: false, initial: "0"}),
|
||||
lastPlayed: new fields.StringField(),
|
||||
playtime: new fields.NumberField({integer: true, min: 0, initial: 0}),
|
||||
nextSession: new fields.StringField({blank: false, nullable: true, initial: null}),
|
||||
resetKeys: new fields.BooleanField({required: false, initial: undefined}),
|
||||
safeMode: new fields.BooleanField({required: false, initial: undefined}),
|
||||
version: new fields.StringField({required: true, blank: false, nullable: true, initial: null})
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static type = "world";
|
||||
|
||||
/**
|
||||
* The default icon used for this type of Package.
|
||||
* @type {string}
|
||||
*/
|
||||
static icon = "fa-globe-asia";
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(data) {
|
||||
super.migrateData(data);
|
||||
|
||||
// Legacy compatibility strings
|
||||
data.compatibility = data.compatibility || {};
|
||||
if ( data.compatibility.maximum === "1.0.0" ) data.compatibility.maximum = undefined;
|
||||
if ( data.coreVersion && !data.compatibility.verified ) {
|
||||
data.compatibility.minimum = data.compatibility.verified = data.coreVersion;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Check the given compatibility data against the current installation state and determine its availability.
|
||||
* @param {Partial<PackageManifestData>} data The compatibility data to test.
|
||||
* @param {object} [options]
|
||||
* @param {ReleaseData} [options.release] A specific software release for which to test availability.
|
||||
* Tests against the current release by default.
|
||||
* @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.
|
||||
* @param {number} [options.systemAvailabilityThreshold] Ignore the world's own core software compatibility and
|
||||
* instead defer entirely to the system's core software
|
||||
* compatibility, if the world's availability is less than
|
||||
* this.
|
||||
* @returns {number}
|
||||
*/
|
||||
static testAvailability(data, { release, modules, systems, systemAvailabilityThreshold }={}) {
|
||||
systems ??= globalThis.packages?.System ?? game.systems;
|
||||
modules ??= globalThis.packages?.Module ?? game.modules;
|
||||
const { relationships } = data;
|
||||
const codes = CONST.PACKAGE_AVAILABILITY_CODES;
|
||||
systemAvailabilityThreshold ??= codes.UNKNOWN
|
||||
|
||||
// If the World itself is incompatible for some reason, report that directly.
|
||||
const wa = super.testAvailability(data, { release });
|
||||
if ( this.isIncompatibleWithCoreVersion(wa) ) return wa;
|
||||
|
||||
// If the System is missing or incompatible, report that directly.
|
||||
const system = data.system instanceof foundry.packages.BaseSystem ? data.system : systems.get(data.system);
|
||||
if ( !system ) return codes.MISSING_SYSTEM;
|
||||
const sa = system.availability;
|
||||
// FIXME: Why do we only check if the system is incompatible with the core version or UNKNOWN?
|
||||
// Proposal: If the system is anything but VERIFIED, UNVERIFIED_BUILD, or UNVERIFIED_GENERATION, we should return
|
||||
// the system availability.
|
||||
if ( system.incompatibleWithCoreVersion || (sa === codes.UNKNOWN) ) return sa;
|
||||
|
||||
// Test the availability of all required modules.
|
||||
const checkedModules = new Set();
|
||||
// TODO: We do not need to check system requirements here if the above proposal is implemented.
|
||||
const requirements = [...relationships.requires.values(), ...system.relationships.requires.values()];
|
||||
for ( const r of requirements ) {
|
||||
if ( (r.type !== "module") || checkedModules.has(r.id) ) continue;
|
||||
const module = modules.get(r.id);
|
||||
if ( !module ) return codes.MISSING_DEPENDENCY;
|
||||
// FIXME: Why do we only check if the module is incompatible with the core version?
|
||||
// Proposal: We should check the actual compatibility information for the relationship to ensure that the module
|
||||
// satisfies it.
|
||||
if ( module.incompatibleWithCoreVersion ) return codes.REQUIRES_DEPENDENCY_UPDATE;
|
||||
checkedModules.add(r.id);
|
||||
}
|
||||
|
||||
// Inherit from the System availability in certain cases.
|
||||
if ( wa <= systemAvailabilityThreshold ) return sa;
|
||||
return wa;
|
||||
}
|
||||
}
|
||||
76
resources/app/common/packages/module.mjs
Normal file
76
resources/app/common/packages/module.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
/** @module packages */
|
||||
|
||||
export {default as BasePackage} from "./base-package.mjs";
|
||||
export {default as BaseWorld} from "./base-world.mjs";
|
||||
export {default as BaseSystem} from "./base-system.mjs";
|
||||
export {default as BaseModule} from "./base-module.mjs";
|
||||
export {PackageCompatibility, RelatedPackage} from "./base-package.mjs";
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* Type Definitions */
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* @typedef {Object} PackageAuthorData
|
||||
* @property {string} name The author name
|
||||
* @property {string} [email] The author email address
|
||||
* @property {string} [url] A website url for the author
|
||||
* @property {string} [discord] A Discord username for the author
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PackageCompendiumData
|
||||
* @property {string} name The canonical compendium name. This should contain no spaces or special characters
|
||||
* @property {string} label The human-readable compendium name
|
||||
* @property {string} path The local relative path to the compendium source directory. The filename should match
|
||||
* the name attribute
|
||||
* @property {string} type The specific document type that is contained within this compendium pack
|
||||
* @property {string} [system] Denote that this compendium pack requires a specific game system to function properly
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PackageLanguageData
|
||||
* @property {string} lang A string language code which is validated by Intl.getCanonicalLocales
|
||||
* @property {string} name The human-readable language name
|
||||
* @property {string} path The relative path to included JSON translation strings
|
||||
* @property {string} [system] Only apply this set of translations when a specific system is being used
|
||||
* @property {string} [module] Only apply this set of translations when a specific module is active
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} RelatedPackage
|
||||
* @property {string} id The id of the related package
|
||||
* @property {string} type The type of the related package
|
||||
* @property {string} [manifest] An explicit manifest URL, otherwise learned from the Foundry web server
|
||||
* @property {PackageCompatibility} [compatibility] The compatibility data with this related Package
|
||||
* @property {string} [reason] The reason for this relationship
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PackageManifestData
|
||||
* The data structure of a package manifest. This data structure is extended by BasePackage subclasses to add additional
|
||||
* type-specific fields.
|
||||
* [[include:full-manifest.md]]
|
||||
*
|
||||
* @property {string} id The machine-readable unique package id, should be lower-case with no spaces or special characters
|
||||
* @property {string} title The human-readable package title, containing spaces and special characters
|
||||
* @property {string} [description] An optional package description, may contain HTML
|
||||
* @property {PackageAuthorData[]} [authors] An array of author objects who are co-authors of this package. Preferred to the singular author field.
|
||||
* @property {string} [url] A web url where more details about the package may be found
|
||||
* @property {string} [license] A web url or relative file path where license details may be found
|
||||
* @property {string} [readme] A web url or relative file path where readme instructions may be found
|
||||
* @property {string} [bugs] A web url where bug reports may be submitted and tracked
|
||||
* @property {string} [changelog] A web url where notes detailing package updates are available
|
||||
* @property {string} version The current package version
|
||||
* @property {PackageCompatibility} [compatibility] The compatibility of this version with the core Foundry software
|
||||
* @property {string[]} [scripts] An array of urls or relative file paths for JavaScript files which should be included
|
||||
* @property {string[]} [esmodules] An array of urls or relative file paths for ESModule files which should be included
|
||||
* @property {string[]} [styles] An array of urls or relative file paths for CSS stylesheet files which should be included
|
||||
* @property {PackageLanguageData[]} [languages] An array of language data objects which are included by this package
|
||||
* @property {PackageCompendiumData[]} [packs] An array of compendium packs which are included by this package
|
||||
* @property {PackageRelationships} [relationships] An organized object of relationships to other Packages
|
||||
* @property {boolean} [socket] Whether to require a package-specific socket namespace for this package
|
||||
* @property {string} [manifest] A publicly accessible web URL which provides the latest available package manifest file. Required in order to support module updates.
|
||||
* @property {string} [download] A publicly accessible web URL where the source files for this package may be downloaded. Required in order to support module installation.
|
||||
* @property {boolean} [protected=false] Whether this package uses the protected content access system.
|
||||
*/
|
||||
56
resources/app/common/packages/sub-types.mjs
Normal file
56
resources/app/common/packages/sub-types.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
import {getType, mergeObject} from "../utils/helpers.mjs";
|
||||
import {ObjectField} from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {Record<string, Record<string, object>>} DocumentTypesConfiguration
|
||||
*/
|
||||
|
||||
/**
|
||||
* A special [ObjectField]{@link ObjectField} available to packages which configures any additional Document subtypes
|
||||
* provided by the package.
|
||||
*/
|
||||
export default class AdditionalTypesField extends ObjectField {
|
||||
|
||||
/** @inheritDoc */
|
||||
static get _defaults() {
|
||||
return mergeObject(super._defaults, {
|
||||
readonly: true,
|
||||
validationError: "is not a valid sub-types configuration"
|
||||
});
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_validateType(value, options={}) {
|
||||
super._validateType(value, options);
|
||||
for ( const [documentName, subtypes] of Object.entries(value) ) {
|
||||
const cls = getDocumentClass(documentName);
|
||||
if ( !cls ) throw new Error(`${this.validationError}: '${documentName}' is not a valid Document type`);
|
||||
if ( !cls.hasTypeData ) {
|
||||
throw new Error(`${this.validationError}: ${documentName} Documents do not support sub-types`);
|
||||
}
|
||||
if ( getType(subtypes) !== "Object" ) throw new Error(`Malformed ${documentName} documentTypes declaration`);
|
||||
for ( const [type, config] of Object.entries(subtypes) ) this.#validateSubtype(cls, type, config);
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate a single defined document subtype.
|
||||
* @param {typeof Document} documentClass The document for which the subtype is being registered
|
||||
* @param {string} type The requested subtype name
|
||||
* @param {object} config The provided subtype configuration
|
||||
* @throws {Error} An error if the subtype is invalid or malformed
|
||||
*/
|
||||
#validateSubtype(documentClass, type, config) {
|
||||
const dn = documentClass.documentName;
|
||||
if ( documentClass.metadata.coreTypes.includes(type) ) {
|
||||
throw new Error(`"${type}" is a reserved core type for the ${dn} document`);
|
||||
}
|
||||
if ( getType(config) !== "Object" ) {
|
||||
throw new Error(`Malformed "${type}" subtype declared for ${dn} documentTypes`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user