Mod Creation/Essentials

Revision as of 04:30, 15 October 2022 by Buttchouda (talk | contribs) (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...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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 manifest.json 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 manifest.json file is located).

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:

{
  "namespace": "hello-world",
  "icon": "assets/icon.png",
  "setup": "src/setup.mjs",
  "load": ["assets/style.css"]
}

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.

The namespace can only contain alphanumeric characters and underscores and cannot start with the word "melvor".

Namespace Valid
helloWorld ✔️
hello_world_123 ✔️
HelloWorld!
melvorWorld

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.

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 .png or .svg, and the icon is typically displayed at a maximum of 38px in-game.

setup?: string

This property is required only if the "load" 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.

load?: string | string[]

This property is required only if the "setup" property is not present in the manifest.

This value accepts either a single path or an array of paths to resources to load. These resources are loaded in the order of the array, after the "setup" resource has been run. Valid resources to be loaded through this property are JavaScript script files (.js), JavaScript module files (.mjs), CSS stylesheets (.css), and HTML files containing templates (.html). However, unless your mod is very simple, the recommended approach to loading JavaScript resources (.js or .mjs) is through code in your mod's entry-point ("setup").

It's also important to note that while .js is considered a valid extension for JavaScript module files for the "setup" property, modules loaded through "load" must end with .mjs or they will be treated as regular script files.

Structuring Your Code

Using Modules (Recommended)

There are a number of ways to structure your code to be loaded, whether it's scripts or modules, "setup" or "load". Each might have a good use case but the recommended approach for most mods is to write your code using JavaScript modules and to have a single entry-point (defined as "setup" in manifest.json), while leaving the "load" property exclusively for loading your CSS.

Using this approach will keep your code clean and manageable, while avoiding polluting the global JavaScript scope, and as a result, avoiding conflicts with other mods. If you're unfamiliar with JavaScript modules, you can check out resources like JavaScript Modules on W3Schools or on MDN for a more detailed look. The general pattern you'll be using is exporting module "features" (functions, variables, etc) and then import them into other modules for use. This is also the approach this guide will use in all of its examples.

Let's start with what a module that's defined as your mod's "setup" entry-point should look like:

// setup.mjs
export function setup(ctx) {
  console.log('Hello World!');
}

We export a function named setup here because that is what the Mod Manager looks for when loading a "setup" module. Without one, an error would be thrown when loading this mod. This setup 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.

To take advantage of the modular approach to JavaScript, however, we cannot use a static import at the top of the file like we would prefer to in a regular JavaScript environment. Due to the nature of how modding resources are stored, we have to dynamically import modules using a special loadModule function, contained within the context object. This acts identical to a dynamic import but allows you to use a resource path relative to the root of your mod.

If we define a helper module helper.mjs:

// helper.mjs
export function greet(name) {
  console.log(`Hello, ${name}!`);
}

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

// setup.mjs
export async function setup({ loadModule }) {
  const { greet } = await loadModule('helper.mjs');
  greet('Melvor'); // > Hello, Melvor!
}

If you need to access the context object from your helper module, there are two approaches:

1. Pass the context object from the setup function to the loaded module:

// configService.mjs
export function init(ctx) {
  // Perform actions using the context object here...
}

// setup.mjs
export async function setup(ctx) {
  const configService = await ctx.loadModule('configService.mjs');
  configService.init(ctx);
}

2. Use the getContext method on the global mod object:

// configService.mjs
const ctx = mod.getContext(import.meta);

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

You must pass import.meta - a special JavaScript object available in all modules - to the mod.getContext method to receive your mod's context object.

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 "setup" module) or the "load" 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.

export async function setup({ loadScript }) {
  // Make sure you await the call to loadScript if your code beyond relies on it
  await loadScript('hello-melvor-script.js');
  // hello-melvor-script.js has executed

  // But don't bother awaiting it if it's not time-sensitive
  loadScript('some-independent-script.js');
}

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

mod.register(ctx => {
  // Use the context object here
});

Note that the mod.register method will only work on scripts injected through either loadScript or the "load" property of the manifest.