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

286 lines
11 KiB
JavaScript
Raw Normal View History

2025-01-04 00:34:03 +01:00
/**
* A class responsible for managing defined game settings or settings menus.
* Each setting is a string key/value pair belonging to a certain namespace and a certain store scope.
*
* When Foundry Virtual Tabletop is initialized, a singleton instance of this class is constructed within the global
* Game object as game.settings.
*
* @see {@link Game#settings}
* @see {@link Settings}
* @see {@link SettingsConfig}
*/
class ClientSettings {
constructor(worldSettings) {
/**
* A object of registered game settings for this scope
* @type {Map<string, SettingsConfig>}
*/
this.settings = new Map();
/**
* Registered settings menus which trigger secondary applications
* @type {Map}
*/
this.menus = new Map();
/**
* The storage interfaces used for persisting settings
* Each storage interface shares the same API as window.localStorage
*/
this.storage = new Map([
["client", window.localStorage],
["world", new WorldSettings(worldSettings)]
]);
}
/* -------------------------------------------- */
/**
* Return a singleton instance of the Game Settings Configuration app
* @returns {SettingsConfig}
*/
get sheet() {
if ( !this._sheet ) this._sheet = new SettingsConfig();
return this._sheet;
}
/* -------------------------------------------- */
/**
* Register a new game setting under this setting scope
*
* @param {string} namespace The namespace under which the setting is registered
* @param {string} key The key name for the setting under the namespace
* @param {SettingConfig} data Configuration for setting data
*
* @example Register a client setting
* ```js
* game.settings.register("myModule", "myClientSetting", {
* name: "Register a Module Setting with Choices",
* hint: "A description of the registered setting and its behavior.",
* scope: "client", // This specifies a client-stored setting
* config: true, // This specifies that the setting appears in the configuration view
* requiresReload: true // This will prompt the user to reload the application for the setting to take effect.
* type: String,
* choices: { // If choices are defined, the resulting setting will be a select menu
* "a": "Option A",
* "b": "Option B"
* },
* default: "a", // The default value for the setting
* onChange: value => { // A callback function which triggers when the setting is changed
* console.log(value)
* }
* });
* ```
*
* @example Register a world setting
* ```js
* game.settings.register("myModule", "myWorldSetting", {
* name: "Register a Module Setting with a Range slider",
* hint: "A description of the registered setting and its behavior.",
* scope: "world", // This specifies a world-level setting
* config: true, // This specifies that the setting appears in the configuration view
* requiresReload: true // This will prompt the GM to have all clients reload the application for the setting to
* // take effect.
* type: new foundry.fields.NumberField({nullable: false, min: 0, max: 100, step: 10}),
* default: 50, // The default value for the setting
* onChange: value => { // A callback function which triggers when the setting is changed
* console.log(value)
* }
* });
* ```
*/
register(namespace, key, data) {
if ( !namespace || !key ) throw new Error("You must specify both namespace and key portions of the setting");
data.key = key;
data.namespace = namespace;
data.scope = ["client", "world"].includes(data.scope) ? data.scope : "client";
key = `${namespace}.${key}`;
// Validate type
if ( data.type ) {
const allowedTypes = [foundry.data.fields.DataField, foundry.abstract.DataModel, Function];
if ( !allowedTypes.some(t => data.type instanceof t) ) {
throw new Error(`Setting ${key} type must be a DataField, DataModel, or callable function`);
}
// Sync some setting data with the DataField
if ( data.type instanceof foundry.data.fields.DataField ) {
data.default ??= data.type.initial;
data.type.name = key;
data.type.label ??= data.label;
data.type.hint ??= data.hint;
}
}
// Setting values may not be undefined, only null, so the default should also adhere to this behavior
data.default ??= null;
// Store the setting configuration
this.settings.set(key, data);
// Reinitialize to cast the value of the Setting into its defined type
if ( data.scope === "world" ) this.storage.get("world").getSetting(key)?.reset();
}
/* -------------------------------------------- */
/**
* Register a new sub-settings menu
*
* @param {string} namespace The namespace under which the menu is registered
* @param {string} key The key name for the setting under the namespace
* @param {SettingSubmenuConfig} data Configuration for setting data
*
* @example Define a settings submenu which handles advanced configuration needs
* ```js
* game.settings.registerMenu("myModule", "mySettingsMenu", {
* name: "My Settings Submenu",
* label: "Settings Menu Label", // The text label used in the button
* hint: "A description of what will occur in the submenu dialog.",
* icon: "fas fa-bars", // A Font Awesome icon used in the submenu button
* type: MySubmenuApplicationClass, // A FormApplication subclass which should be created
* restricted: true // Restrict this submenu to gamemaster only?
* });
* ```
*/
registerMenu(namespace, key, data) {
if ( !namespace || !key ) throw new Error("You must specify both namespace and key portions of the menu");
data.key = `${namespace}.${key}`;
data.namespace = namespace;
if ( !((data.type?.prototype instanceof FormApplication)
|| (data.type?.prototype instanceof foundry.applications.api.ApplicationV2) )) {
throw new Error("You must provide a menu type that is a FormApplication or ApplicationV2 instance or subclass");
}
this.menus.set(data.key, data);
}
/* -------------------------------------------- */
/**
* Get the value of a game setting for a certain namespace and setting key
*
* @param {string} namespace The namespace under which the setting is registered
* @param {string} key The setting key to retrieve
*
* @example Retrieve the current setting value
* ```js
* game.settings.get("myModule", "myClientSetting");
* ```
*/
get(namespace, key) {
key = this.#assertKey(namespace, key);
const config = this.settings.get(key);
const storage = this.storage.get(config.scope);
// Get the Setting instance
let setting;
switch ( config.scope ) {
case "client":
setting = new Setting({key, value: storage.getItem(key) ?? config.default});
break;
case "world":
setting = storage.getSetting(key);
if ( !setting ) setting = new Setting({key, value: config.default});
break;
}
return setting.value;
}
/* -------------------------------------------- */
/**
* Set the value of a game setting for a certain namespace and setting key
*
* @param {string} namespace The namespace under which the setting is registered
* @param {string} key The setting key to retrieve
* @param {*} value The data to assign to the setting key
* @param {object} [options] Additional options passed to the server when updating world-scope settings
* @returns {*} The assigned setting value
*
* @example Update the current value of a setting
* ```js
* game.settings.set("myModule", "myClientSetting", "b");
* ```
*/
async set(namespace, key, value, options={}) {
key = this.#assertKey(namespace, key);
const setting = this.settings.get(key);
if ( value === undefined ) value = setting.default;
// Assign using DataField
if ( setting.type instanceof foundry.data.fields.DataField ) {
const err = setting.type.validate(value, {fallback: false});
if ( err instanceof foundry.data.validation.DataModelValidationFailure ) throw err.asError();
}
// Assign using DataModel
if ( foundry.utils.isSubclass(setting.type, foundry.abstract.DataModel) ) {
value = setting.type.fromSource(value, {strict: true});
}
// Save the setting change
if ( setting.scope === "world" ) await this.#setWorld(key, value, options);
else this.#setClient(key, value, setting.onChange);
return value;
}
/* -------------------------------------------- */
/**
* Assert that the namespace and setting name were provided and form a valid key.
* @param {string} namespace The setting namespace
* @param {string} settingName The setting name
* @returns {string} The combined setting key
*/
#assertKey(namespace, settingName) {
const key = `${namespace}.${settingName}`;
if ( !namespace || !settingName ) throw new Error("You must specify both namespace and key portions of the"
+ `setting, you provided "${key}"`);
if ( !this.settings.has(key) ) throw new Error(`"${key}" is not a registered game setting`);
return key;
}
/* -------------------------------------------- */
/**
* Create or update a Setting document in the World database.
* @param {string} key The setting key
* @param {*} value The desired setting value
* @param {object} [options] Additional options which are passed to the document creation or update workflows
* @returns {Promise<Setting>} The created or updated Setting document
*/
async #setWorld(key, value, options) {
if ( !game.ready ) throw new Error("You may not set a World-level Setting before the Game is ready.");
const current = this.storage.get("world").getSetting(key);
const json = JSON.stringify(value);
if ( current ) return current.update({value: json}, options);
else return Setting.create({key, value: json}, options);
}
/* -------------------------------------------- */
/**
* Create or update a Setting document in the browser client storage.
* @param {string} key The setting key
* @param {*} value The desired setting value
* @param {Function} onChange A registered setting onChange callback
* @returns {Setting} A Setting document which represents the created setting
*/
#setClient(key, value, onChange) {
const storage = this.storage.get("client");
const json = JSON.stringify(value);
let setting;
if ( key in storage ) {
setting = new Setting({key, value: storage.getItem(key)});
const diff = setting.updateSource({value: json});
if ( foundry.utils.isEmpty(diff) ) return setting;
}
else setting = new Setting({key, value: json});
storage.setItem(key, json);
if ( onChange instanceof Function ) onChange(value);
return setting;
}
}