Settings are a general way for packages to persist and store data without being attached to a document.
Official Documentation
Legend
Setting.defineSchema // `.` indicates static method or property
ClientSettings#register // `#` indicates instance method or property
game.settings.register // The ClientSettings class is instantiated as part of the `game` object
Settings, like flags, are a way for packages to store and persist data. Unlike flags, Settings are not tied to a specific document.
For the vast majority of use-cases, settings are intended to be modified by a UI, either a Menu or within the Module Settings panel itself. These settings are intended to be used to modify the functionality of a package, rather than store arbitrary data for that module or system.
The following elements are crucial to understanding settings.
Settings have a scope
field which indicates if it's part of the device's localStorage (scope: client
) or if it should be stored in the world's database (scope: world
).
If you wish to store data specific to a user across any devices they might use, consider instead storing the data as a flag on the user document. Alternatively, store the data as part of a object in the setting.
Client settings are always editable by any user, as they are device-specific. This works well for display-based settings.
World settings have a global permission level ("Modify Configuration Settings") that is shared with the ability to enable or disable modules. By default, only Assistant GMs and Game Masters can edit world settings. This is a critical limitation that may require sockets to work around.
The ClientSettings
are a singleton class instantiated as part of the game object.
See Setting Types below for examples about the different types of settings that can be registered.
Settings should be registered during the
init
hook.
All settings must be registered before they can be set or accessed. This needs to be done with game.settings.register
, with game.settings
being an instance of ClientSettings
.
/*
* Create a custom config setting
*/
game.settings.register('myModuleName', 'mySettingName', {
name: 'My Setting',
hint: 'A description of the registered setting and its behavior.',
scope: 'world', // "world" = sync to db, "client" = local storage
config: true, // false if you dont want it to show in module config
type: Number, // You want the primitive class, e.g. Number, not the name of the class as a string
default: 0,
onChange: value => { // value is the new value of the setting
console.log(value)
},
requiresReload: true, // true if you want to prompt the user to reload
/** Creates a select dropdown */
choices: {
1: "Option Label 1",
2: "Option Label 2",
3: "Option Label 3"
},
/** Number settings can have a range slider, with an optional step property */
range: {
min: 0,
step: 2,
max: 10
},
/** "audio", "image", "video", "imagevideo", "folder", "font", "graphics", "text", or "any" */
filePicker: "any"
});
name
and hint
, and the labels in choices
are localized by the setting configuration application on render, so you can register settings in init
and just pass a localizable string for those valuesconfig
defaults to undefined
which behaves the same as false
requiresReload
is useful for settings that make changes during the init
or setup
hooks.scope
defaults to "client"type
for complex settings that need data validation.filePicker
restricts what kinds of files can be chosen for the settingThe type
of a setting is expected to be a constructor which is used when the setting's value is gotten. The 'normal' primitive constructors cover all basic use cases:
You can use fundamental language constructs as types
There's also some lesser used primitive types that are nevertheless eligible
It is possible however to leverage this functionality to do some advanced data manipulation with a complex setting object during the get
. Doing so has some gotchas surrounding the onChange
callback.
class SomeClass {
constructor(parsedJson) {
this.merged = parsedJson?.foo + parsedJson?.bar;
this.foo = parsedJson?.foo;
this.bar = parsedJson?.bar;
}
}
game.settings.register('myModuleName', 'customClassSetting', { type: SomeClass });
game.settings.set('myModuleName', 'customClassSetting', {foo: 'foosius', bar: 'whatever'});
game.settings.get('myModuleName', 'customClassSetting').merged; // 'foosiuswhatever'
As an even more advanced use case, you could pass a DataModel
as a setting to provide advanced validation; the type casting has a special case from these where it calls YourDataModel.fromSource
.
When registering a setting, instead of passing a hard-coded string to name
or hint
, it is possible to pass a localization path to support translations. Both name
and hint
are run through game.i18n.localize
before being displayed in the Setting UI.
Settings with
scope: world
cannot beset
until theready
hook.
A setting's value can be set with game.settings.set
. It's important to note that a scope: world
setting can only be set by a user with the "Modify Configuration Settings" permission (by default this is only Game Master and Assistant GM users), while scope: client
settings will only persist on the user's local machine.
const whateverValue = 'foo';
game.settings.set('myModuleName','myModuleSetting', whateverValue);
Easily handled data:
- Objects and Arrays which do not contain functions
- Strings
- Numbers
- Booleans
A setting's value is stringified and stored as a string. This limits the possible values for a setting to anything which can survive a JSON.stringify()
and subsequent JSON.parse()
.
Note that JSON.stringify
will prefer to use a value's toJSON()
method if one is available, all Foundry documents have such a method which strips the document back to its base data.
If you wish to improve validation when updating a complex setting, you should consider a data model or data field. If you're just using String
or Number
, it will run the new value through those primitives first before storing to the database (e.g. if the setting is type: Number
, and someone passes set(scope, key, "5")
, the setting will run Number("5")
to cast the type). StringField
and NumberField
will accomplish similar casting behavior but also allow further refinements, such as whether a blank string is allowed or whether to enforce that the number is an integer.
Settings can be read with game.settings.get
.
const someVariable = game.settings.get('myModuleName','myModuleSetting');
console.log(someVariable); // expected to be 'foo'
Unless a setting has actively been saved to the world database with a call to game.settings.set
, it will fill in with the registered default
. This means that if you update the default, it will automatically apply to not only new users but also current ones. This can be useful, but also means that you can't rely on a setting's value to detect "old" users in the caes of a setting that is tracking things like previous module versions if you aren't actively creating a database entry.
One way to check if there's a database-backed value is to call game.settings.storage.get("world").getSetting
, which accesses the actual world collection of setting documents (comparable to game.actors
). If that returns undefined
, there's no underlying DB entry for the setting and it's just going to use the default. Note that the key is the concatenated namespace
and settingName
, e.g. core.compendiumConfiguration
.
When getting a setting's value, the type
of the setting registered is used by Core as a constructor for the returned value.
Example:
game.settings.register('myModuleName', 'myNumber', { type: Number });
game.settings.set('myModuleName', 'myNumber', 'some string');
game.settings.get('myModuleName', 'myNumber'); // NaN
For more information on the basic primitive constructors and how they convert values, this article has a good overview.
On the backend, Settings are fairly simple documents; they have an _id
, key
, value
, and _stats
field. They are the only document type to not have a flags
field. Unlike every other primary document, their world collection is not a property of the Game
class directly; instead, game.settings
accesses the singleton instance of ClientSettings
, which then has the actual WorldSettings instance as a sub-property. This is in part because there are actually two places to store settings; WorldSettings is shared database, but localStorage provides per-client settings separate from Foundry's normal document-based DB operations.
Where settings registration comes in is providing safeguards for the returned values
get
and set
operations check if a setting has been registeredtype
gets used to cast the JSON stringified value
of the Setting document, which is returned by the get
operationThe other pieces of the registration are used by the SettingsConfig application for config: true
; if you provide a DataField
instance for the type, it will call that field's toInput
function, and then appropriately label with the name
and hint
properties.
Here are some tips and tricks for working with settings.
There is no hook for when a setting changes, instead an onChange
callback must be provided during registration (You of course could run Hooks.call()
in that callback). This callback is fired after the setting has been set, meaning a settings.get
inside this callback will return the new value of the setting, not the old.
The onChange
callback does not fire if there are no differences between the value
being set
and the current value returned from settings.get
.
This callback will fire on all clients for world scoped settings, but only locally for client scoped settings. Its only argument is the raw value of the setting that was set
.
Because this
value
argument is not necessarily the same value that would be returned fromsettings.get
, it is safer to get the new value in this callback if you intend to operate on it.
This section will provide snippets and screenshots for the various common setting configurations. These snippets have the minimum number of options required to display the setting and may require tweaking for your specific use case. They also make use of foundry.data.fields
to make it easier to further customize the type behavior.
game.settings.register('core', 'myCheckbox', {
name: 'My Boolean',
config: true,
type: new foundry.data.fields.BooleanField(),
});
game.settings.get('core', 'myCheckbox'); // false
game.settings.register('core', 'myInput', {
name: 'My Text',
config: true,
type: new foundry.data.fields.StringField(),
});
game.settings.get('core', 'myInput'); // 'Foo'
game.settings.register('core', 'mySelect', {
name: 'My Select',
config: true,
type: new foundry.data.fields.StringField({
choices: {
"a": "Option A",
"b": "Option B"
},
}),
});
game.settings.get('core', 'mySelect'); // 'a'
The key
of the choices
object is what is stored in the setting when the user selects an option from the dropdown.
The value
s of the choices
object are automatically run through game.i18n.localize
before being displayed in the Setting UI.
game.settings.register('core', 'myNumber', {
name: 'My Number',
config: true,
type: new foundry.data.fields.NumberField(),
});
game.settings.get('core', 'myNumber'); // 1
game.settings.register('core', 'myRange', {
name: 'My Number Range',
config: true,
type: new foundry.data.fields.NumberField({
min: 0, max: 100, step: 10,
initial: 0, nullable: false
}),
});
game.settings.get('core', 'myRange'); // 50
game.settings.register('core', 'myFile', {
name: 'My File',
config: true,
type: String,
filePicker: true,
});
game.settings.get('core', 'myFile'); // 'path/to/file'
The following can be given to the filePicker
option to change the behavior of the File Picker UI when it is opened. These are useful if you need the user to select only an image for instance.
'audio'
- Displays audio files only'image'
- Displays image files only'video'
- Displays video files only'imagevideo'
- Displays images and video files'folder'
- Allows selection of a directory (beware, it does not enforce directory selection)'font'
- Display font files only'graphics'
- Display 3D files only'text'
- Display text files only'any'
- No different than true
If the setting is registered with either the default filePicker: true
or filePicker: 'folder'
it is possible for a user to select a directory instead of a file. This is not forced however and the user might still select a file.
When saved, the directory path is the only string which is saved and does not contain information about the source which the directory was chosen from. Without strict assumptions and checking those assumptions, this kind of setting has a high chance of causing errors or unexpected behavior (e.g. creating a folder on the user's local storage instead of their configured S3 bucket).
Sometimes a package is more complex than a few settings will allow a user to configure. In these cases it is recommended to register a settings menu with game.settings.registerMenu
, and manage the configuration with a FormApplication or Dialog. Note that registerMenu
does not register a setting by itself, simply a menu button.
Menus work best when used in conjunction with a registered setting of type Object
which has been set to config: false
. A menu could also be used to control many individual settings if desired.
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
restricted: true // Restrict this submenu to gamemaster only?
});
game.settings.register('myModuleName', 'myComplexSettingName', {
scope: 'world', // "world" = sync to db, "client" = local storage
config: false, // we will use the menu above to edit this setting
type: Object,
default: {}, // can be used to set up the default structure
});
/**
* For more information about FormApplications, see:
* https://hackmd.io/UsmsgTj6Qb6eDw3GTi5XCg
*/
class MySubmenuApplicationClass extends FormApplication {
// lots of other things...
getData() {
return game.settings.get('myModuleName', 'myComplexSettingName');
}
_updateObject(event, formData) {
const data = expandObject(formData);
game.settings.set('myModuleName', 'myComplexSettingName', data);
}
}
FormApplications in particular allow you to run any logic you want during the _updateObject
method. This could be leveraged to accomplish many things:
_updateObject
), you could run validation on user inputs before saving them to the setting value.Here are some common issues when working with settings.
Happens when game.settings.register
is called with an invalid first argument. This first argument is intended to be the id
or name
of an active package.
game.settings.get
and .set
both throw this error if the requested setting has not been registered yet.
Foundry has a hard limit in ClientSettings##setWorld
that prevents modifying the game settings prior to ready
. This means any "module setup" type dialogs should wait until Hooks.once("ready"
.