Mod Creation/Mod Context API Reference: Difference between revisions

From Melvor Idle
No edit summary
(→‎MethodPatch.after(hook: (returnValue: any, ...args: any) => any | void): void: Make it more obvious that the after function has one more argument than the original call (the return type))
 
(33 intermediate revisions by 3 users not shown)
Line 7: Line 7:
If the module is defined as the <code>"setup"</code> for your mod in <code>manifest.json</code>, the exported <code>setup</code> function will receive the context object as a sole parameter:
If the module is defined as the <code>"setup"</code> for your mod in <code>manifest.json</code>, the exported <code>setup</code> function will receive the context object as a sole parameter:


  <nowiki>export function setup(ctx) {
  <syntaxhighlight lang="js" line>export function setup(ctx) {
   // ...
   // ...
}</nowiki>
}</syntaxhighlight>


Otherwise, use the global <code>mod.getContext</code> method, passing in your module's meta object:
Otherwise, use the global <code>mod.getContext</code> method, passing in your module's meta object:


  <nowiki>const ctx = mod.getContext(import.meta);</nowiki>
  <syntaxhighlight lang="js" line>const ctx = mod.getContext(import.meta);</syntaxhighlight>


=== From a Script ===
=== From a Script ===
Line 19: Line 19:
The recommended approach for scripts is to use the global <code>mod.register</code> method. This only works in scripts injected via the <code>"load"</code> property of <code>manifest.json</code> or the <code>loadScript</code> method of the context object.
The recommended approach for scripts is to use the global <code>mod.register</code> method. This only works in scripts injected via the <code>"load"</code> property of <code>manifest.json</code> or the <code>loadScript</code> method of the context object.


  <nowiki>mod.register(ctx => {
  <syntaxhighlight lang="js" line>mod.register(ctx => {
   // ...
   // ...
});</nowiki>
});</syntaxhighlight>


=== From a Lifecycle Method ===
=== From a Lifecycle Method ===
Line 27: Line 27:
All game lifecycle method callbacks will also receive their respective mod's context object as a sole parameter.
All game lifecycle method callbacks will also receive their respective mod's context object as a sole parameter.


  <nowiki>onCharacterLoaded(ctx => {
  <syntaxhighlight lang="js" line>onCharacterLoaded(ctx => {
   // ...
   // ...
});</nowiki>
});</syntaxhighlight>


=== From the Dev Context ===
=== From the Dev Context ===
Line 35: Line 35:
For easier prototyping, you can use the global <code>mod.getDevContext</code> method to get a special dev mod context object. This should not be used in a mod, but only for development purposes (via the console). Any APIs that require resources will not work as the dev "mod" does not contain any resources.
For easier prototyping, you can use the global <code>mod.getDevContext</code> method to get a special dev mod context object. This should not be used in a mod, but only for development purposes (via the console). Any APIs that require resources will not work as the dev "mod" does not contain any resources.


  <nowiki>const devCtx = mod.getDevContext();</nowiki>
  <syntaxhighlight lang="js" line>const devCtx = mod.getDevContext();</syntaxhighlight>
 
== Getter Properties ==
 
=== name: string ===
 
The name of the mod.
 
=== namespace: string | undefined ===
 
The defined namespace of the mod, if provided.
 
=== version: string ===
 
The currently loaded version of the mod.


== Loading Resources ==
== Loading Resources ==
Line 55: Line 69:
'''Example'''
'''Example'''


  <nowiki>const url = ctx.getResourceUrl('sea-shanty-2.ogg');
  <syntaxhighlight lang="js" line>const url = ctx.getResourceUrl('sea-shanty-2.ogg');
const song = new Audio(url);
const song = new Audio(url);
song.loop = true;
song.loop = true;
song.play();</nowiki>
song.play();</syntaxhighlight>


=== loadModule(path: string): Promise<any> ===
=== loadModule(path: string): Promise<any> ===
Line 74: Line 88:
'''Example'''
'''Example'''


  <nowiki>// my-module.mjs
  <syntaxhighlight lang="js" line>// my-module.mjs
export function greet(name) {
export function greet(name) {
   console.log(`Hello, ${name}!`);
   console.log(`Hello, ${name}!`);
}</nowiki>
}</syntaxhighlight>


  <nowiki>const myModule = await ctx.loadModule('my-module.mjs');
  <syntaxhighlight lang="js" line>const myModule = await ctx.loadModule('my-module.mjs');
myModule.greet('Melvor'); // Hello, Melvor!</nowiki>
myModule.greet('Melvor'); // Hello, Melvor!</syntaxhighlight>


=== loadScript(path: string): Promise<void> ===
=== loadScript(path: string): Promise<void> ===
Line 96: Line 110:
'''Example'''
'''Example'''


  <nowiki>// Await call if wanting the script to run before continuing
  <syntaxhighlight lang="js" line>// Await call if wanting the script to run before continuing
await ctx.loadScript('my-script.js');
await ctx.loadScript('my-script.js');
// my-script.js has run
// my-script.js has run
Line 102: Line 116:
// Don't await if no dependency on script
// Don't await if no dependency on script
ctx.loadScript('my-independednt-script.js');
ctx.loadScript('my-independednt-script.js');
// my-independent-script.js has NOT run yet</nowiki>
// my-independent-script.js has NOT run yet</syntaxhighlight>


=== loadTemplates(path: string): void ===
=== loadTemplates(path: string): Promise<void> ===


Inject all <template> elements contained in a given HTML file into the document body.
Inject all <template> elements contained in a given HTML file into the document body.
Line 111: Line 125:


<code>path: string</code> The relative path to the HTML resource
<code>path: string</code> The relative path to the HTML resource
'''Returns'''
<code>Promise<void></code> A promise that is resolved once all templates have been injected into the document body.


'''Example'''
'''Example'''


  <nowiki>ctx.loadTemplates('my-templates.html');</nowiki>
  <syntaxhighlight lang="js" line>ctx.loadTemplates('my-templates.html');</syntaxhighlight>


=== loadStylesheet(path: string): void ===
=== loadStylesheet(path: string): void ===
Line 126: Line 144:
'''Example'''
'''Example'''


  <nowiki>ctx.loadStylesheet('my-styles.css');</nowiki>
  <syntaxhighlight lang="js" line>ctx.loadStylesheet('my-styles.css');</syntaxhighlight>


=== loadData(path: string): Promise<any> ===
=== loadData(path: string): Promise<any> ===
Line 142: Line 160:
'''Example'''
'''Example'''


  <nowiki>// my-data.json
  <syntaxhighlight lang="js" line>// my-data.json
{
{
   "coolThings": [
   "coolThings": [
     "rocks"
     "rocks"
   ]
   ]
}</nowiki>
}</syntaxhighlight>


  <nowiki>// in JavaScript
<small>''Comments in JSON are purely illustrative and not valid markup''</small>
 
  <syntaxhighlight lang="js" line>// in JavaScript
const myData = await ctx.loadData('my-data.json');
const myData = await ctx.loadData('my-data.json');
console.log(myData.coolThings); // ['rocks']</nowiki>
console.log(myData.coolThings); // ['rocks']</syntaxhighlight>
 
== Sharing Resources ==
 
=== share(resourcePath: string): void ===
 
Shares a packed mod resource for other mods to use.
 
'''Parameters'''
 
<code>resourcePath: string</code> The resource path to be shared.
 
'''Example'''
 
<syntaxhighlight lang="js" line>// manifest.json
{
  "namespace": "helloMelvor"
}</syntaxhighlight>
 
<small>''Comments in JSON are purely illustrative and not valid markup''</small>
 
<syntaxhighlight lang="js" line>// in JavaScript
ctx.share('my_cool_image.png');
ctx.share('Greeter.mjs');</syntaxhighlight>
 
Then another mod can use the resource anywhere that accepts a mod resource path.
 
<syntaxhighlight lang="js" line>
ctx.getResourceUrl('helloMelvor:my_cool_image.png');
const { Greeter } = await loadModule('helloMelvor:Greeter.mjs');
const greeter = new Greeter();</syntaxhighlight>


== Lifecycle Hooks ==
== Lifecycle Hooks ==
Line 165: Line 215:
'''Example'''
'''Example'''


  <nowiki>ctx.onModsLoaded(async (ctx) => {
  <syntaxhighlight lang="js" line>ctx.onModsLoaded(async (ctx) => {
   // ...
   // ...
});</nowiki>
});</syntaxhighlight>


=== onCharacterSelectionLoaded(callback: (ctx: ModContext) => void | Promise<void>): void ===
=== onCharacterSelectionLoaded(callback: (ctx: ModContext) => void | Promise<void>): void ===


Execute code after the the character selection screen has fully loaded.
Execute code after the character selection screen has fully loaded.


'''Parameters'''
'''Parameters'''
Line 179: Line 229:
'''Example'''
'''Example'''


  <nowiki>ctx.onCharacterSelectionLoaded(async (ctx) => {
  <syntaxhighlight lang="js" line>ctx.onCharacterSelectionLoaded(async (ctx) => {
   // ...
   // ...
});</nowiki>
});</syntaxhighlight>
 
=== onInterfaceAvailable(callback: (ctx: ModContext) => void | Promise<void>): void ===
 
Execute code before the character is loaded but after the game interface is initially injected into the page (but not initialized). Mostly useful for adding interface elements for custom skills that need to be present before <code>onCharacterLoaded</code>.
 
'''Parameters'''
 
<code>callback: (ctx: ModContext) => void | Promise<void></code> A callback function that receives the mod's context object as a parameter. Can be synchronous or asynchronous.
 
'''Example'''
 
<syntaxhighlight lang="js" line>ctx.onInterfaceAvailable(async (ctx) => {
  // ...
});</syntaxhighlight>


=== onCharacterLoaded(callback: (ctx: ModContext) => void | Promise<void>): void ===
=== onCharacterLoaded(callback: (ctx: ModContext) => void | Promise<void>): void ===
Line 193: Line 257:
'''Example'''
'''Example'''


  <nowiki>ctx.onCharacterLoaded(async (ctx) => {
  <syntaxhighlight lang="js" line>ctx.onCharacterLoaded(async (ctx) => {
   // ...
   // ...
});</nowiki>
});</syntaxhighlight>


=== onInterfaceReady(callback: (ctx: ModContext) => void | Promise<void>): void ===
=== onInterfaceReady(callback: (ctx: ModContext) => void | Promise<void>): void ===
Line 207: Line 271:
'''Example'''
'''Example'''


  <nowiki>ctx.onInterfaceReady(async (ctx) => {
  <syntaxhighlight lang="js" line>ctx.onInterfaceReady(async (ctx) => {
   // ...
   // ...
});</nowiki>
});</syntaxhighlight>


== Adding and Modifying Game Data ==
== Game Object Registration ==


✔️ '''Not documented yet, but it is available''' ✔️
The game object registration API can be accessed through the <code>gameData</code> property on the root context object.
 
=== addPackage(data: string | GameDataPackage): Promise<void> ===
 
Registers a game data package.
 
'''Parameters'''
 
<code>data: string | GameDataPackage</code> The resource path to your game data package <code>.json</code> file or a valid JavaScript GameDataPackage object.
 
'''Example'''
 
<syntaxhighlight lang="js" line>// data.json
{
  "$schema": "https://melvoridle.com/assets/schema/gameData.json",
  "data": {
    // data objects here
  }
}</syntaxhighlight>
 
<small>''Comments in JSON are purely illustrative and not valid markup''</small>
 
<syntaxhighlight lang="js" line>await ctx.gameData.addPackage('data.json');</syntaxhighlight>
 
=== buildPackage(builder: (packageBuilder: GameDataPackageBuilder) => void): BuiltGameDataPackage ===
 
Builds a GameDataPackage object using the <code>GameDataPackageBuilder</code> API.
 
'''Parameters'''
 
<code>builder: (packageBuilder: GameDataPackageBuilder) => void</code> The builder to be used to add individual game objects to the data package.
 
'''Returns'''
 
<code>BuiltGameDataPackage</code> A wrapper for the game data package. See information below.
 
'''Example'''
 
<syntaxhighlight lang="js" line>ctx.gameData.buildPackage((p) => {
  // data registration here
});</syntaxhighlight>
 
==== BuiltGameDataPackage.package: GameDataPackage ====
 
(Property) The actual built <code>GameDataPackage</code> object.
 
==== BuiltGameDataPackage.add(): void ====
 
Registers the built game data package.
 
'''Example'''
 
<syntaxhighlight lang="js" line>const pkg = ctx.gameData.buildPackage((p) => { /* ... */ });
pkg.add();</syntaxhighlight>


== Mod Settings ==
== Mod Settings ==
{{Disclaimer|When loading your mod as a Local Mod via the Creator Toolkit, the mod must be linked to mod.io and you must have subscribed to and installed the mod via mod.io in order for this data to persist.}}


The mod settings API can be accessed through the <code>settings</code> property on the root context object.
The mod settings API can be accessed through the <code>settings</code> property on the root context object.
Line 233: Line 352:
'''Example'''
'''Example'''


  <nowiki>ctx.settings.section('General');
  <syntaxhighlight lang="js" line>ctx.settings.section('General');
ctx.settings.section('Other');
ctx.settings.section('Other');
// Sections will be displayed in the settings window in this order
// Sections will be displayed in the settings window in this order
// 1. General
// 1. General
// 2. Other</nowiki>
// 2. Other</syntaxhighlight>


==== Section.add(config: SettingConfig | SettingConfig[]): void ====
==== Section.add(config: SettingConfig | SettingConfig[]): void ====
Line 249: Line 368:
'''Example'''
'''Example'''


  <nowiki>ctx.settings.section('General').add({
  <syntaxhighlight lang="js" line>ctx.settings.section('General').add({
   type: 'switch',
   type: 'switch',
   name: 'awesomeness-detection',
   name: 'awesomeness-detection',
Line 265: Line 384:
     label: 'Pick a Number',
     label: 'Pick a Number',
     hint: '1 through 10'
     hint: '1 through 10'
}]);</nowiki>
}]);</syntaxhighlight>


==== Section.get(name: string): any ====
==== Section.get(name: string): any ====
Line 281: Line 400:
'''Example'''
'''Example'''


  <nowiki>// Assuming the player has typed "1" into the setting
  <syntaxhighlight lang="js" line>// Assuming the player has typed "1" into the setting
ctx.settings.section('Other').get('pick-a-number'); // 1</nowiki>
ctx.settings.section('Other').get('pick-a-number'); // 1</syntaxhighlight>


==== Section.set(name: string, value: any): void ====
==== Section.set(name: string, value: any): void ====
Line 296: Line 415:
'''Example'''
'''Example'''


  <nowiki>ctx.settings.section('Other').set('pick-a-number', 5);</nowiki>
  <syntaxhighlight lang="js" line>ctx.settings.section('Other').set('pick-a-number', 5);</syntaxhighlight>


=== type(name: string, config: SettingTypeConfig): void ===
=== type(name: string, config: SettingTypeConfig): void ===
Line 310: Line 429:
'''Example'''
'''Example'''


  <nowiki>// manifest.json
  <syntaxhighlight lang="js" line>// manifest.json
{
{
   "namespace": "my_mod",
   "namespace": "my_mod",
   // ...
   // ...
}</nowiki>
}</syntaxhighlight>
 
<small>''Comments in JSON are purely illustrative and not valid markup''</small>


  <nowiki>ctx.settings.type('customText', {
  <syntaxhighlight lang="js" line>ctx.settings.type('customText', {
   // See example config in SettingTypeConfig section below
   // See example config in SettingTypeConfig section below
});
});


ctx.settings.section('Genera').add({
ctx.settings.section('General').add({
   type: 'customText',
   type: 'customText',
   // ...
   // ...
});</nowiki>
});</syntaxhighlight>


Other mods will have to add your namespace to use your custom type:
Other mods will have to add your namespace to use your custom type:


  <nowiki>ctx.settings.section('Other').add({
  <syntaxhighlight lang="js" line>ctx.settings.section('Other').add({
   type: 'my_mod:customText',
   type: 'my_mod:customText',
   // ...
   // ...
});</nowiki>
});</syntaxhighlight>


==== SettingTypeConfig ====
==== SettingTypeConfig ====
Line 348: Line 469:
Individual settings can opt to return validation errors in their <code>onChange</code> method. You can give a place to display this validation error in an element with a class of <code>validation-message</code>.
Individual settings can opt to return validation errors in their <code>onChange</code> method. You can give a place to display this validation error in an element with a class of <code>validation-message</code>.


  <nowiki>// The render function for a simple text box
  <syntaxhighlight lang="js" line>// The render function for a simple text box
function render(name, onChange, config) {
function render(name, onChange, config) {
   const input = document.createElement('input');
   const input = document.createElement('input');
Line 354: Line 475:
   input.type = 'text';
   input.type = 'text';
   input.name = name;
   input.name = name;
  input.addEventListener('change', () => onChange());


   const label = document.createElement('label');
   const label = document.createElement('label');
Line 362: Line 484:
     const hint = document.createElement('small');
     const hint = document.createElement('small');
     hint.textContent = config.hint;
     hint.textContent = config.hint;
     label.appendChile(hint);
     label.appendChild(hint);
   }
   }


Line 374: Line 496:


   return root;
   return root;
}</nowiki>
}</syntaxhighlight>


===== get(root: HTMLElement): any =====
===== get(root: HTMLElement): any =====
Line 380: Line 502:
The <code>get</code> function is responsible for retrieving the current value of the setting. It receives just one parameter, the root HTML element returned from the render function, which can be useful for getting the current value.
The <code>get</code> function is responsible for retrieving the current value of the setting. It receives just one parameter, the root HTML element returned from the render function, which can be useful for getting the current value.


  <nowiki>// get function for simple text input defined by above render
  <syntaxhighlight lang="js" line>// get function for simple text input defined by above render
function get(root) {
function get(root) {
   return root.querySelector('input').value;
   return root.querySelector('input').value;
}</nowiki>
}</syntaxhighlight>


===== set(root: HTMLElement, value: any): void =====
===== set(root: HTMLElement, value: any): void =====
Line 389: Line 511:
The <code>set</code> function is responsible to keeping the HTML up-to-date with the current value (if updated programmatically). It receives the root HTML element from the render function and the value being set as the two parameters.
The <code>set</code> function is responsible to keeping the HTML up-to-date with the current value (if updated programmatically). It receives the root HTML element from the render function and the value being set as the two parameters.


  <nowiki>// set function for simple text setting defined above
  <syntaxhighlight lang="js" line>// set function for simple text setting defined above
function set(root, value) {
function set(root, value) {
   root.querySelector('input').value = value;
   root.querySelector('input').value = value;
}</nowiki>
}</syntaxhighlight>


==== Example ====
==== Example ====


  <nowiki>// Use functions defined in above examples as reference
  <syntaxhighlight lang="js" line>// Use functions defined in above examples as reference
ctx.settings.type('simpleText', {
ctx.settings.type('simpleText', {
   render: render,
   render: render,
   get: get,
   get: get,
   set: set
   set: set
});</nowiki>
});</syntaxhighlight>


=== Built-In Types ===
=== Built-In Types ===
Line 409: Line 531:
All individual settings inherit this base setting config object.
All individual settings inherit this base setting config object.


  <nowiki>interface SettingConfig {
  <syntaxhighlight lang="js" line>interface SettingConfig {
   type: string; // Type of the setting
   type: string; // Type of the setting
   name: string; // Name of the setting
   name: string; // Name of the setting
Line 416: Line 538:
   default: any; // Default value for the setting
   default: any; // Default value for the setting
   onChange(value: any, previousValue: any): void | boolean | string // See notes
   onChange(value: any, previousValue: any): void | boolean | string // See notes
}</nowiki>
}</syntaxhighlight>


The <code>onChange</code> option is a callback function that receives the new value being set and the previous value of the setting. This function can optionally return a value to serve as a validator:
The <code>onChange</code> option is a callback function that receives the new value being set and the previous value of the setting. This function can optionally return a value to serve as a validator:
Line 428: Line 550:
A simple textbox that accepts any character by default. Value is of type <code>string</code>.
A simple textbox that accepts any character by default. Value is of type <code>string</code>.


  <nowiki>interface TextConfig implements SettingConfig {
  <syntaxhighlight lang="js" line>interface TextConfig implements SettingConfig {
   type: 'text';
   type: 'text';
   maxLength: number; // Max length attribute for the textbox
   maxLength: number; // Max length attribute for the textbox
}</nowiki>
}</syntaxhighlight>


==== Number ====
==== Number ====
Line 437: Line 559:
A simple textbox that only accepts numbers. Value is of type <code>number</code>.
A simple textbox that only accepts numbers. Value is of type <code>number</code>.


  <nowiki>interface NumberConfig implements SettingConfig {
  <syntaxhighlight lang="js" line>interface NumberConfig implements SettingConfig {
   type: 'number';
   type: 'number';
   min: number; // Minimum value to be entered
   min: number; // Minimum value to be entered
   max: number; // Maximum value to be entered
   max: number; // Maximum value to be entered
}</nowiki>
}</syntaxhighlight>


==== Switch ====
==== Switch ====
Line 447: Line 569:
An on/off toggle switch. Value is of type <code>boolean</code>.
An on/off toggle switch. Value is of type <code>boolean</code>.


  <nowiki>interface SwitchConfig implements SettingConfig {
  <syntaxhighlight lang="js" line>interface SwitchConfig implements SettingConfig {
   type: 'switch'
   type: 'switch'
}</nowiki>
}</syntaxhighlight>


==== Dropdown ====
==== Dropdown ====
Line 455: Line 577:
A dropdown button. Example: "Default Page on Load" game setting. Value is of type <code>any</code>.
A dropdown button. Example: "Default Page on Load" game setting. Value is of type <code>any</code>.


  <nowiki>DropdownConfig implements SettingConfig {
  <syntaxhighlight lang="js" line>DropdownConfig implements SettingConfig {
   type: 'dropdown';
   type: 'dropdown';
   color: string; // see Button config
   color: string; // see Button config
   options: DropdownOption[]; // see note
   options: DropdownOption[]; // see note
}</nowiki>
}</syntaxhighlight>


The <code>options</code> option defines the dropdown options available to be selected. Dropdown option schema is:
The <code>options</code> option defines the dropdown options available to be selected. Dropdown option schema is:


  <nowiki>interface DropdownOption {
  <syntaxhighlight lang="js" line>interface DropdownOption {
   value: any; // value that is used by the setting
   value: any; // value that is used by the setting
   display: string | HTMLElement; // display text or element on the option
   display: string | HTMLElement; // display text or element on the option
}</nowiki>
}</syntaxhighlight>


==== Button ====
==== Button ====
Line 472: Line 594:
A button. Value is <code>undefined</code>.
A button. Value is <code>undefined</code>.


  <nowiki>interface ButtonConfig implements SettingConfig {
  <syntaxhighlight lang="js" line>interface ButtonConfig implements SettingConfig {
   type: 'button';
   type: 'button';
   display: string | HTMLElement; // displayed text or element inside the button
   display: string | HTMLElement; // displayed text or element inside the button
   color: string; // see note
   color: string; // see note
   onClick(): void; // triggered on click of the button
   onClick(): void; // triggered on click of the button
}</nowiki>
}</syntaxhighlight>


The <code>color</code> option is appended to a CSS class starting with <code>btn-</code> and defaults to <code>primary</code> (<code>btn-primary</code>) if not defined. Default colors available:
The <code>color</code> option is appended to a CSS class starting with <code>btn-</code> and defaults to <code>primary</code> (<code>btn-primary</code>) if not defined. Default colors available:
Line 491: Line 613:
==== Checkbox Group ====
==== Checkbox Group ====
A group of checkboxes. Value is of type <code>any[]</code>.
A group of checkboxes. Value is of type <code>any[]</code>.
  <nowiki>interface CheckboxGroupConfig implements SettingConfig {
  <syntaxhighlight lang="js" line>interface CheckboxGroupConfig implements SettingConfig {
   type: 'checkbox-group';
   type: 'checkbox-group';
   options: CheckboxOption[]; // see note
   options: CheckboxOption[]; // see note
}</nowiki>
}</syntaxhighlight>


The <code>options</code> option defines the checkboxes that are available to be selected. Checkbox option schema is:
The <code>options</code> option defines the checkboxes that are available to be selected. Checkbox option schema is:


<nowiki>interface CheckboxOption {
<syntaxhighlight lang="js" line>interface CheckboxOption {
   value: any; // value to be added to array that is set as setting value
   value: any; // value to be added to array that is set as setting value
   label: string | HTMLElement;
   label: string | HTMLElement;
   hint: string | HTMLElement;
   hint: string | HTMLElement;
}</nowiki>
}</syntaxhighlight>


==== Radio Group ====
==== Radio Group ====
A group of radio buttons. Value is of type <code>any</code>.
A group of radio buttons. Value is of type <code>any</code>.
  <nowiki>interface RadioGroupConfig implements SettingConfig {
  <syntaxhighlight lang="js" line>interface RadioGroupConfig implements SettingConfig {
   type: 'radio-group';
   type: 'radio-group';
   options: CheckboxOption[]; // see checkbox group's options schema
   options: CheckboxOption[]; // see checkbox group's options schema
}</nowiki>
}</syntaxhighlight>


==== Label ====
==== Label ====
A simple label. Value is <code>undefined</code>.
A simple label. Value is <code>undefined</code>.
  <nowiki>interface LabelConfig implements SettingConfig {
  <syntaxhighlight lang="js" line>interface LabelConfig implements SettingConfig {
   type: 'label';
   type: 'label';
}</nowiki>
}</syntaxhighlight>


==== Custom ====
==== Custom ====
A custom-rendered setting. See [[#SettingTypeConfig|SettingTypeConfig]] section above. This is different from registering a custom setting type as this is a one-off and will not register the type for reuse. Value is of type <code>any</code>.
A custom-rendered setting. See [[#SettingTypeConfig|SettingTypeConfig]] section above. This is different from registering a custom setting type as this is a one-off and will not register the type for reuse. Value is of type <code>any</code>.
  <nowiki>interface CustomConfig implements SettingConfig, SettingTypeConfig {
  <syntaxhighlight lang="js" line>interface CustomConfig implements SettingConfig, SettingTypeConfig {
   type: 'custom';
   type: 'custom';
}</nowiki>
}</syntaxhighlight>


== Character Data Storage ==
== Character Data Storage ==
{{Disclaimer|When loading your mod as a Local Mod via the Creator Toolkit, the mod must be linked to mod.io and you must have subscribed to and installed the mod via mod.io in order for this data to persist.}}
The character storage API can be accessed through the <code>characterStorage</code> property on the root context object.
The character storage API can be accessed through the <code>characterStorage</code> property on the root context object.


=== Limitations ===
=== Limitations ===
The character storage can only be used once a character has been loaded (after lifecycle hook <code>onCharacterLoaded</code>).
The character storage can only be used once a character has been loaded (after lifecycle hook <code>onCharacterLoaded</code>).


Each character can store up to 8,192 bytes (8kb) of data per mod, including keys. Only JSON-serializable data can be stored. This includes primitive types (<code>string</code>, <code>number</code>, <code>boolean</code>) and objects and arrays that contain only primitive types or other objects or arrays that fit this description. This serialization/deserialization is handled automatically.
Each character can store up to 8,192 bytes (8kb) of data per mod, including keys. Only JSON-serializable data can be stored. This includes primitive types (<code>string</code>, <code>number</code>, <code>boolean</code>) and objects and arrays that contain only primitive types or other objects or arrays that fit this description. This serialization/deserialization is handled automatically.


=== <code>setItem(key: string, data: any): void</code> ===
=== setItem(key: string, data: any): void ===
 
Sets a key/value pair in character storage.
Sets a key/value pair in character storage.


Line 541: Line 668:


'''Example'''
'''Example'''
<nowiki>ctx.characterStorage.setItem('coolThings', ['rocks']);</nowiki>


=== <code>getItem(key: string): any</code> ===
<syntaxhighlight lang="js" line>ctx.characterStorage.setItem('coolThings', ['rocks']);</syntaxhighlight>
 
=== getItem(key: string): any ===
 
Gets a value by its key from character storage.
Gets a value by its key from character storage.


Line 555: Line 684:


'''Example'''
'''Example'''
<nowiki>ctx.characterStorage.getItem('coolThings'); // returns ['rocks']</nowiki>


=== <code>removeItem(key: string): void</code> ===
<syntaxhighlight lang="js" line>ctx.characterStorage.getItem('coolThings'); // returns ['rocks']</syntaxhighlight>
 
=== removeItem(key: string): void ===
 
Removes a key/value pair by key from character storage.
Removes a key/value pair by key from character storage.


Line 565: Line 696:


'''Example'''
'''Example'''
<nowiki>ctx.characterStorage.removeItem('coolThings');
ctx.characterStorage.getItem('coolThings'); // returns undefined</nowiki>


=== <code>clear(): void</code> ===
<syntaxhighlight lang="js" line>ctx.characterStorage.removeItem('coolThings');
ctx.characterStorage.getItem('coolThings'); // returns undefined</syntaxhighlight>
 
=== clear(): void ===
 
Removes all key/value pairs from character storage.
Removes all key/value pairs from character storage.


'''Example'''
'''Example'''
  <nowiki>ctx.characterStorage.clear();</nowiki>
 
  <syntaxhighlight lang="js" line>ctx.characterStorage.clear();</syntaxhighlight>


== Account Data Storage ==
== Account Data Storage ==
{{Disclaimer|When loading your mod as a Local Mod via the Creator Toolkit, the mod must be linked to mod.io and you must have subscribed to and installed the mod via mod.io in order for this data to persist.}}
The account storage API can be accessed through the <code>accountStorage</code> property on the root context object.
The account storage API can be accessed through the <code>accountStorage</code> property on the root context object.


=== Limitations ===
=== Limitations ===
Due to the cloud-based nature of how account data is stored and potential network issues the player may experience, data integrity is not 100% guaranteed in the account storage. Account storage is advised to be used sparingly.
Due to the cloud-based nature of how account data is stored and potential network issues the player may experience, data integrity is not 100% guaranteed in the account storage. Account storage is advised to be used sparingly.


An account can store up to 8,192 bytes (8kb) of data per mod, including keys. Only JSON-serializable data can be stored. This includes primitive types (<code>string</code>, <code>number</code>, <code>boolean</code>) and objects and arrays that contain only primitive types or other objects or arrays that fit this description. This serialization/deserialization is handled automatically.
An account can store up to 8,192 bytes (8kb) of data per mod, including keys. Only JSON-serializable data can be stored. This includes primitive types (<code>string</code>, <code>number</code>, <code>boolean</code>) and objects and arrays that contain only primitive types or other objects or arrays that fit this description. This serialization/deserialization is handled automatically.


=== <code>setItem(key: string, data: any): void</code> ===
=== setItem(key: string, data: any): void ===
 
Sets a key/value pair in account storage.
Sets a key/value pair in account storage.


Line 592: Line 731:


'''Example'''
'''Example'''
<nowiki>ctx.accountStorage.setItem('coolThings', ['rocks']);</nowiki>


=== <code>getItem(key: string): any</code> ===
<syntaxhighlight lang="js" line>ctx.accountStorage.setItem('coolThings', ['rocks']);</syntaxhighlight>
 
=== getItem(key: string): any ===
 
Gets a value by its key from account storage.
Gets a value by its key from account storage.


Line 606: Line 747:


'''Example'''
'''Example'''
<nowiki>ctx.accountStorage.getItem('coolThings'); // returns ['rocks']</nowiki>


=== <code>removeItem(key: string): void</code> ===
<syntaxhighlight lang="js" line>ctx.accountStorage.getItem('coolThings'); // returns ['rocks']</syntaxhighlight>
 
=== removeItem(key: string): void ===
 
Removes a key/value pair by key from account storage.
Removes a key/value pair by key from account storage.


Line 616: Line 759:


'''Example'''
'''Example'''
<nowiki>ctx.accountStorage.removeItem('coolThings');
ctx.accountStorage.getItem('coolThings'); // returns undefined</nowiki>


=== <code>clear(): void</code> ===
<syntaxhighlight lang="js" line>ctx.accountStorage.removeItem('coolThings');
ctx.accountStorage.getItem('coolThings'); // returns undefined</syntaxhighlight>
 
=== clear(): void ===
 
Removes all key/value pairs from account storage.
Removes all key/value pairs from account storage.


'''Example'''
'''Example'''
<nowiki>ctx.accountStorage.clear();</nowiki>


== Game Method Patching/Hooking ==
<syntaxhighlight lang="js" line>ctx.accountStorage.clear();</syntaxhighlight>
=== <code>patch(className: class, methodName: string): Patch</code> ===
 
This is the entry-point to the method patching API. The Patch object returned by this method can perform further actions with the specified class and method.
== Game Object Patching/Hooking ==
 
=== A Quick Note on Function Syntax ===
 
When patching methods, for most scenarios you'll want to use a traditional function expression, rather than the arrow expression syntax. This will ensure <code>this</code> is bound to the class instance that is calling the method, rather than the context where the patch was defined.
 
For example,
<syntaxhighlight lang="js" line="1">export function setup({ patch }) {
  const methodPatch = patch(Class, 'method');
  // Do this
  methodPatch.before(function () { });
 
  // Or this
  function beforePatch () { }
  methodPatch.before(beforePatch);
 
  // Not this, unless you understand the implications of doing so
  methodPatch.before(() => { });
}</syntaxhighlight>
 
=== patch(className: class, methodOrPropertyName: string): MethodPatch | PropertyPatch ===
 
This is the entry-point to the method and getter/setter patching API. Depending on if the second parameter is a method or getter/setter property, a <code>MethodPatch</code> or <code>PropertyPatch</code> object will be returned, respectively. The MethodPatch/PropertyPatch object should then be used to perform further actions with the specified class and method/property.


'''Parameters'''
'''Parameters'''


<code>className: class</code> Class containing the method you want to patch. Should be the actual class reference, not a string, e.g. <code>Skill</code>, not <code>'Skill'</code>.
<code>className: class</code> Class containing the method or getter/setter you want to patch. Should be the actual class reference, not a string, e.g. <code>Skill</code>, not <code>'Skill'</code>.


<code>methodName: string</code> Name of the method to patch.
<code>methodOrPropertyName: string</code> Name of the method or getter/setter property to patch.


'''Returns'''
'''Returns'''


<code>Patch</code> A patch object for the specified class and method. See below for usage.
<code>MethodPatch | PropertyPatcch</code> A patch object for the specified class and method or getter/setter property. See below for usage.


'''Example'''
'''Example'''
<nowiki>ctx.patch(Skill, 'addXP');</nowiki>


==== <code>Patch.before(hook: (...args: any) => any[] | void): void</code> ====
<syntaxhighlight lang="js" line>ctx.patch(Skill, 'addXP'); // Returns a MethodPatch
ctx.patch(Skill, 'level'); // Returns a PropertyPatch</syntaxhighlight>
 
==== MethodPatch.before(hook: (...args: any) => any[] | void): void ====
 
Execute a callback function immediately before the method body is called. The callback function's parameters are the arguments being passed into the method call. Optionally the callback function can return an array of values to override the arguments being passed to the method body. If no return value is specified (returns <code>undefined</code>), the arguments are left as-is.
Execute a callback function immediately before the method body is called. The callback function's parameters are the arguments being passed into the method call. Optionally the callback function can return an array of values to override the arguments being passed to the method body. If no return value is specified (returns <code>undefined</code>), the arguments are left as-is.


Line 650: Line 819:


'''Example'''
'''Example'''
<nowiki>// Double all XP gains
ctx.patch(Skill, 'addXP').before((amount, masteryAction) => [amount * 2, masteryAction]);</nowiki>


==== <code>Patch.after(hook: (returnValue: any, ...args: any) => any | void): void</code> ====
<syntaxhighlight lang="js" line>// Double all XP gains
ctx.patch(Skill, 'addXP').before(function (amount, masteryAction) {
  return [amount * 2, masteryAction];
});</syntaxhighlight>
 
==== MethodPatch.after(hook: (returnValue: any, ...args: any) => any | void): void ====
 
Execute a callback function immediate after the method body is finished executing. The callback function's first parameter is the value returned from the method body. The rest of the parameters are the arguments that were passed into the method body. Optionally the callback function can return a new value to override the method's return value. If no return value is specified (returns <code>undefined</code>), the return value is left as-is.
Execute a callback function immediate after the method body is finished executing. The callback function's first parameter is the value returned from the method body. The rest of the parameters are the arguments that were passed into the method body. Optionally the callback function can return a new value to override the method's return value. If no return value is specified (returns <code>undefined</code>), the return value is left as-is.


Line 661: Line 834:


'''Example'''
'''Example'''
  <nowiki>// The player never misses an attack
 
ctx.patch(Player, 'rollToHit').after(willHit => {
  <syntaxhighlight lang="js" line>// The player never misses an attack
   if (!willHit) console.log('A miss? I think not!');
// Patching: rollToHit(target: Character, attack: SpecialAttack): boolean;
ctx.patch(Player, 'rollToHit').after(function(willHit, target, attack) {
   if (!willHit) {
    console.log(`A miss? With ${attack.name}? Against ${target.noun.plain}? I think not!`);
  }
   return true;
   return true;
});</nowiki>
})</syntaxhighlight>
 
==== MethodPatch.replace(replacement: (replacedMethod: (...args: any) => any, ...args: any) => any): void ====


==== <code>Patch.replace(replacement: (replacedMethod: (...args: any) => any, ...args: any) => any): void</code> ====
Execute a callback function instead of the method's current body. The callback function's first parameter is the replaced method body. The rest of the parameters are the arguments that were to be passed to the method. The callback function's return value is the return value for the method. The replacement function is still subject to argument/return value modifications made in <code>before</code> and <code>after</code> hooks, respectively.
Execute a callback function instead of the method's current body. The callback function's first parameter is the replaced method body. The rest of the parameters are the arguments that were to be passed to the method. The callback function's return value is the return value for the method. The replacement function is still subject to argument/return value modifications made in <code>before</code> and <code>after</code> hooks, respectively.


Line 675: Line 853:


'''Example'''
'''Example'''
  <nowiki>ctx.patch(Skill, 'addXP').replace(function(o, amount, masteryAction) {
 
  <syntaxhighlight lang="js" line>ctx.patch(Skill, 'addXP').replace(function (o, amount, masteryAction) {
   // Prevent any woodcutting XP
   // Prevent any woodcutting XP
   if (this.id === 'melvorD:Woodcutting') return;
   if (this.id === 'melvorD:Woodcutting') return;
Line 684: Line 863:
   // Grant all other XP as normal
   // Grant all other XP as normal
   return o(amount, masteryAction);
   return o(amount, masteryAction);
});</nowiki>
});</syntaxhighlight>


It's important to note that using the <code>replace</code> method replaces the current method body, meaning multiple calls of the <code>replace</code> method get executed in the reverse order that they were declared:
It's important to note that using the <code>replace</code> method replaces the current method body, meaning multiple calls of the <code>replace</code> method get executed in the reverse order that they were declared:
<nowiki>const xpPatch = ctx.patch(Skill, 'addXP');


xpPatch.replace((o, amount, masteryAction) => {
<syntaxhighlight lang="js" line>const xpPatch = ctx.patch(Skill, 'addXP');
 
xpPatch.replace(function (o, amount, masteryAction) {
   console.log('Replacement #1');
   console.log('Replacement #1');
   return o(amount, masteryAction);
   return o(amount, masteryAction);
});
});


xpPatch.replace({o, amount, masteryAction) => {
xpPatch.replace(function (o, amount, masteryAction) {
   console.log('Replacement #2');
   console.log('Replacement #2');
   return o(amount, masteryAction);
   return o(amount, masteryAction);
Line 702: Line 882:
// Logs:
// Logs:
// Replacement #2
// Replacement #2
// Replacement #1</nowiki>
// Replacement #1</syntaxhighlight>
 
==== PropertyPatch.get(getter: (o: () => any) => any): void ====
 
Execute the provided function and return the return value when a getter property is accessed.
 
'''Parameters'''
 
<code>getter: (o: () => any) => any</code> The getter function to be executed. The parameter <code>o</code> is a reference to the getter method being replaced, which is either a previous getter patch or the original getter method.
 
'''Example'''
 
<syntaxhighlight lang="js" line>// Effectively double available Township resources
ctx.patch(TownshipResource, 'amount').get(function (o) {
  return o() * 2;
});
// Or more practically, make resources unlimited
ctx.patch(TownshipResource, 'amount').get(function () {
  return 999999;
});</syntaxhighlight>
 
==== PropertyPatch.set(setter: (o: (value: any) => void, value: any) => void): void ====
 
Execute the provided function when a setter property is accessed.
 
'''Parameters'''
 
<code>setter: (o: (value: any) => void, value: any) => void</code> The setter function to be executed. The first parameter, <code>o</code>, is a reference to the setter method being replaced, which is either a previous setter patch or the original setter method. The second parameter, <code>value</code>, contains the value being set.
 
'''Example'''
 
<syntaxhighlight lang="js" line>// Sorry, there aren't many setters in the game to use for a practical example
// Doubles whatever resource amount is being set
ctx.patch(TownshipResource, 'amount').set(function (o, amount) {
  return o(amount * 2);
});
// While in-game
game.township.resources.getObjectByID('melvorF:Wood').amount = 1000;
game.township.renderQueue.resourceAmounts = true;
// 2000 wood is available</syntaxhighlight>


=== <code>isPatched(className: class, methodName: string): boolean</code> ===
==== PropertyPatch.replace(getter?: (o: () => any) => any, setter?: (o: (value: any) => void, value: any) => void): void ====
Checks whether or not a method has been patched.
 
Alias for calling <code>get</code> and <code>set</code> at the same time.


'''Parameters'''
'''Parameters'''


<code>className: class</code> Class containing the method to check for having been patched. Should be the actual class reference, not a string, e.g. <code>Skill</code>, not <code>'Skill'</code>.
<code>getter: (o: () => any) => any</code> See above Parameters for <code>get</code>.


<code>methodName: string</code> Name of the method to check.
<code>setter: (o: (value: any) => void, value: any) => void</code> See above Parameters for <code>set</code>.
 
'''Example'''
 
See above examples for <code>get</code> and <code>set</code>.
 
=== isPatched(className: class, methodOrPropertyName: string): boolean ===
 
Checks whether or not a method or getter/setter property has been patched.
 
'''Parameters'''
 
<code>className: class</code> Class containing the method or property to check for having been patched. Should be the actual class reference, not a string, e.g. <code>Skill</code>, not <code>'Skill'</code>.
 
<code>methodOrPropertyName: string</code> Name of the method or property to check.


'''Returns'''
'''Returns'''


<code>boolean</code> Whether or not the given class method is patched.
<code>boolean</code> Whether or not the given class method or property is patched.


'''Example'''
'''Example'''
  <nowiki>ctx.isPatched(Skill, 'addXP'); // false
 
  <syntaxhighlight lang="js" line>ctx.isPatched(Skill, 'addXP'); // false
ctx.patch(Skill, 'addXP');
ctx.patch(Skill, 'addXP');
ctx.isPatched(Skill, 'addXP'); // true</nowiki>
ctx.isPatched(Skill, 'addXP'); // true</syntaxhighlight>


== Exposing Properties and Methods (Mod API) ==
== Exposing Properties and Methods (Mod API) ==
You may want to allow other mods to be able to interact or integrate with your mod through an API you define. To do so, the recommended approach is through the <code>api</code> method on the context object. After defining an API using the method below, other mods can access it through the global <code>mod.api['your_mods_namespace']</code> object.
You may want to allow other mods to be able to interact or integrate with your mod through an API you define. To do so, the recommended approach is through the <code>api</code> method on the context object. After defining an API using the method below, other mods can access it through the global <code>mod.api['your_mods_namespace']</code> object.


=== <code>api(endpoints?: object): object</code> ===
=== api(endpoints?: object): object ===
 
Specify properties and methods to expose on the global <code>mod.api['your_mods_namespace']</code> object. Can be called multiple times to append more endpoints.
Specify properties and methods to expose on the global <code>mod.api['your_mods_namespace']</code> object. Can be called multiple times to append more endpoints.


Line 737: Line 974:


'''Example'''
'''Example'''
  <nowiki>// manifest.json
 
  <syntaxhighlight lang="js" line>// manifest.json
{
{
   "namespace": "helloWorld",
   "namespace": "helloWorld",
   "setup": "setup.mjs"
   "setup": "setup.mjs"
}</nowiki>
}</syntaxhighlight>
 
<small>''Comments in JSON are purely illustrative and not valid markup''</small>


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ api }) {
export function setup({ api }) {
   api({
   api({
     greet: name => console.log(`Hello, ${name!}`);
     greet: name => console.log(`Hello, ${name!}`);
   });
   });
}</nowiki>
}</syntaxhighlight>


Other mods would then be able to interact with your API:  
Other mods would then be able to interact with your API:  
  <nowiki>// some other mod
 
mod.api.helloWorld.greet('Melvor'); // Hello, Melvor!</nowiki>
  <syntaxhighlight lang="js" line>// some other mod
mod.api.helloWorld.greet('Melvor'); // Hello, Melvor!</syntaxhighlight>
{{ModGuideNav}}
{{Menu}}

Latest revision as of 18:36, 10 June 2024

Accessing the Mod Context Object

All examples in this guide will assume a mod context object ctx is in the current scope.

From a Module

If the module is defined as the "setup" for your mod in manifest.json, the exported setup function will receive the context object as a sole parameter:

export function setup(ctx) {
  // ...
}

Otherwise, use the global mod.getContext method, passing in your module's meta object:

const ctx = mod.getContext(import.meta);

From a Script

The recommended approach for scripts is to use the global mod.register method. This only works in scripts injected via the "load" property of manifest.json or the loadScript method of the context object.

mod.register(ctx => {
  // ...
});

From a Lifecycle Method

All game lifecycle method callbacks will also receive their respective mod's context object as a sole parameter.

onCharacterLoaded(ctx => {
  // ...
});

From the Dev Context

For easier prototyping, you can use the global mod.getDevContext method to get a special dev mod context object. This should not be used in a mod, but only for development purposes (via the console). Any APIs that require resources will not work as the dev "mod" does not contain any resources.

const devCtx = mod.getDevContext();

Getter Properties

name: string

The name of the mod.

namespace: string | undefined

The defined namespace of the mod, if provided.

version: string

The currently loaded version of the mod.

Loading Resources

🚨 All resource file paths must be relative to the root of your mod 🚨

getResourceUrl(path: string): string

Retrieves a usable URL for any resource packaged in your mod.

Parameters

path: string The relative path to the resource to generate a URL for

Returns

string The URL to the requested resource

Example

const url = ctx.getResourceUrl('sea-shanty-2.ogg');
const song = new Audio(url);
song.loop = true;
song.play();

loadModule(path: string): Promise<any>

Dynamically imports a JavaScript module.

Parameters

path: string The relative path to the module resource

Returns

Promise<any> A promise that resolves to an object containing all exported features from the module

Example

// my-module.mjs
export function greet(name) {
  console.log(`Hello, ${name}!`);
}
const myModule = await ctx.loadModule('my-module.mjs');
myModule.greet('Melvor'); // Hello, Melvor!

loadScript(path: string): Promise<void>

Injects a JavaScript file into the page.

Parameters

path: string The relative path to the script resource

Returns

Promise<void> A promise that resolves when the injected script has finished running

Example

// Await call if wanting the script to run before continuing
await ctx.loadScript('my-script.js');
// my-script.js has run

// Don't await if no dependency on script
ctx.loadScript('my-independednt-script.js');
// my-independent-script.js has NOT run yet

loadTemplates(path: string): Promise<void>

Inject all <template> elements contained in a given HTML file into the document body.

Parameters

path: string The relative path to the HTML resource

Returns

Promise<void> A promise that is resolved once all templates have been injected into the document body.

Example

ctx.loadTemplates('my-templates.html');

loadStylesheet(path: string): void

Injects a CSS stylesheet into the page.

Parameters

path: string The relative path to the stylesheet resource

Example

ctx.loadStylesheet('my-styles.css');

loadData(path: string): Promise<any>

Loads data from a JSON resource.

Parameters

path: string The relative path to the JSON resource

Returns

Promise<any> A promise that resolves to the parsed JSON object

Example

// my-data.json
{
  "coolThings": [
    "rocks"
  ]
}

Comments in JSON are purely illustrative and not valid markup

// in JavaScript
const myData = await ctx.loadData('my-data.json');
console.log(myData.coolThings); // ['rocks']

Sharing Resources

share(resourcePath: string): void

Shares a packed mod resource for other mods to use.

Parameters

resourcePath: string The resource path to be shared.

Example

// manifest.json
{
  "namespace": "helloMelvor"
}

Comments in JSON are purely illustrative and not valid markup

// in JavaScript
ctx.share('my_cool_image.png');
ctx.share('Greeter.mjs');

Then another mod can use the resource anywhere that accepts a mod resource path.

ctx.getResourceUrl('helloMelvor:my_cool_image.png');
const { Greeter } = await loadModule('helloMelvor:Greeter.mjs');
const greeter = new Greeter();

Lifecycle Hooks

onModsLoaded(callback: (ctx: ModContext) => void | Promise<void>): void

Execute code after all mods have been loaded (character select screen).

Parameters

callback: (ctx: ModContext) => void | Promise<void> A callback function that receives the mod's context object as a parameter. Can be synchronous or asynchronous.

Example

ctx.onModsLoaded(async (ctx) => {
  // ...
});

onCharacterSelectionLoaded(callback: (ctx: ModContext) => void | Promise<void>): void

Execute code after the character selection screen has fully loaded.

Parameters

callback: (ctx: ModContext) => void | Promise<void> A callback function that receives the mod's context object as a parameter. Can be synchronous or asynchronous.

Example

ctx.onCharacterSelectionLoaded(async (ctx) => {
  // ...
});

onInterfaceAvailable(callback: (ctx: ModContext) => void | Promise<void>): void

Execute code before the character is loaded but after the game interface is initially injected into the page (but not initialized). Mostly useful for adding interface elements for custom skills that need to be present before onCharacterLoaded.

Parameters

callback: (ctx: ModContext) => void | Promise<void> A callback function that receives the mod's context object as a parameter. Can be synchronous or asynchronous.

Example

ctx.onInterfaceAvailable(async (ctx) => {
  // ...
});

onCharacterLoaded(callback: (ctx: ModContext) => void | Promise<void>): void

Execute code after the player's chosen character has loaded and all game objects are created, but before offline progress calculations.

Parameters

callback: (ctx: ModContext) => void | Promise<void> A callback function that receives the mod's context object as a parameter. Can be synchronous or asynchronous.

Example

ctx.onCharacterLoaded(async (ctx) => {
  // ...
});

onInterfaceReady(callback: (ctx: ModContext) => void | Promise<void>): void

Execute code after offline progress has been calculated and all in-game user interface elements have been created.

Parameters

callback: (ctx: ModContext) => void | Promise<void> A callback function that receives the mod's context object as a parameter. Can be synchronous or asynchronous.

Example

ctx.onInterfaceReady(async (ctx) => {
  // ...
});

Game Object Registration

The game object registration API can be accessed through the gameData property on the root context object.

addPackage(data: string | GameDataPackage): Promise<void>

Registers a game data package.

Parameters

data: string | GameDataPackage The resource path to your game data package .json file or a valid JavaScript GameDataPackage object.

Example

// data.json
{
  "$schema": "https://melvoridle.com/assets/schema/gameData.json",
  "data": {
    // data objects here
  }
}

Comments in JSON are purely illustrative and not valid markup

await ctx.gameData.addPackage('data.json');

buildPackage(builder: (packageBuilder: GameDataPackageBuilder) => void): BuiltGameDataPackage

Builds a GameDataPackage object using the GameDataPackageBuilder API.

Parameters

builder: (packageBuilder: GameDataPackageBuilder) => void The builder to be used to add individual game objects to the data package.

Returns

BuiltGameDataPackage A wrapper for the game data package. See information below.

Example

ctx.gameData.buildPackage((p) => {
  // data registration here
});

BuiltGameDataPackage.package: GameDataPackage

(Property) The actual built GameDataPackage object.

BuiltGameDataPackage.add(): void

Registers the built game data package.

Example

const pkg = ctx.gameData.buildPackage((p) => { /* ... */ });
pkg.add();

Mod Settings

When loading your mod as a Local Mod via the Creator Toolkit, the mod must be linked to mod.io and you must have subscribed to and installed the mod via mod.io in order for this data to persist.

The mod settings API can be accessed through the settings property on the root context object.

section(name: string): Section

Gets or creates a settings section. The order that sections are created are the order they will display in a mod's settings window.

Parameters

name: string The name of the section. This will be displayed as a header of the section in the settings window.

Returns

Section The section's object, used to perform add, set, or get settings.

Example

ctx.settings.section('General');
ctx.settings.section('Other');
// Sections will be displayed in the settings window in this order
// 1. General
// 2. Other

Section.add(config: SettingConfig | SettingConfig[]): void

Adds a setting to the section. The order that settings are added to a section are the order they will display in a mod's settings window.

Parameters

config: SettingConfig | SettingConfig[] The setting's configuration object or an array of configuration objects to add multiple settings at once. See Settings Types section below for setting configuration options.

Example

ctx.settings.section('General').add({
  type: 'switch',
  name: 'awesomeness-detection',
  label: 'Awesomeness Detection',
  hint: 'Determines if you are awesome or not.',
  default: false
});

ctx.settings.section('Other').add([{
    type: 'label',
    label: 'I am just a label though my story seldom told...'
  }, {
    type: 'number',
    name: 'pick-a-number',
    label: 'Pick a Number',
    hint: '1 through 10'
}]);

Section.get(name: string): any

Gets the current value of a setting by its name property.

Parameters

name: string The name of the setting to get the value of

Returns

any The current value of the setting

Example

// Assuming the player has typed "1" into the setting
ctx.settings.section('Other').get('pick-a-number'); // 1

Section.set(name: string, value: any): void

Programmatically sets the value of a setting by its name property.

Parameters

name: string The name of the setting to set the value of

value: any The value to set the setting to

Example

ctx.settings.section('Other').set('pick-a-number', 5);

type(name: string, config: SettingTypeConfig): void

Registers a setting type that can then be used by by any mod when adding a setting.

Parameters

name: string The name of the setting type. This is what should be used for the type property of a setting configuration when adding a new setting. Other mods have to prepend the name with your mod's namespace.

config: SettingTypeConfig An object defining the setting type's behavior. See definition below.

Example

// manifest.json
{
  "namespace": "my_mod",
  // ...
}

Comments in JSON are purely illustrative and not valid markup

ctx.settings.type('customText', {
  // See example config in SettingTypeConfig section below
});

ctx.settings.section('General').add({
  type: 'customText',
  // ...
});

Other mods will have to add your namespace to use your custom type:

ctx.settings.section('Other').add({
  type: 'my_mod:customText',
  // ...
});

SettingTypeConfig

All functions are required.

render(name: string, onChange: () => void, config: SettingConfig): HTMLElement

The render function is responsible for using any properties passed into the config to render HTML for the setting.

The name parameter should be used as a form of id in the setting's HTML, if needed. The common use case for this is setting an <input>'s name and id attributes to this value, and then setting a <label>'s for attribute to this value as well.

The onChange parameter should be called when this setting's value is changed. The common use case for this is adding this as an event listener to an <input> element's change event.

The config parameter holds all values passed in the config object when this setting is being added. For example, the label, hint, default, etc. properties.

Individual settings can opt to return validation errors in their onChange method. You can give a place to display this validation error in an element with a class of validation-message.

// The render function for a simple text box
function render(name, onChange, config) {
  const input = document.createElement('input');
  input.id = name;
  input.type = 'text';
  input.name = name;
  input.addEventListener('change', () => onChange());

  const label = document.createElement('label');
  label.for = name;
  label.textContent = config.label;

  if (config.hint) {
    const hint = document.createElement('small');
    hint.textContent = config.hint;
    label.appendChild(hint);
  }

  const validation = document.createElement('small');
  validation.classList.add('text-danger', 'validation-message');

  const root = document.createElement('div');
  root.appendChild(input);
  root.appendChild(label);
  root.appendChild(validation);

  return root;
}
get(root: HTMLElement): any

The get function is responsible for retrieving the current value of the setting. It receives just one parameter, the root HTML element returned from the render function, which can be useful for getting the current value.

// get function for simple text input defined by above render
function get(root) {
  return root.querySelector('input').value;
}
set(root: HTMLElement, value: any): void

The set function is responsible to keeping the HTML up-to-date with the current value (if updated programmatically). It receives the root HTML element from the render function and the value being set as the two parameters.

// set function for simple text setting defined above
function set(root, value) {
  root.querySelector('input').value = value;
}

Example

// Use functions defined in above examples as reference
ctx.settings.type('simpleText', {
  render: render,
  get: get,
  set: set
});

Built-In Types

Base Setting Configuration

All individual settings inherit this base setting config object.

interface SettingConfig {
  type: string; // Type of the setting
  name: string; // Name of the setting
  label: string | HTMLElement; // Display label for the setting
  hint: string | HTMLElement; // Small help text to display alongside the setting
  default: any; // Default value for the setting
  onChange(value: any, previousValue: any): void | boolean | string // See notes
}

The onChange option is a callback function that receives the new value being set and the previous value of the setting. This function can optionally return a value to serve as a validator:

  • No return value / undefined / true / truth-y (non-string) value: Validates successfully and allows the value to be changed
  • false / false-y value: Validation fails and setting value is restored to previous
  • string value: Validation fails, setting value is restored to previous, and the string contents are displayed in a .validation-message element, if available (see custom render above)

Text

A simple textbox that accepts any character by default. Value is of type string.

interface TextConfig implements SettingConfig {
  type: 'text';
  maxLength: number; // Max length attribute for the textbox
}

Number

A simple textbox that only accepts numbers. Value is of type number.

interface NumberConfig implements SettingConfig {
  type: 'number';
  min: number; // Minimum value to be entered
  max: number; // Maximum value to be entered
}

Switch

An on/off toggle switch. Value is of type boolean.

interface SwitchConfig implements SettingConfig {
  type: 'switch'
}

Dropdown

A dropdown button. Example: "Default Page on Load" game setting. Value is of type any.

DropdownConfig implements SettingConfig {
  type: 'dropdown';
  color: string; // see Button config
  options: DropdownOption[]; // see note
}

The options option defines the dropdown options available to be selected. Dropdown option schema is:

interface DropdownOption {
  value: any; // value that is used by the setting
  display: string | HTMLElement; // display text or element on the option
}

Button

A button. Value is undefined.

interface ButtonConfig implements SettingConfig {
  type: 'button';
  display: string | HTMLElement; // displayed text or element inside the button
  color: string; // see note
  onClick(): void; // triggered on click of the button
}

The color option is appended to a CSS class starting with btn- and defaults to primary (btn-primary) if not defined. Default colors available:

  • primary: blue
  • secondary: grey
  • success: green
  • info: light blue
  • warning: yellow
  • danger: red
  • dark: dark grey

Checkbox Group

A group of checkboxes. Value is of type any[].

interface CheckboxGroupConfig implements SettingConfig {
  type: 'checkbox-group';
  options: CheckboxOption[]; // see note
}

The options option defines the checkboxes that are available to be selected. Checkbox option schema is:

interface CheckboxOption {
  value: any; // value to be added to array that is set as setting value
  label: string | HTMLElement;
  hint: string | HTMLElement;
}

Radio Group

A group of radio buttons. Value is of type any.

interface RadioGroupConfig implements SettingConfig {
  type: 'radio-group';
  options: CheckboxOption[]; // see checkbox group's options schema
}

Label

A simple label. Value is undefined.

interface LabelConfig implements SettingConfig {
  type: 'label';
}

Custom

A custom-rendered setting. See SettingTypeConfig section above. This is different from registering a custom setting type as this is a one-off and will not register the type for reuse. Value is of type any.

interface CustomConfig implements SettingConfig, SettingTypeConfig {
  type: 'custom';
}

Character Data Storage

When loading your mod as a Local Mod via the Creator Toolkit, the mod must be linked to mod.io and you must have subscribed to and installed the mod via mod.io in order for this data to persist.

The character storage API can be accessed through the characterStorage property on the root context object.

Limitations

The character storage can only be used once a character has been loaded (after lifecycle hook onCharacterLoaded).

Each character can store up to 8,192 bytes (8kb) of data per mod, including keys. Only JSON-serializable data can be stored. This includes primitive types (string, number, boolean) and objects and arrays that contain only primitive types or other objects or arrays that fit this description. This serialization/deserialization is handled automatically.

setItem(key: string, data: any): void

Sets a key/value pair in character storage.

Parameters

key: string The key to identify the data being stored. Used in calls to getItem and removeItem.

data: any The data to be stored. See limitations above.

Example

ctx.characterStorage.setItem('coolThings', ['rocks']);

getItem(key: string): any

Gets a value by its key from character storage.

Parameters

key: string The key of the data to retrieve

Returns

any The data retrieved. Returns undefined if no such key is stored.

Example

ctx.characterStorage.getItem('coolThings'); // returns ['rocks']

removeItem(key: string): void

Removes a key/value pair by key from character storage.

Parameters

key: string The key of the key/value pair to remove

Example

ctx.characterStorage.removeItem('coolThings');
ctx.characterStorage.getItem('coolThings'); // returns undefined

clear(): void

Removes all key/value pairs from character storage.

Example

ctx.characterStorage.clear();

Account Data Storage

When loading your mod as a Local Mod via the Creator Toolkit, the mod must be linked to mod.io and you must have subscribed to and installed the mod via mod.io in order for this data to persist.

The account storage API can be accessed through the accountStorage property on the root context object.

Limitations

Due to the cloud-based nature of how account data is stored and potential network issues the player may experience, data integrity is not 100% guaranteed in the account storage. Account storage is advised to be used sparingly.

An account can store up to 8,192 bytes (8kb) of data per mod, including keys. Only JSON-serializable data can be stored. This includes primitive types (string, number, boolean) and objects and arrays that contain only primitive types or other objects or arrays that fit this description. This serialization/deserialization is handled automatically.

setItem(key: string, data: any): void

Sets a key/value pair in account storage.

Parameters

key: string The key to identify the data being stored. Used in calls to getItem and removeItem.

data: any The data to be stored. See limitations above.

Example

ctx.accountStorage.setItem('coolThings', ['rocks']);

getItem(key: string): any

Gets a value by its key from account storage.

Parameters

key: string The key of the data to retrieve

Returns

any The data retrieved. Returns undefined if no such key is stored.

Example

ctx.accountStorage.getItem('coolThings'); // returns ['rocks']

removeItem(key: string): void

Removes a key/value pair by key from account storage.

Parameters

key: string The key of the key/value pair to remove

Example

ctx.accountStorage.removeItem('coolThings');
ctx.accountStorage.getItem('coolThings'); // returns undefined

clear(): void

Removes all key/value pairs from account storage.

Example

ctx.accountStorage.clear();

Game Object Patching/Hooking

A Quick Note on Function Syntax

When patching methods, for most scenarios you'll want to use a traditional function expression, rather than the arrow expression syntax. This will ensure this is bound to the class instance that is calling the method, rather than the context where the patch was defined.

For example,

export function setup({ patch }) {
  const methodPatch = patch(Class, 'method');
  // Do this
  methodPatch.before(function () { });
  
  // Or this
  function beforePatch () { }
  methodPatch.before(beforePatch);
  
  // Not this, unless you understand the implications of doing so
  methodPatch.before(() => { });
}

patch(className: class, methodOrPropertyName: string): MethodPatch | PropertyPatch

This is the entry-point to the method and getter/setter patching API. Depending on if the second parameter is a method or getter/setter property, a MethodPatch or PropertyPatch object will be returned, respectively. The MethodPatch/PropertyPatch object should then be used to perform further actions with the specified class and method/property.

Parameters

className: class Class containing the method or getter/setter you want to patch. Should be the actual class reference, not a string, e.g. Skill, not 'Skill'.

methodOrPropertyName: string Name of the method or getter/setter property to patch.

Returns

MethodPatch | PropertyPatcch A patch object for the specified class and method or getter/setter property. See below for usage.

Example

ctx.patch(Skill, 'addXP'); // Returns a MethodPatch
ctx.patch(Skill, 'level'); // Returns a PropertyPatch

MethodPatch.before(hook: (...args: any) => any[] | void): void

Execute a callback function immediately before the method body is called. The callback function's parameters are the arguments being passed into the method call. Optionally the callback function can return an array of values to override the arguments being passed to the method body. If no return value is specified (returns undefined), the arguments are left as-is.

Parameters

hook: (...args: any) => any[] | void The callback hook to be executed.

Example

// Double all XP gains
ctx.patch(Skill, 'addXP').before(function (amount, masteryAction) {
  return [amount * 2, masteryAction];
});

MethodPatch.after(hook: (returnValue: any, ...args: any) => any | void): void

Execute a callback function immediate after the method body is finished executing. The callback function's first parameter is the value returned from the method body. The rest of the parameters are the arguments that were passed into the method body. Optionally the callback function can return a new value to override the method's return value. If no return value is specified (returns undefined), the return value is left as-is.

Parameters

hook: (returnValue: any, ...args: any) => any | void The callback hook to be executed.

Example

// The player never misses an attack
// Patching: rollToHit(target: Character, attack: SpecialAttack): boolean;
ctx.patch(Player, 'rollToHit').after(function(willHit, target, attack) {
  if (!willHit) {
    console.log(`A miss? With ${attack.name}? Against ${target.noun.plain}? I think not!`);
  }
  return true;
})

MethodPatch.replace(replacement: (replacedMethod: (...args: any) => any, ...args: any) => any): void

Execute a callback function instead of the method's current body. The callback function's first parameter is the replaced method body. The rest of the parameters are the arguments that were to be passed to the method. The callback function's return value is the return value for the method. The replacement function is still subject to argument/return value modifications made in before and after hooks, respectively.

Parameters

replacement: (replacedMethod: (...args: any) => any, ...args: any) => any The callback function to replace the method body.

Example

ctx.patch(Skill, 'addXP').replace(function (o, amount, masteryAction) {
  // Prevent any woodcutting XP
  if (this.id === 'melvorD:Woodcutting') return;

  // Double any mining XP
  if (this.id === 'melvorD:Mining') return o(amount * 2, masteryAction);

  // Grant all other XP as normal
  return o(amount, masteryAction);
});

It's important to note that using the replace method replaces the current method body, meaning multiple calls of the replace method get executed in the reverse order that they were declared:

const xpPatch = ctx.patch(Skill, 'addXP');

xpPatch.replace(function (o, amount, masteryAction) {
  console.log('Replacement #1');
  return o(amount, masteryAction);
});

xpPatch.replace(function (o, amount, masteryAction) {
  console.log('Replacement #2');
  return o(amount, masteryAction);
});

game.woodcutting.addXP(100);
// Logs:
// Replacement #2
// Replacement #1

PropertyPatch.get(getter: (o: () => any) => any): void

Execute the provided function and return the return value when a getter property is accessed.

Parameters

getter: (o: () => any) => any The getter function to be executed. The parameter o is a reference to the getter method being replaced, which is either a previous getter patch or the original getter method.

Example

// Effectively double available Township resources
ctx.patch(TownshipResource, 'amount').get(function (o) {
  return o() * 2;
});
// Or more practically, make resources unlimited
ctx.patch(TownshipResource, 'amount').get(function () {
  return 999999;
});

PropertyPatch.set(setter: (o: (value: any) => void, value: any) => void): void

Execute the provided function when a setter property is accessed.

Parameters

setter: (o: (value: any) => void, value: any) => void The setter function to be executed. The first parameter, o, is a reference to the setter method being replaced, which is either a previous setter patch or the original setter method. The second parameter, value, contains the value being set.

Example

// Sorry, there aren't many setters in the game to use for a practical example
// Doubles whatever resource amount is being set
ctx.patch(TownshipResource, 'amount').set(function (o, amount) {
  return o(amount * 2);
});
// While in-game
game.township.resources.getObjectByID('melvorF:Wood').amount = 1000;
game.township.renderQueue.resourceAmounts = true;
// 2000 wood is available

PropertyPatch.replace(getter?: (o: () => any) => any, setter?: (o: (value: any) => void, value: any) => void): void

Alias for calling get and set at the same time.

Parameters

getter: (o: () => any) => any See above Parameters for get.

setter: (o: (value: any) => void, value: any) => void See above Parameters for set.

Example

See above examples for get and set.

isPatched(className: class, methodOrPropertyName: string): boolean

Checks whether or not a method or getter/setter property has been patched.

Parameters

className: class Class containing the method or property to check for having been patched. Should be the actual class reference, not a string, e.g. Skill, not 'Skill'.

methodOrPropertyName: string Name of the method or property to check.

Returns

boolean Whether or not the given class method or property is patched.

Example

ctx.isPatched(Skill, 'addXP'); // false
ctx.patch(Skill, 'addXP');
ctx.isPatched(Skill, 'addXP'); // true

Exposing Properties and Methods (Mod API)

You may want to allow other mods to be able to interact or integrate with your mod through an API you define. To do so, the recommended approach is through the api method on the context object. After defining an API using the method below, other mods can access it through the global mod.api['your_mods_namespace'] object.

api(endpoints?: object): object

Specify properties and methods to expose on the global mod.api['your_mods_namespace'] object. Can be called multiple times to append more endpoints.

Parameters

endpoint: object An object containing any properties or methods you want to expose. Can be omitted to just retrieve your mod's current API object.

Returns

object The mod's API object

Example

// manifest.json
{
  "namespace": "helloWorld",
  "setup": "setup.mjs"
}

Comments in JSON are purely illustrative and not valid markup

// setup.mjs
export function setup({ api }) {
  api({
    greet: name => console.log(`Hello, ${name!}`);
  });
}

Other mods would then be able to interact with your API:

// some other mod
mod.api.helloWorld.greet('Melvor'); // Hello, Melvor!