Files
Foundry-VTT-Docker/resources/app/client/core/issues.js

291 lines
9.7 KiB
JavaScript
Raw Normal View History

2025-01-04 00:34:03 +01:00
/**
* An object structure of document types at the top level, with a count of different sub-types for that document type.
* @typedef {Record<string, Record<string, number>>} ModuleSubTypeCounts
*/
/**
* A class responsible for tracking issues in the current world.
*/
class ClientIssues {
/**
* Keep track of valid Documents in the world that are using module-provided sub-types.
* @type {Map<string, ModuleSubTypeCounts>}
*/
#moduleTypeMap = new Map();
/**
* Keep track of document validation failures.
* @type {object}
*/
#documentValidationFailures = {};
/**
* @typedef {object} UsabilityIssue
* @property {string} message The pre-localized message to display in relation to the usability issue.
* @property {string} severity The severity of the issue, either "error", "warning", or "info".
* @property {object} [params] Parameters to supply to the localization.
*/
/**
* Keep track of any usability issues related to browser or technology versions.
* @type {Record<string, UsabilityIssue>}
*/
#usabilityIssues = {};
/**
* The minimum supported resolution.
* @type {{WIDTH: number, HEIGHT: number}}
*/
static #MIN_RESOLUTION = {WIDTH: 1024, HEIGHT: 700};
/**
* @typedef {object} BrowserTest
* @property {number} minimum The minimum supported version for this browser.
* @property {RegExp} match A regular expression to match the browser against the user agent string.
* @property {string} message A message to display if the user's browser version does not meet the minimum.
*/
/**
* The minimum supported client versions.
* @type {Record<string, BrowserTest>}
*/
static #BROWSER_TESTS = {
Electron: {
minimum: 29,
match: /Electron\/(\d+)\./,
message: "ERROR.ElectronVersion"
},
Chromium: {
minimum: 105,
match: /Chrom(?:e|ium)\/(\d+)\./,
message: "ERROR.BrowserVersion"
},
Firefox: {
minimum: 121,
match: /Firefox\/(\d+)\./,
message: "ERROR.BrowserVersion"
},
Safari: {
minimum: 15.4,
match: /Version\/(\d+)\..*Safari\//,
message: "ERROR.BrowserVersion"
}
};
/* -------------------------------------------- */
/**
* Add a Document to the count of module-provided sub-types.
* @param {string} documentName The Document name.
* @param {string} subType The Document's sub-type.
* @param {object} [options]
* @param {boolean} [options.decrement=false] Decrement the counter rather than incrementing it.
*/
#countDocumentSubType(documentName, subType, {decrement=false}={}) {
if ( !((typeof subType === "string") && subType.includes(".")) ) return;
const [moduleId, ...rest] = subType.split(".");
subType = rest.join(".");
if ( !this.#moduleTypeMap.has(moduleId) ) this.#moduleTypeMap.set(moduleId, {});
const counts = this.#moduleTypeMap.get(moduleId);
const types = counts[documentName] ??= {};
types[subType] ??= 0;
if ( decrement ) types[subType] = Math.max(types[subType] - 1, 0);
else types[subType]++;
}
/* -------------------------------------------- */
/**
* Detect the user's browser and display a notification if it is below the minimum required version.
*/
#detectBrowserVersion() {
for ( const [browser, {minimum, match, message}] of Object.entries(ClientIssues.#BROWSER_TESTS) ) {
const [, version] = navigator.userAgent.match(match) ?? [];
if ( !Number.isNumeric(version) ) continue;
if ( Number(version) < minimum ) {
const err = game.i18n.format(message, {browser, version, minimum});
ui.notifications?.error(err, {permanent: true, console: true});
this.#usabilityIssues.browserVersionIncompatible = {
message,
severity: "error",
params: {browser, version, minimum}
};
}
break;
}
}
/* -------------------------------------------- */
/**
* Record a reference to a resolution notification ID so that we can remove it if the problem is remedied.
* @type {number}
*/
#resolutionTooLowNotification;
/**
* Detect the user's resolution and display a notification if it is too small.
*/
#detectResolution() {
const {WIDTH: reqWidth, HEIGHT: reqHeight} = ClientIssues.#MIN_RESOLUTION;
const {innerWidth: width, innerHeight: height} = window;
if ( (height < reqHeight) || (width < reqWidth) ) {
// Display a permanent error notification
if ( ui.notifications && !this.#resolutionTooLowNotification ) {
this.#resolutionTooLowNotification = ui.notifications.error(game.i18n.format("ERROR.LowResolution", {
width, reqWidth, height, reqHeight
}), {permanent: true});
}
// Record the usability issue
this.#usabilityIssues.resolutionTooLow = {
message: "ERROR.LowResolution",
severity: "error",
params: {width, reqWidth, height, reqHeight}
};
}
// Remove an error notification if present
else {
if ( this.#resolutionTooLowNotification ) {
this.#resolutionTooLowNotification = ui.notifications.remove(this.#resolutionTooLowNotification);
}
delete this.#usabilityIssues.resolutionTooLow;
}
}
/* -------------------------------------------- */
/**
* Detect and display warnings for known performance issues which may occur due to the user's hardware or browser
* configuration.
* @internal
*/
_detectWebGLIssues() {
const context = canvas.app.renderer.context;
try {
const rendererInfo = SupportDetails.getWebGLRendererInfo(context.gl);
if ( /swiftshader/i.test(rendererInfo) ) {
ui.notifications.warn("ERROR.NoHardwareAcceleration", {localize: true, permanent: true});
this.#usabilityIssues.hardwareAccel = {message: "ERROR.NoHardwareAcceleration", severity: "error"};
}
} catch ( err ) {
ui.notifications.warn("ERROR.RendererNotDetected", {localize: true, permanent: true});
this.#usabilityIssues.noRenderer = {message: "ERROR.RendererNotDetected", severity: "warning"};
}
// Verify that WebGL2 is being used.
if ( !canvas.supported.webGL2 ) {
ui.notifications.error("ERROR.NoWebGL2", {localize: true, permanent: true});
this.#usabilityIssues.webgl2 = {message: "ERROR.NoWebGL2", severity: "error"};
}
}
/* -------------------------------------------- */
/**
* Add an invalid Document to the module-provided sub-type counts.
* @param {typeof Document} cls The Document class.
* @param {object} source The Document's source data.
* @param {object} [options]
* @param {boolean} [options.decrement=false] Decrement the counter rather than incrementing it.
* @internal
*/
_countDocumentSubType(cls, source, options={}) {
if ( cls.hasTypeData ) this.#countDocumentSubType(cls.documentName, source.type, options);
for ( const [embeddedName, field] of Object.entries(cls.hierarchy) ) {
if ( !(field instanceof foundry.data.fields.EmbeddedCollectionField) ) continue;
for ( const embedded of source[embeddedName] ) {
this._countDocumentSubType(field.model, embedded, options);
}
}
}
/* -------------------------------------------- */
/**
* Track a validation failure that occurred in a WorldCollection.
* @param {WorldCollection} collection The parent collection.
* @param {object} source The Document's source data.
* @param {DataModelValidationError} error The validation error.
* @internal
*/
_trackValidationFailure(collection, source, error) {
if ( !(collection instanceof WorldCollection) ) return;
if ( !(error instanceof foundry.data.validation.DataModelValidationError) ) return;
const documentName = collection.documentName;
this.#documentValidationFailures[documentName] ??= {};
this.#documentValidationFailures[documentName][source._id] = {name: source.name, error};
}
/* -------------------------------------------- */
/**
* Detect and record certain usability error messages which are likely to result in the user having a bad experience.
* @internal
*/
_detectUsabilityIssues() {
this.#detectResolution();
this.#detectBrowserVersion();
window.addEventListener("resize", foundry.utils.debounce(this.#detectResolution.bind(this), 250), {passive: true});
}
/* -------------------------------------------- */
/**
* Get the Document sub-type counts for a given module.
* @param {Module|string} module The module or its ID.
* @returns {ModuleSubTypeCounts}
*/
getSubTypeCountsFor(module) {
return this.#moduleTypeMap.get(module.id ?? module);
}
/* -------------------------------------------- */
/**
* Retrieve all sub-type counts in the world.
* @returns {Iterator<string, ModuleSubTypeCounts>}
*/
getAllSubTypeCounts() {
return this.#moduleTypeMap.entries();
}
/* -------------------------------------------- */
/**
* Retrieve the tracked validation failures.
* @returns {object}
*/
get validationFailures() {
return this.#documentValidationFailures;
}
/* -------------------------------------------- */
/**
* Retrieve the tracked usability issues.
* @returns {Record<string, UsabilityIssue>}
*/
get usabilityIssues() {
return this.#usabilityIssues;
}
/* -------------------------------------------- */
/**
* @typedef {object} PackageCompatibilityIssue
* @property {string[]} error Error messages.
* @property {string[]} warning Warning messages.
*/
/**
* Retrieve package compatibility issues.
* @returns {Record<string, PackageCompatibilityIssue>}
*/
get packageCompatibilityIssues() {
return game.data.packageWarnings;
}
}