Mod Creation/Essentials: Difference between revisions

no edit summary
(Created page with "First time writing a mod for Melvor Idle? Consider starting with the Mod Creation/Getting Started guide. == Creating a Mod == 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 y...")
 
No edit summary
Line 121: Line 121:


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.
== 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.
== 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.
=== Load (Import) a Module ===
Use <code>ctx.loadModule</code> to import a JavaScript module's exported features.
<nowiki>// my-module.mjs
export function greet(name) {
  console.log(`Hello, ${name}!`);
}
export const importantData = ['e', 'r', 'e', 'h', 't', ' ', 'o', 'l', 'l', 'e', 'h'];</nowiki>
<nowiki>// setup.mjs
export async function setup({ loadModule }) {
  const myModule = await loadModule('my-module.mjs');
  myModule.greet('Melvor'); // Hello, Melvor!
  console.log(myModule.importantData.reverse().join('')); // hello there
}</nowiki>
=== Load (Inject) a Script ===
Use <code>ctx.loadScript</code> to inject a JavaScript file into the page.
<nowiki>// setup.mjs
export await function setup({ loadScript }) {
  // Wait for script to run
  await loadScript('my-script.js');
  // Or not
  loadScript('my-independent-script.js');
}</nowiki>
=== Load (Inject) HTML Templates ===
Use <code>ctx.loadTemplates</code> to inject all <code>&lt;template&gt;</code> elements into the document body.
<nowiki>// setup.mjs
export function setup({ loadTemplates }) {
  loadTemplates('my-templates.html');
}</nowiki>
=== Load (Inject) a Stylesheet ===
Use <code>ctx.loadStylesheet</code> to inject a CSS file into the page.
<nowiki>// setup.mjs
export function setup({ loadStylesheet }) {
  loadStylesheet('my-styles.css');
}</nowiki>
=== Load Data from JSON ===
Use <code>ctx.loadData</code> to read and automatically parse a JSON resource.
<nowiki>// my-data.json
{
  "coolThings": [
    "rocks"
  ]
}</nowiki>
<nowiki>// setup.mjs
export async function setup({ loadData }) {
  const data = await loadData('my-data.json');
  console.log(data.coolThings[0]); // ['rocks']
}</nowiki>
=== 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.
<nowiki>// setup.mjs
export function setup({ getResourceUrl }) {
  const url = getResourceUrl('sea-shanty-2.ogg');
  const song = new Audio(url);
  song.loop = true;
  song.play();
}</nowiki>
== 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:
* <code>onModsLoaded</code>: Occurs after all enabled mods have completed their initial load
* <code>onCharacterSelectionLoaded</code>: Occurs after the character selection screen is completely loaded.
* <code>onCharacterLoaded</code>: Occurs after a character has been selected and all game objects have been constructed, but before offline progress has been calculated.
* <code>onInterfaceReady</code>: Occurs after offline progress has been calculated and the in-game interface can be reliably modified. Also useful for any long-running, or non-vital processes that might negatively impact the player experience by increased character load times.
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
export function setup({ onModsLoaded, onCharacterLoaded, onInterfaceReady }) {
  onModsLoaded(ctx => {
    // Utilize other mod APIs at character select
  });
  onCharacterSelectionLoaded(ctx => {
    // Build or modify character selection UI elements
  });
  onCharacterLoaded(ctx => {
    // Modify or hook into game objects to influence offline calculations
  });
  onInterfaceReady(ctx => {
    // Build or modify in-game UI elements
  });
}</nowiki>
== 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.
Settings' values are persisted on a per-character basis and will be saved within the character's save file.
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
export function setup({ settings }) {
  // Creates a section labeled "General"
  settings.section('General');
  // Future calls to that section will not create a new "General" section, but instead return the already existing one
  settings.section('General');
}</nowiki>
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
export function setup({ settings }) {
  const generalSettings = settings.section('General');
  // You can add settings one-by-one
  generalSettings.add({
    type: 'switch',
    name: 'awesomeness-detection',
    label: 'Awesomeness Detection',
    hint: 'Determines if you are awesome or not.',
    default: false
  });
  // Or multiple at a time by passing in an array
  generalSettings.add([{
    type: 'label',
    display: 'I am just a label though my story seldom told...'
  }, {
    type: 'number',
    name: 'pick-a-number',
    label: 'Pick a Number',
    hint: '1 through 10'
  }]);
}</nowiki>
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
const { settings } = mod.getContext(import.meta);
const generalSettings = settings.section('General');
generalSettings.set('pick-a-number', 1);
console.log(generalSettings.get('pick-a-number')); // 1</nowiki>
=== Setting Types ===
There are currently eight predefined setting types that will automatically create a usable input:
* Text
* Number
* Switch
* Dropdown
* Button
* Checkbox Group
* Radio Group
* Label
In addition, you add a custom setting by configuring additional properties. For more information on the configuration options available for each of these, refer to the relevant section of the [[Mod Creation/Mod Context API Reference]] guide.
== 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:
* Sidebar
** Categories
*** Items
**** Subitems
An example of a category is Combat and Attack would be an item within that category. Subitems are what's used for the Completion Log's sections.
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
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>
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', {
  before: 'Attack', // Move the Slayer item above Attack
  ignoreToggle: true // Keep Slayer visible when its category has been hidden
});</nowiki>
The full definition of each sidebar piece's configuration object can be found in the [[Mod Creation/Sidebar API Reference]] guide.
If you need to retrieve all existing categories, items, or subitems, use their respective methods:
<nowiki>sidebar.categories(); // returns an array of all categories
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>
Removing categories, items, and subitems is also possible:
<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.removeAllCategories(); // Remove all categories, but why?
// Same kind of structure for items and subitems:
sidebar.category('Modding').item('Mod Manager').remove();
sidebar.category('General').item('Completion Log').removeAllSubitems();</nowiki>
You can find specifics about the sidebar API in the [[Mod Creation/Sidebar API Reference]] guide.
== 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).
<nowiki>// setup.mjs
export function setup({ characterStorage }) {
  // This would all function identically with accountStorage, but also be available across characters
  characterStorage.setItem('my-favorite-pet', 7);
  console.log(PETS[characterStorage.getItem('my-favorite-pet')].name); // Larry, the Lonely Lizard
  characterStorage.removeItem('my-favorite-pet');
  characterStorage.clear(); // Removes all currently stored items
}</nowiki>
=== 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.
In addition, only JSON-serializable data can be stored. This includes any JavaScript primitive value (strings, numbers, and booleans) or an object or array containing only primitive values (or an object or array containing only primitive values, etc.). You do not have to serialize/deserialize the data yourself.
Finally, due to the nature of account data being persisted to the cloud, data integrity cannot be 100% guaranteed due to possible network issues the player might experience.
== 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:
<nowiki>// setup.mjs
export function setup({ patch }) {
  const xpPatch = patch(Skill, 'addXP');
}</nowiki>
From there you can use that patch to perform any of the following actions.
=== 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.
<nowiki>// setup.mjs
export function setup({ patch }) {
  patch(Skill, 'addXP').before((amount, masteryAction) => {
    console.log(`Doubling XP from ${amount} to ${amount * 2}!`);
    return [amount * 2, masteryAction]; // Double all XP gains
  });
}</nowiki>
=== 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.''
<nowiki>// setup.mjs
export function setup({ patch }) {
  patch(Player, 'rollToHit').after((willHit) => {
    if (!willHit) console.log('A miss? I think not!');
    return true;
  });
}</nowiki>
=== 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.
<nowiki>// setup.mjs
export function setup({ patch }) {
  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);
  });
}</nowiki>
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
export function setup({ patch, onInterfaceReady }) {
  const xpPatch = patch(Skill, 'addXP');
  xpPatch.replace(xpA);
  xpPatch.replace(xpB);
  onInterfaceReady(() => {
    game.woodcutting.addXP(100);
    // Logs:
    // XP replace B
    // XP replace A
  });
}
function xpA(o, amount, masteryAction) {
  console.log('XP replace A');
  return o(amount, masteryAction);
}
function xpB(o, amount, masteryAction) {
  console.log('XP replace B');
  return o(amount, masteryAction);
}</nowiki>
== 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.
<nowiki>// manifest.json
{
  "namespace": "helloWorld",
  "setup": "setup.mjs"
}</nowiki>
<nowiki>// setup.mjs
export function setup({ api }) {
  api({
    greet: name => console.log(`Hello, ${name!}`);
  });
}</nowiki>
Other mods would then be able to interact with your API:
<nowiki>// some other mod
mod.api.helloWorld.greet('Melvor'); // Hello, Melvor!</nowiki>
== 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:
<nowiki>const devCtx = mod.getDevContext();</nowiki>
''This method/context '''should not''' be used from within a mod.''
== 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:
* [[Mod Creation/Mod Context API Reference]]
* [[Mod Creation/Sidebar API Reference]]
89

edits