Mod Creation/Essentials: Difference between revisions

no edit summary
No edit summary
No edit summary
Line 2: Line 2:


== Creating a Mod ==
== Creating a Mod ==
A mod in Melvor Idle is simply composed of two parts: metadata and resources.
A mod in Melvor Idle is simply composed of two parts: metadata and resources.


The '''metadata''' is defined in your mod's mod.io profile page (name, version, previews, etc.) and in a <code>manifest.json</code> file that '''''must''' be located at the root of your mod's directory'' (this holds metadata that tells the Mod Manager how to load your mod).
The '''metadata''' is defined in your mod's mod.io profile page (name, version, previews, etc.) and in a <code>manifest.json</code> file that '''''must''' be located at the root of your mod's directory'' (this holds metadata that tells the Mod Manager how to load your mod).


'''Resources''' are really everything else. JavaScript modules and scripts, CSS stylesheets, images, sounds, etc. are all considered resources that will be accessed through either the manifest or dynamically through your JavaScript code. When referencing a resource from anywhere (manifestor code), the path should '''''always''' be relative to the root of your mod'' (where the <code>manifest.json</code> file is located).
'''Resources''' are really everything else. JavaScript modules and scripts, CSS stylesheets, images, sounds, etc. are all considered resources that will be accessed through either the manifest or dynamically through your JavaScript code. When referencing a resource from anywhere (manifest or code), the path should '''''always''' be relative to the root of your mod'' (where the <code>manifest.json</code> file is located).


=== The Manifest ===
=== The Manifest ===
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>{
  <nowiki>{
   "namespace": "hello-world",
   "namespace": "hello-world",
Line 17: Line 20:
}</nowiki>
}</nowiki>


==== <code>namespace?: string</code> ====
==== namespace?: string ====
 
A few important modding APIs (tools) available from JavaScript require a namespace to be defined. This helps the game to keep your mod's data organized - think of this as an id for your mod that will be used by other mods and the game itself to access stored data that you've defined. As such, it's best to choose a namespace that easily identifies your mod in case another mod wants to interact with it.
A few important modding APIs (tools) available from JavaScript require a namespace to be defined. This helps the game to keep your mod's data organized - think of this as an id for your mod that will be used by other mods and the game itself to access stored data that you've defined. As such, it's best to choose a namespace that easily identifies your mod in case another mod wants to interact with it.


Line 36: Line 40:
While this property is optional, it's good practice to include it to avoid future troubleshooting if you end up using an API that requires a namespace.
While this property is optional, it's good practice to include it to avoid future troubleshooting if you end up using an API that requires a namespace.


==== <code>icon?: string</code> ====
==== icon?: string ====
 
An optional icon to be displayed alongside your mod in a number of places, like the My Mods list in the Mod Manager. The value should be the path to the image file relative to the root of your mod (where your manifest is located). Accepted file types for an icon are <code>.png</code> or <code>.svg</code>, and the icon is typically displayed at a maximum of 38px in-game.
An optional icon to be displayed alongside your mod in a number of places, like the My Mods list in the Mod Manager. The value should be the path to the image file relative to the root of your mod (where your manifest is located). Accepted file types for an icon are <code>.png</code> or <code>.svg</code>, and the icon is typically displayed at a maximum of 38px in-game.


==== <code>setup?: string</code> ====
==== setup?: string ====
 
''This property is '''required''' only if the <code>"load"</code> property is not present in the manifest''.
''This property is '''required''' only if the <code>"load"</code> property is not present in the manifest''.


This value should be a file path pointing to a JavaScript module to act as the entry-point to your mod; this concept will be covered more in the following section.
This value should be a file path pointing to a JavaScript module to act as the entry-point to your mod; this concept will be covered more in the following section.


==== <code>load?: string | string[]</code> ====
==== load?: string | string[] ====
 
''This property is '''required''' only if the <code>"setup"</code> property is not present in the manifest.''
''This property is '''required''' only if the <code>"setup"</code> property is not present in the manifest.''


Line 52: Line 59:


=== Structuring Your Code ===
=== Structuring Your Code ===
==== Using Modules (Recommended) ====
==== Using Modules (Recommended) ====
There are a number of ways to structure your code to be loaded, whether it's scripts or modules, <code>"setup"</code> or <code>"load"</code>. Each might have a good use case but the recommended approach for most mods is to write your code using [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules JavaScript modules] and to have a single entry-point (defined as <code>"setup"</code> in <code>manifest.json</code>), while leaving the "load" property exclusively for loading your CSS.
There are a number of ways to structure your code to be loaded, whether it's scripts or modules, <code>"setup"</code> or <code>"load"</code>. Each might have a good use case but the recommended approach for most mods is to write your code using [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules JavaScript modules] and to have a single entry-point (defined as <code>"setup"</code> in <code>manifest.json</code>), while leaving the "load" property exclusively for loading your CSS.


Line 58: Line 67:


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
  <nowiki>// setup.mjs
export function setup(ctx) {
export function setup(ctx) {
Line 68: Line 78:


If we define a helper module helper.mjs:
If we define a helper module helper.mjs:
  <nowiki>// helper.mjs
  <nowiki>// helper.mjs
export function greet(name) {
export function greet(name) {
Line 74: Line 85:


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
  <nowiki>// setup.mjs
export async function setup({ loadModule }) {
export async function setup({ loadModule }) {
Line 83: Line 95:


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
  <nowiki>// configService.mjs
export function init(ctx) {
export function init(ctx) {
Line 93: Line 106:
   configService.init(ctx);
   configService.init(ctx);
}</nowiki>
}</nowiki>
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
  <nowiki>// configService.mjs
const ctx = mod.getContext(import.meta);
const ctx = mod.getContext(import.meta);
Line 100: Line 115:
   // Perform actions using the context object here...
   // Perform actions using the context object here...
}</nowiki>
}</nowiki>
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 <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.


==== Using Scripts ====
==== Using Scripts ====
If you choose to include plain scripts in your mod, whether it's out of familiarity or a special use case, you can load (inject) scripts into the game either through the context object (perhaps received from a <code>"setup"</code> module) or the <code>"load"</code> property of the manifest.
If you choose to include plain scripts in your mod, whether it's out of familiarity or a special use case, you can load (inject) scripts into the game either through the context object (perhaps received from a <code>"setup"</code> module) or the <code>"load"</code> property of the manifest.


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 }) {
  <nowiki>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
Line 116: Line 134:


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 => {
  <nowiki>mod.register(ctx => {
   // Use the context object here
   // Use the context object here
Line 123: Line 142:


== The Context Object ==
== The Context Object ==
Your mod's context object is the central point used for setting up your mod and making modifications to the game. The majority of the other sections in this guide will cover the concepts enabled through the APIs available on the object. For a more in-depth look at the documentation for the context object, refer to the [[Mod Creation/Mod Context API Reference]] guide.
Your mod's context object is the central point used for setting up your mod and making modifications to the game. The majority of the other sections in this guide will cover the concepts enabled through the APIs available on the object. For a more in-depth look at the documentation for the context object, refer to the [[Mod Creation/Mod Context API Reference]] guide.


== Accessing Your Mod's Resources ==
== Accessing Your Mod's Resources ==
Chances are you will package some resources in your mod that aren't covered by the loading options defined in the manifest and instead need to rely on loading these resources through your code. Your mod's context object provides methods for retrieving these resources. Keep in mind that all file path references to your resources should be ''relative to the root of your mod''. Some common scenarios are below.
Chances are you will package some resources in your mod that aren't covered by the loading options defined in the manifest and instead need to rely on loading these resources through your code. Your mod's context object provides methods for retrieving these resources. Keep in mind that all file path references to your resources should be ''relative to the root of your mod''. Some common scenarios are below.


=== Load (Import) a Module ===
=== Load (Import) a Module ===
Use <code>ctx.loadModule</code> to import a JavaScript module's exported features.
Use <code>ctx.loadModule</code> to import a JavaScript module's exported features.


Line 146: Line 168:


=== Load (Inject) a Script ===
=== Load (Inject) a Script ===
Use <code>ctx.loadScript</code> to inject a JavaScript file into the page.
Use <code>ctx.loadScript</code> to inject a JavaScript file into the page.


Line 157: Line 180:


=== 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 <code>ctx.loadTemplates</code> to inject all <code>&lt;template&gt;</code> elements into the document body.


Line 165: Line 189:


=== Load (Inject) a Stylesheet ===
=== Load (Inject) a Stylesheet ===
Use <code>ctx.loadStylesheet</code> to inject a CSS file into the page.
Use <code>ctx.loadStylesheet</code> to inject a CSS file into the page.


Line 173: Line 198:


=== Load Data from JSON ===
=== Load Data from JSON ===
Use <code>ctx.loadData</code> to read and automatically parse a JSON resource.
Use <code>ctx.loadData</code> to read and automatically parse a JSON resource.


Line 181: Line 207:
   ]
   ]
}</nowiki>
}</nowiki>
  <nowiki>// setup.mjs
  <nowiki>// setup.mjs
export async function setup({ loadData }) {
export async function setup({ loadData }) {
Line 188: Line 215:


=== 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 <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.


Line 199: Line 227:


== Game Lifecycle Hooks ==
== Game Lifecycle Hooks ==
Utilizing the game's lifecycle hooks will allow your mod to perform actions at specific times, which may be useful for waiting for certain game objects to be available. The game lifecycle hooks are as follows:
Utilizing the game's lifecycle hooks will allow your mod to perform actions at specific times, which may be useful for waiting for certain game objects to be available. The game lifecycle hooks are as follows:


Line 207: Line 236:


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
  <nowiki>// setup.mjs
export function setup({ onModsLoaded, onCharacterLoaded, onInterfaceReady }) {
export function setup({ onModsLoaded, onCharacterLoaded, onInterfaceReady }) {
Line 227: Line 257:


== Mod Settings ==
== Mod Settings ==
Your mod can define settings for the player to interact with and visually configure your mod in-game. This feature is accessible through a <code>settings</code> property on the context object. If your mod has any settings defined, your mod will appear in the sidebar under Mod Settings. Clicking this will open up a window with all of your defined settings.
Your mod can define settings for the player to interact with and visually configure your mod in-game. This feature is accessible through a <code>settings</code> property on the context object. If your mod has any settings defined, your mod will appear in the sidebar under Mod Settings. Clicking this will open up a window with all of your defined settings.


Line 232: Line 263:


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
  <nowiki>// setup.mjs
export function setup({ settings }) {
export function setup({ settings }) {
Line 242: Line 274:


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
  <nowiki>// setup.mjs
export function setup({ settings }) {
export function setup({ settings }) {
Line 267: Line 300:


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
  <nowiki>// elsewhere.mjs
const { settings } = mod.getContext(import.meta);
const { settings } = mod.getContext(import.meta);
Line 275: Line 309:


=== Setting Types ===
=== Setting Types ===
There are currently eight predefined setting types that will automatically create a usable input:
There are currently eight predefined setting types that will automatically create a usable input:


Line 289: Line 324:


== Customizing the Sidebar ==
== Customizing the Sidebar ==
If you want to add or modify the in-game sidebar (the menu with the bank, skills, etc.) there is an globally-scoped in-game API, <code>sidebar</code>, for doing so. The sidebar is organized into four levels:
If you want to add or modify the in-game sidebar (the menu with the bank, skills, etc.) there is an globally-scoped in-game API, <code>sidebar</code>, for doing so. The sidebar is organized into four levels:


Line 299: Line 335:


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
  <nowiki>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
Line 304: Line 341:


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', {
  <nowiki>sidebar.category('Combat').item('Slayer', {
   before: 'Attack', // Move the Slayer item above Attack
   before: 'Attack', // Move the Slayer item above Attack
Line 312: Line 350:


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
  <nowiki>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
Line 317: Line 356:


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
  <nowiki>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)
Line 326: Line 366:


You can find specifics about the sidebar API in the [[Mod Creation/Sidebar API Reference]] guide.
You can find specifics about the sidebar API in the [[Mod Creation/Sidebar API Reference]] guide.
== Creating Reusable HTML Components ==
coming soon


== Storing Data ==
== Storing Data ==
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
  <nowiki>// setup.mjs
export function setup({ characterStorage }) {
export function setup({ characterStorage }) {
Line 342: Line 388:


=== Limitations ===
=== Limitations ===
Currently, a mod's character storage and account storage are each (separately) limited to 8,192 bytes (8kb) of total data. This means each character can store up to 8kb per mod, but only 8kb total can be  stored to an account.
Currently, a mod's character storage and account storage are each (separately) limited to 8,192 bytes (8kb) of total data. This means each character can store up to 8kb per mod, but only 8kb total can be  stored to an account.


Line 349: Line 396:


== Game Object Patching/Hooking ==
== Game Object Patching/Hooking ==
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
  <nowiki>// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
Line 358: Line 407:


=== Do Something Before ===
=== Do Something Before ===
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
  <nowiki>// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
Line 368: Line 419:


=== Do Something After ===
=== Do Something After ===
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
  <nowiki>// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
Line 378: Line 431:


=== Replace the Method Entirely ===
=== Replace the Method Entirely ===
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
  <nowiki>// setup.mjs
export function setup({ patch }) {
export function setup({ patch }) {
Line 394: Line 449:


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
  <nowiki>// setup.mjs
export function setup({ patch, onInterfaceReady }) {
export function setup({ patch, onInterfaceReady }) {
Line 420: Line 476:


== Exposing APIs ==
== Exposing 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.
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
  <nowiki>// manifest.json
{
{
Line 426: Line 484:
   "setup": "setup.mjs"
   "setup": "setup.mjs"
}</nowiki>
}</nowiki>
  <nowiki>// setup.mjs
  <nowiki>// setup.mjs
export function setup({ api }) {
export function setup({ api }) {
Line 434: Line 493:


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


== The Dev Context ==
== The Dev Context ==
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>
  <nowiki>const devCtx = mod.getDevContext();</nowiki>


Line 444: Line 506:


== Next Steps ==
== Next Steps ==
Hopefully this guide covers most common modding scenarios and will be a useful reference during your mod development time. For more in-depth looks at specific concepts, consider checking out the following guides:
Hopefully this guide covers most common modding scenarios and will be a useful reference during your mod development time. For more in-depth looks at specific concepts, consider checking out the following guides:
* [[Mod Creation/Mod Context API Reference]]
* [[Mod Creation/Mod Context API Reference]]
* [[Mod Creation/Sidebar API Reference]]
* [[Mod Creation/Sidebar API Reference]]
91

edits