Mod Creation/Essentials: Difference between revisions

Use SyntaxHighlight
(User:Coolrox95 Add mod guide navigation)
(Use SyntaxHighlight)
Line 13: Line 13:
Before you begin writing code, it's a good idea to start by defining some metadata in the manifest.json file. A complete manifest.json might look like the following:
Before you begin writing code, it's a good idea to start by defining some metadata in the manifest.json file. A complete manifest.json might look like the following:


  <nowiki>{
  <syntaxhighlight lang="js" line>{
   "namespace": "helloWorld",
   "namespace": "helloWorld",
   "icon": "assets/icon.png",
   "icon": "assets/icon.png",
   "setup": "src/setup.mjs",
   "setup": "src/setup.mjs",
   "load": ["assets/style.css"]
   "load": ["assets/style.css"]
}</nowiki>
}</syntaxhighlight>


==== namespace?: string ====
==== namespace?: string ====
Line 26: Line 26:
The namespace can only contain alphanumeric characters and underscores and cannot start with the word "melvor".
The namespace can only contain alphanumeric characters and underscores and cannot start with the word "melvor".


{|
{| class="wikitable"
! Namespace !! Valid !!
! Namespace !! Valid  
|-
|-
| <code>helloWorld</code> || ✔️
| <code>helloWorld</code> || ✔️
Line 70: Line 70:
Let's start with what a module that's defined as your mod's <code>"setup"</code> entry-point should look like:
Let's start with what a module that's defined as your mod's <code>"setup"</code> entry-point should look like:


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup(ctx) {
export function setup(ctx) {
   console.log('Hello World!');
   console.log('Hello World!');
}</nowiki>
}</syntaxhighlight>


We export a function named <code>setup</code> here because that is what the Mod Manager looks for when loading a <code>"setup"</code> module. Without one, an error would be thrown when loading this mod. This <code>setup</code> function is called and receives the mod's context object as soon as the mod is loaded, which happens just before the character select screen is visible. Therefore, this mod would write 'Hello World!' to the console at that time.
We export a function named <code>setup</code> here because that is what the Mod Manager looks for when loading a <code>"setup"</code> module. Without one, an error would be thrown when loading this mod. This <code>setup</code> function is called and receives the mod's context object as soon as the mod is loaded, which happens just before the character select screen is visible. Therefore, this mod would write 'Hello World!' to the console at that time.
Line 81: Line 81:
If we define a helper module helper.mjs:
If we define a helper module helper.mjs:


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


We can then use code we export in our setup function:
We can then use code we export in our setup function:


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export async function setup({ loadModule }) {
export async function setup({ loadModule }) {
   const { greet } = await loadModule('helper.mjs');
   const { greet } = await loadModule('helper.mjs');
   greet('Melvor'); // > Hello, Melvor!
   greet('Melvor'); // > Hello, Melvor!
}</nowiki>
}</syntaxhighlight>


If you need to access the context object from your helper module, there are two approaches:
If you need to access the context object from your helper module, there are two approaches:
Line 98: Line 98:
1. Pass the context object from the setup function to the loaded module:
1. Pass the context object from the setup function to the loaded module:


  <nowiki>// configService.mjs
  <syntaxhighlight lang="js" line>// configService.mjs
export function init(ctx) {
export function init(ctx) {
   // Perform actions using the context object here...
   // Perform actions using the context object here...
Line 107: Line 107:
   const configService = await ctx.loadModule('configService.mjs');
   const configService = await ctx.loadModule('configService.mjs');
   configService.init(ctx);
   configService.init(ctx);
}</nowiki>
}</syntaxhighlight>


2. Use the <code>getContext</code> method on the global <code>mod</code> object:
2. Use the <code>getContext</code> method on the global <code>mod</code> object:


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


export function init() {
export function init() {
   // Perform actions using the context object here...
   // Perform actions using the context object here...
}</nowiki>
}</syntaxhighlight>


You must pass <code>import.meta</code> - a special JavaScript object available in all modules - to the <code>mod.getContext</code> method to receive your mod's context object.
You must pass <syntaxhighlight lang="js" inline>import.meta</syntaxhighlight> - a special JavaScript object available in all modules - to the <syntaxhighlight lang="js" inline>mod.getContext</syntaxhighlight> method to receive your mod's context object.


==== Using Scripts ====
==== Using Scripts ====
Line 126: Line 126:
Loading a script through the context object is very similar to loading a module but you will not receive back a value.
Loading a script through the context object is very similar to loading a module but you will not receive back a value.


  <nowiki>export async function setup({ loadScript }) {
  <syntaxhighlight lang="js" line>export async function setup({ loadScript }) {
   // Make sure you await the call to loadScript if your code beyond relies on it
   // Make sure you await the call to loadScript if your code beyond relies on it
   await loadScript('hello-melvor-script.js');
   await loadScript('hello-melvor-script.js');
Line 133: Line 133:
   // But don't bother awaiting it if it's not time-sensitive
   // But don't bother awaiting it if it's not time-sensitive
   loadScript('some-independent-script.js');
   loadScript('some-independent-script.js');
}</nowiki>
}</syntaxhighlight>


From inside your script, you can still access the context object:
From inside your script, you can still access the context object:


  <nowiki>mod.register(ctx => {
  <syntaxhighlight lang="js" line>mod.register(ctx => {
   // Use the context object here
   // Use the context object here
});</nowiki>
});</syntaxhighlight>


Note that the mod.register method will only work on scripts injected through either loadScript or the "load" property of the manifest.
Note that the mod.register method will only work on scripts injected through either loadScript or the "load" property of the manifest.
Line 155: Line 155:
=== Load (Import) a Module ===
=== Load (Import) a Module ===


Use <code>ctx.loadModule</code> to import a JavaScript module's exported features.
Use <syntaxhighlight lang="js" inline>ctx.loadModule</syntaxhighlight> to import a JavaScript module's exported features.


  <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}!`);
}
}


export const importantData = ['e', 'r', 'e', 'h', 't', ' ', 'o', 'l', 'l', 'e', 'h'];</nowiki>
export const importantData = ['e', 'r', 'e', 'h', 't', ' ', 'o', 'l', 'l', 'e', 'h'];</syntaxhighlight>


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export async function setup({ loadModule }) {
export async function setup({ loadModule }) {
   const myModule = await loadModule('my-module.mjs');
   const myModule = await loadModule('my-module.mjs');
   myModule.greet('Melvor'); // Hello, Melvor!
   myModule.greet('Melvor'); // Hello, Melvor!
   console.log(myModule.importantData.reverse().join('')); // hello there
   console.log(myModule.importantData.reverse().join('')); // hello there
}</nowiki>
}</syntaxhighlight>


=== Load (Inject) a Script ===
=== Load (Inject) a Script ===


Use <code>ctx.loadScript</code> to inject a JavaScript file into the page.
Use <syntaxhighlight lang="js" inline>ctx.loadScript</syntaxhighlight> to inject a JavaScript file into the page.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export await function setup({ loadScript }) {
export await function setup({ loadScript }) {
   // Wait for script to run
   // Wait for script to run
Line 181: Line 181:
   // Or not
   // Or not
   loadScript('my-independent-script.js');
   loadScript('my-independent-script.js');
}</nowiki>
}</syntaxhighlight>


=== Load (Inject) HTML Templates ===
=== Load (Inject) HTML Templates ===


Use <code>ctx.loadTemplates</code> to inject all <code>&lt;template&gt;</code> elements into the document body.
Use <syntaxhighlight lang="js" inline>ctx.loadTemplates</syntaxhighlight> to inject all <syntaxhighlight lang="html" inline><template></syntaxhighlight> elements into the document body.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ loadTemplates }) {
export function setup({ loadTemplates }) {
   loadTemplates('my-templates.html');
   loadTemplates('my-templates.html');
}</nowiki>
}</syntaxhighlight>


=== Load (Inject) a Stylesheet ===
=== Load (Inject) a Stylesheet ===


Use <code>ctx.loadStylesheet</code> to inject a CSS file into the page.
Use <syntaxhighlight lang="js" inline>ctx.loadStylesheet</syntaxhighlight> to inject a CSS file into the page.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ loadStylesheet }) {
export function setup({ loadStylesheet }) {
   loadStylesheet('my-styles.css');
   loadStylesheet('my-styles.css');
}</nowiki>
}</syntaxhighlight>


=== Load Data from JSON ===
=== Load Data from JSON ===


Use <code>ctx.loadData</code> to read and automatically parse a JSON resource.
Use <syntaxhighlight lang="js" inline>ctx.loadData</syntaxhighlight> to read and automatically parse a JSON resource.


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


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export async function setup({ loadData }) {
export async function setup({ loadData }) {
   const data = await loadData('my-data.json');
   const data = await loadData('my-data.json');
   console.log(data.coolThings[0]); // ['rocks']  
   console.log(data.coolThings[0]); // ['rocks']  
}</nowiki>
}</syntaxhighlight>


=== Images, Sounds, and Anything Else ===
=== Images, Sounds, and Anything Else ===


Nearly any resource can be accessed and used in some way with <code>ctx.getResourceUrl</code> - the helper methods above all use this behind the scenes. With the resource's URL, you can use built-in JavaScript methods to consume the resource.
Nearly any resource can be accessed and used in some way with <syntaxhighlight lang="js" inline>ctx.getResourceUrl</syntaxhighlight> - the helper methods above all use this behind the scenes. With the resource's URL, you can use built-in JavaScript methods to consume the resource.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ getResourceUrl }) {
export function setup({ getResourceUrl }) {
   const url = getResourceUrl('sea-shanty-2.ogg');
   const url = getResourceUrl('sea-shanty-2.ogg');
Line 228: Line 228:
   song.loop = true;
   song.loop = true;
   song.play();
   song.play();
}</nowiki>
}</syntaxhighlight>


== Game Lifecycle Hooks ==
== Game Lifecycle Hooks ==
Line 243: Line 243:
All of the game's lifecycle hooks are available through your mod's context object and accept a callback function as a sole parameter. This callback function can be synchronous or asynchronous and will be executed at the specified time and receive your mod's context object as a parameter.
All of the game's lifecycle hooks are available through your mod's context object and accept a callback function as a sole parameter. This callback function can be synchronous or asynchronous and will be executed at the specified time and receive your mod's context object as a parameter.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ onModsLoaded, onCharacterLoaded, onInterfaceReady }) {
export function setup({ onModsLoaded, onCharacterLoaded, onInterfaceReady }) {
   onModsLoaded(ctx => {
   onModsLoaded(ctx => {
Line 260: Line 260:
     // Build or modify in-game UI elements
     // Build or modify in-game UI elements
   });
   });
}</nowiki>
}</syntaxhighlight>


== Adding and Modifying Game Objects ==
== Adding and Modifying Game Objects ==
Line 282: Line 282:
To begin with this approach, your JSON files should all be constructed with:
To begin with this approach, your JSON files should all be constructed with:


  <nowiki>{
  <syntaxhighlight lang="js" line>{
   "$schema": "https://melvoridle.com/assets/schema/gameData.json",
   "$schema": "https://melvoridle.com/assets/schema/gameData.json",
   "data": {
   "data": {


   }
   }
}</nowiki>
}</syntaxhighlight>


If you're using a text editor that supports it, you should now get autocomplete and type checking on the fields you create.
If you're using a text editor that supports it, you should now get autocomplete and type checking on the fields you create.
Line 293: Line 293:
Here is an example of defining an item:
Here is an example of defining an item:


  <nowiki>{
  <syntaxhighlight lang="js" line>{
   "$schema": "https://melvoridle.com/assets/schema/gameData.json",
   "$schema": "https://melvoridle.com/assets/schema/gameData.json",
   "data": {
   "data": {
Line 327: Line 327:
     }]
     }]
   }
   }
}</nowiki>
}</syntaxhighlight>


You would then register your game data using one of the following methods:
You would then register your game data using one of the following methods:


  <nowiki>// manifest.json
  <syntaxhighlight lang="js" line>// manifest.json
{
{
   "namespace": "helloWorld",
   "namespace": "helloWorld",
   "load": ["path-to-your-data.json"]
   "load": ["path-to-your-data.json"]
}</nowiki>
}</syntaxhighlight>


''or''
''or''


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export async function setup({ gameData }) {
export async function setup({ gameData }) {
   await gameData.addPackage('path-to-your-data.json');
   await gameData.addPackage('path-to-your-data.json');
}</nowiki>
}</syntaxhighlight>


=== Building a Data Package at Runtime ===
=== Building a Data Package at Runtime ===
Line 358: Line 358:
The entry-point for using this approach looks like this:
The entry-point for using this approach looks like this:


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ gameData }) {
export function setup({ gameData }) {
   gameData.buildPackage((p) => {
   gameData.buildPackage((p) => {
     // use the `p` object to add game objects
     // use the `p` object to add game objects
   }).add();
   }).add();
}</nowiki>
}</syntaxhighlight>


Following the same example above of adding an item:
Following the same example above of adding an item:


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ gameData }) {
export function setup({ gameData }) {
   gameData.buildPackage((p) => {
   gameData.buildPackage((p) => {
Line 399: Line 399:
     });
     });
   }).add();
   }).add();
}</nowiki>
}</syntaxhighlight>


Your game data should already be registered from the <code>.add()</code> method being called on your built package.
Your game data should already be registered from the <code>.add()</code> method being called on your built package.
Line 413: Line 413:
Settings are divided (in code and visually) into sections. Get or create a section using the <code>section(name)</code> method on the <code>settings</code> object. The value passed in for the <code>name</code> parameter is used as a header for the section, so this should be human-readable. These sections are displayed in the order that they are created.
Settings are divided (in code and visually) into sections. Get or create a section using the <code>section(name)</code> method on the <code>settings</code> object. The value passed in for the <code>name</code> parameter is used as a header for the section, so this should be human-readable. These sections are displayed in the order that they are created.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ settings }) {
export function setup({ settings }) {
   // Creates a section labeled "General"
   // Creates a section labeled "General"
Line 420: Line 420:
   // Future calls to that section will not create a new "General" section, but instead return the already existing one
   // Future calls to that section will not create a new "General" section, but instead return the already existing one
   settings.section('General');
   settings.section('General');
}</nowiki>
}</syntaxhighlight>


The object returned from using <code>section()</code> can then be used for adding settings to that section. Refer to the next section for settings configurations.
The object returned from using <code>section()</code> can then be used for adding settings to that section. Refer to the next section for settings configurations.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ settings }) {
export function setup({ settings }) {
   const generalSettings = settings.section('General');
   const generalSettings = settings.section('General');
Line 446: Line 446:
     hint: '1 through 10'
     hint: '1 through 10'
   }]);
   }]);
}</nowiki>
}</syntaxhighlight>


You can then <code>get</code> or <code>set</code> the value of any defined setting by its <code>name</code> property.
You can then <code>get</code> or <code>set</code> the value of any defined setting by its <code>name</code> property.


  <nowiki>// elsewhere.mjs
  <syntaxhighlight lang="js" line>// elsewhere.mjs
const { settings } = mod.getContext(import.meta);
const { settings } = mod.getContext(import.meta);


const generalSettings = settings.section('General');
const generalSettings = settings.section('General');
generalSettings.set('pick-a-number', 1);
generalSettings.set('pick-a-number', 1);
console.log(generalSettings.get('pick-a-number')); // 1</nowiki>
console.log(generalSettings.get('pick-a-number')); // 1</syntaxhighlight>


=== Setting Types ===
=== Setting Types ===
Line 487: Line 487:
Each of the customizable (categories, items, subitems) pieces are generally interacted with the same way.
Each of the customizable (categories, items, subitems) pieces are generally interacted with the same way.


  <nowiki>const combat = sidebar.catetory('Combat'); // Get the Combat category, or create one if it doesn't exist
  <syntaxhighlight lang="js" line>const combat = sidebar.catetory('Combat'); // Get the Combat category, or create one if it doesn't exist
const attack = sidebar.category('Combat').item('Attack'); // Get the Attack item within Combat or create one if it doesn't exist
const attack = sidebar.category('Combat').item('Attack'); // Get the Attack item within Combat or create one if it doesn't exist
attack.subitem('Wut'); // Get the Wut subitem within Attack or create one if it doesn't exist</nowiki>
attack.subitem('Wut'); // Get the Wut subitem within Attack or create one if it doesn't exist</syntaxhighlight>


In addition, these can be called with a configuration object as a second parameter to create or update the existing piece with the new configuration.
In addition, these can be called with a configuration object as a second parameter to create or update the existing piece with the new configuration.


  <nowiki>sidebar.category('Combat').item('Slayer', {
  <syntaxhighlight lang="js" line>sidebar.category('Combat').item('Slayer', {
   before: 'Attack', // Move the Slayer item above Attack
   before: 'Attack', // Move the Slayer item above Attack
   ignoreToggle: true // Keep Slayer visible when its category has been hidden
   ignoreToggle: true // Keep Slayer visible when its category has been hidden
});</nowiki>
});</syntaxhighlight>


The full definition of each sidebar piece's configuration object can be found in the [[Mod Creation/Sidebar API Reference]] guide.
The full definition of each sidebar piece's configuration object can be found in the [[Mod Creation/Sidebar API Reference]] guide.
Line 502: Line 502:
If you need to retrieve all existing categories, items, or subitems, use their respective methods:
If you need to retrieve all existing categories, items, or subitems, use their respective methods:


  <nowiki>sidebar.categories(); // returns an array of all categories
  <syntaxhighlight lang="js" line>sidebar.categories(); // returns an array of all categories
sidebar.category('Combat').items(); // returns an array of all Combat items
sidebar.category('Combat').items(); // returns an array of all Combat items
sidebar.category('General').item('Completion Log').subitems(); // returns an array of all Completion Log subitems</nowiki>
sidebar.category('General').item('Completion Log').subitems(); // returns an array of all Completion Log subitems</syntaxhighlight>


Removing categories, items, and subitems is also possible:
Removing categories, items, and subitems is also possible:


  <nowiki>sidebar.category('Non-Combat').remove(); // Remove the entire Non-Combat category
  <syntaxhighlight lang="js" line>sidebar.category('Non-Combat').remove(); // Remove the entire Non-Combat category
sidebar.removeCategory('Combat'); // Alternative (this avoids creating a Combat category if it didn't already exist)
sidebar.removeCategory('Combat'); // Alternative (this avoids creating a Combat category if it didn't already exist)
sidebar.removeAllCategories(); // Remove all categories, but why?
sidebar.removeAllCategories(); // Remove all categories, but why?
Line 514: Line 514:
// Same kind of structure for items and subitems:
// Same kind of structure for items and subitems:
sidebar.category('Modding').item('Mod Manager').remove();
sidebar.category('Modding').item('Mod Manager').remove();
sidebar.category('General').item('Completion Log').removeAllSubitems();</nowiki>
sidebar.category('General').item('Completion Log').removeAllSubitems();</syntaxhighlight>


== Creating Reusable HTML Components ==
== Creating Reusable HTML Components ==
Line 528: Line 528:
If you have the following HTML file:
If you have the following HTML file:


  <nowiki><!-- templates.html -->
  <syntaxhighlight lang="html" line><!-- templates.html -->
<template id="counter-component">
<template id="counter-component">
   <span class="text-light">{{ count }}</span>
   <span class="text-light">{{ count }}</span>
   <button class="btn btn-secondary" @click="inc">+</button>
   <button class="btn btn-secondary" @click="inc">+</button>
</template></nowiki>
</template></syntaxhighlight>


You would import the template in one of the following two ways:
You would import the template in one of the following two ways:


  <nowiki>// manifest.json
  <syntaxhighlight lang="js" line>// manifest.json
{
{
   "load": "templates.html"
   "load": "templates.html"
}</nowiki>
}</syntaxhighlight>


''or''
''or''


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ loadTemplates }) {
export function setup({ loadTemplates }) {
   loadTemplates('templates.html');
   loadTemplates('templates.html');
}</nowiki>
}</syntaxhighlight>


=== Defining a Component ===
=== Defining a Component ===
Line 552: Line 552:
Using the [https://github.com/vuejs/petite-vue#components PetiteVue documentation on components], you should define each component as a function. This component should define its template selector using the <code>$template</code> property, and then any additional properties or methods that the rendered component will use. For example:
Using the [https://github.com/vuejs/petite-vue#components PetiteVue documentation on components], you should define each component as a function. This component should define its template selector using the <code>$template</code> property, and then any additional properties or methods that the rendered component will use. For example:


  <nowiki>function Counter(props) {
  <syntaxhighlight lang="js" line>function Counter(props) {
   return {
   return {
     $template: '#counter-component',
     $template: '#counter-component',
Line 560: Line 560:
     }
     }
   };
   };
}</nowiki>
}</syntaxhighlight>


=== Creating a Component Within the UI ===
=== Creating a Component Within the UI ===
Line 566: Line 566:
Now that your template is loaded and you have a component defined, you can use the helper function <code>ui.create</code> to create an instance of the component within the UI.
Now that your template is loaded and you have a component defined, you can use the helper function <code>ui.create</code> to create an instance of the component within the UI.


  <nowiki>// Create a counter component at the bottom of the Woodcutting page
  <syntaxhighlight lang="js" line>// Create a counter component at the bottom of the Woodcutting page
ui.create(Counter({ count: 0 }), document.getElementById('woodcutting-container'));</nowiki>
ui.create(Counter({ count: 0 }), document.getElementById('woodcutting-container'));</syntaxhighlight>


== Storing Data ==
== Storing Data ==
Line 577: Line 577:
There are two options for storing data for your mod that isn't already saved as part of the game or settings: data saved with a character or data saved to the player's account. For most cases, however, character storage should be the preferred location and account storage used sparingly. Both of these stores are available through your mod's context object, as <code>characterStorage</code> and <code>accountStorage</code>, respectively. Aside from where the data is ultimately saved, character and account storage have identical methods and behaviors. Character storage is not available until after a character has been loaded (<code>onCharacterLoaded</code> lifecycle hook).
There are two options for storing data for your mod that isn't already saved as part of the game or settings: data saved with a character or data saved to the player's account. For most cases, however, character storage should be the preferred location and account storage used sparingly. Both of these stores are available through your mod's context object, as <code>characterStorage</code> and <code>accountStorage</code>, respectively. Aside from where the data is ultimately saved, character and account storage have identical methods and behaviors. Character storage is not available until after a character has been loaded (<code>onCharacterLoaded</code> lifecycle hook).


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ characterStorage }) {
export function setup({ characterStorage }) {
   // This would all function identically with accountStorage, but also be available across characters
   // This would all function identically with accountStorage, but also be available across characters
Line 587: Line 587:


   characterStorage.clear(); // Removes all currently stored items
   characterStorage.clear(); // Removes all currently stored items
}</nowiki>
}</syntaxhighlight>


=== Limitations ===
=== Limitations ===
Line 603: Line 603:
A common modding scenario is to want to override/modify an in-game method or perform an action before or after it has completed. Your mod's context object contains a patch property that can be used for this these cases. Patches can only be applied to methods that exist on a JavaScript class (<code>Player</code>, <code>Enemy</code>, <code>CombatManager</code>, <code>Woodcutting</code>, etc.). To start, define the class and method that you want to patch:
A common modding scenario is to want to override/modify an in-game method or perform an action before or after it has completed. Your mod's context object contains a patch property that can be used for this these cases. Patches can only be applied to methods that exist on a JavaScript class (<code>Player</code>, <code>Enemy</code>, <code>CombatManager</code>, <code>Woodcutting</code>, etc.). To start, define the class and method that you want to patch:


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
   const xpPatch = patch(Skill, 'addXP');
   const xpPatch = patch(Skill, 'addXP');
}</nowiki>
}</syntaxhighlight>


From there you can use that patch to perform any of the following actions.
From there you can use that patch to perform any of the following actions.
Line 614: Line 614:
Use the <code>before</code> method on the patch object to execute code immediately before the patched method. In addition, the callback hook will receive the arguments that were used to call the patched method as parameters, and can optionally modify them by returning the new arguments as an array.
Use the <code>before</code> method on the patch object to execute code immediately before the patched method. In addition, the callback hook will receive the arguments that were used to call the patched method as parameters, and can optionally modify them by returning the new arguments as an array.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
   patch(Skill, 'addXP').before((amount, masteryAction) => {
   patch(Skill, 'addXP').before((amount, masteryAction) => {
Line 620: Line 620:
     return [amount * 2, masteryAction]; // Double all XP gains
     return [amount * 2, masteryAction]; // Double all XP gains
   });
   });
}</nowiki>
}</syntaxhighlight>


=== Do Something After ===
=== Do Something After ===
Line 626: Line 626:
Use the <code>after</code> method on the patch object to execute code immediately after the patched method. In addition, the callback hook will receive the value returned from the patched method along with the arguments used to call it as parameters. Optionally, an after hook can choose to override the returned value by returning a value itself. '''''Only''' a return value of <code>undefined</code> will be ignored.''
Use the <code>after</code> method on the patch object to execute code immediately after the patched method. In addition, the callback hook will receive the value returned from the patched method along with the arguments used to call it as parameters. Optionally, an after hook can choose to override the returned value by returning a value itself. '''''Only''' a return value of <code>undefined</code> will be ignored.''


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
   patch(Player, 'rollToHit').after((willHit) => {
   patch(Player, 'rollToHit').after((willHit) => {
Line 632: Line 632:
     return true;
     return true;
   });
   });
}</nowiki>
}</syntaxhighlight>


=== Replace the Method Entirely ===
=== Replace the Method Entirely ===
Line 638: Line 638:
The <code>replace</code> method on the patch object will override the patched method's body, but before and after hooks will still be executed. The replacement method will receive the current method implementation (the one being replaced) along with the arguments used to call it as parameters. The return value of the replacement method will be the return value of the method call, subject to any changes made in an after hook.
The <code>replace</code> method on the patch object will override the patched method's body, but before and after hooks will still be executed. The replacement method will receive the current method implementation (the one being replaced) along with the arguments used to call it as parameters. The return value of the replacement method will be the return value of the method call, subject to any changes made in an after hook.


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
   patch(Skill, 'addXP').replace(function(o, amount, masteryAction) {
   patch(Skill, 'addXP').replace(function(o, amount, masteryAction) {
Line 645: Line 645:


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


     // 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 the using the replace method replaces the '''current''' method implementation. This means that multiple replacements on the same patched method will be executed in reverse order than they were declared:
It's important to note that the using the replace method replaces the '''current''' method implementation. This means that multiple replacements on the same patched method will be executed in reverse order than they were declared:


  <nowiki>// setup.mjs
  <syntaxhighlight lang="js" line>// setup.mjs
export function setup({ patch, onInterfaceReady }) {
export function setup({ patch, onInterfaceReady }) {
   const xpPatch = patch(Skill, 'addXP');
   const xpPatch = patch(Skill, 'addXP');
Line 677: Line 677:
   console.log('XP replace B');
   console.log('XP replace B');
   return o(amount, masteryAction);
   return o(amount, masteryAction);
}</nowiki>
}</syntaxhighlight>


== Exposing APIs ==
== Exposing APIs ==
Line 685: Line 685:
If your mod serves as a tool for other mods to integrate with, exposing APIs through the context object using <code>ctx.api</code> is the recommended approach. This is especially useful when paired with a mod developed using modules. The <code>api</code> method accepts an object and will expose any properties on that object to the global <code>mod</code> object within the <code>api['your-mods-namespace']</code> property. You can call the <code>api</code> method multiple times to append more APIs.
If your mod serves as a tool for other mods to integrate with, exposing APIs through the context object using <code>ctx.api</code> is the recommended approach. This is especially useful when paired with a mod developed using modules. The <code>api</code> method accepts an object and will expose any properties on that object to the global <code>mod</code> object within the <code>api['your-mods-namespace']</code> property. You can call the <code>api</code> method multiple times to append more APIs.


  <nowiki>// manifest.json
  <syntaxhighlight lang="js" line>// manifest.json
{
{
   "namespace": "helloWorld",
   "namespace": "helloWorld",
   "setup": "setup.mjs"
   "setup": "setup.mjs"
}</nowiki>
}</syntaxhighlight>


  <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
  <syntaxhighlight lang="js" line>// some other mod
mod.api.helloWorld.greet('Melvor'); // Hello, Melvor!</nowiki>
mod.api.helloWorld.greet('Melvor'); // Hello, Melvor!</syntaxhighlight>


== The Dev Context ==
== The Dev Context ==
Line 707: Line 707:
To make it easier to test code before committing to uploading a mod, there is a 'dev' mod context that you can access to try out any of the context object's methods that don't require additional resources, i.e. you can't use <code>loadModule</code>. To access this context, you can use the following in your browser console:
To make it easier to test code before committing to uploading a mod, there is a 'dev' mod context that you can access to try out any of the context object's methods that don't require additional resources, i.e. you can't use <code>loadModule</code>. To access this context, you can use the following in your browser console:


  <nowiki>const devCtx = mod.getDevContext();</nowiki>
  <syntaxhighlight lang="js" line>const devCtx = mod.getDevContext();</syntaxhighlight>


''This method/context '''should not''' be used from within a mod.''
''This method/context '''should not''' be used from within a mod.''
Line 717: Line 717:
* [[Mod Creation/Sidebar API Reference]]
* [[Mod Creation/Sidebar API Reference]]
{{ModGuideNav}}
{{ModGuideNav}}
{{Menu}}