Setting up your Plugin

Note: In this section, we'll be making the following assumptions:

Plugin name: "Tutorial Plugin"

Plugin ID: "com.tutorial.plugin"

DLL Name: "tutorial-plugin.dll"

Now that you've set up your environment, you can start setting up your plugin!

First of all, make a folder to keep all your development work in one place. For example, making a Workspace folder in your Documents folder.

Initializing the project

Open a new terminal in this folder and run:

cargo init --lib tutorial-plugin

This will create a folder called tutorial-plugin containing a basic rust template. Open this folder in VSCode (or your editor of choice.)

Adding dependencies

In the VSCode toolbar on the top-left, click Terminal > New Terminal. In the terminal that appears, run the following:

cargo add --git https://codeberg.org/ExanimaModding/Toolkit.git emf-rs
cargo add log
cargo add pretty-env-logger

If you open /Cargo.toml, you should see these in the [dependencies] section.

Now add a new section to /Cargo.toml:

[lib]
crate-type = ["cdylib"]

This will tell the Rust compiler to create a .dll file when you build the project.

Your /Cargo.toml should now look similar to:

[package]
name = "tutorial-plugin"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
emf-rs = { git = "https://codeberg.org/ExanimaModding/Toolkit.git", version = "0.1.0-beta.1" }
log = "0.4.22"
pretty_env_logger = "0.5.0"

Setting up the plugin config file

Now we need to make the plugin config for EMTK to read. Make a file called config.toml in the root of your project.

Here's an example of what it should contain currently:

[plugin]
id = "com.tutorial.plugin"
executable = "tutorial-plugin.dll"
enabled = true
name = "Tutorial Plugin"
description = "My cool plugin!"
version = "1.0.0"
supported_versions = [] # Not functional yet
url = "https://github.com/yourusername/your-repo" # This should be the URL to your plugin repo.

[plugin.author]
name = "Your Name"
contact = "some way of contacting you" # This is optional, remove if not needed.
url = "https://your-domain.com" # This is optional, remove if not needed.

The id of the plugin should be in Reverse Domain Notation (e.g. dev.megu.my-plugin-name). If you don't have a domain, just use com.your-name.your-plugin-name.

Setting up the boilerplate

Open up src/lib.rs and delete the default code that's in there. Then paste in the following:

use emf_rs::{macros::plugin, once_cell::sync::OnceCell, safer_ffi::prelude::char_p};
use log::*;

static FIRST_RUN: OnceCell<bool> = OnceCell::new();

#[plugin(id = "com.tutorial.plugin")]
mod plugin {}

#[no_mangle]
pub extern "C" fn enable() {
    FIRST_RUN.get_or_init(|| {
        pretty_env_logger::formatted_builder()
            .filter_level(LevelFilter::Debug)
            .init();
        true
    });

    info!("Tutorial plugin enabled");
}

#[no_mangle]
pub extern "C" fn disable() {
    info!("Tutorial plugin disabled");
}

#[no_mangle]
pub extern "C" fn setting_changed_bool(name: char_p::Box, value: bool) {
    let name = name.to_string();

    info!("Setting changed: {} = {}", name, value);
}

#[no_mangle]
pub extern "C" fn setting_changed_int(name: char_p::Box, value: i32) {
    let name = name.to_string();

    info!("Setting changed: {} = {}", name, value);
}

#[no_mangle]
pub extern "C" fn setting_changed_float(name: char_p::Box, value: f32) {
    let name = name.to_string();

    info!("Setting changed: {} = {}", name, value);
}

#[no_mangle]
pub extern "C" fn setting_changed_string(name: char_p::Box, value: char_p::Box) {
    let name = name.to_string();
    let value = value.to_string();

    info!("Setting changed: {} = {}", name, value);
}

Explaining the code

In this section, we will explain the meaning behind each bit of code shown above.

The imports

The first part of the code is the imports. emf_rs is the library you'll use for interacting with EMF. It provides utility methods for scanning, reading, writing memory, as well as easing the creation of function hooks and byte patches.

log is just used as a logging library, providing macros like info!, warn!, error!, and debug!. It will prefix the log with the name of your plugin so that logs can be differentiated.

use emf_rs::{macros::plugin, once_cell::sync::OnceCell, safer_ffi::prelude::char_p};
use log::*;

The plugin macro

emf-rs provides a macro that will make it easy to define byte patches and hooks. It also provides helpers for getting settings and scanning memory.

#[plugin(id = "com.tutorial.plugin")]
mod plugin {}

The enable and disable functions

These functions are executed when your plugin is enabled or disabled by EMF. Typically, if a user has your plugin enabled, the enable function will run when Exanima starts.

Where possible, you should strive to support the disable event as well - undoing or disabling all your changes when it runs. Don't worry though, EMF makes it very easy to enable and disable function hooks and byte patches!

static FIRST_RUN: OnceCell<bool> = OnceCell::new();

#[no_mangle]
pub extern "C" fn enable() {
    FIRST_RUN.get_or_init(|| {
        pretty_env_logger::formatted_builder()
            .filter_level(LevelFilter::Debug)
            .init();
        true
    });

    info!("Tutorial plugin enabled");
}

#[no_mangle]
pub extern "C" fn disable() {
    info!("Tutorial plugin disabled");
}

The static FIRST_RUN is used to run code once. In this case, it's being used to initialise pretty_env_logger which is used to show the plugin's logs. If this were to run multiple times, it would cause an error. The log level Debug being used means that info!, warn!, error!, and debug! logs will be shown, but trace! logs will not.

The settings functions

These functions run whenever the user changes a setting in the in-game overlay. When the user hits save, and event will be fired for each setting that has been changed, and will call the appropriate function.

Because your plugin communicates with EMF over C-FFI, unfortunately, the types are a little strange.

You can think of char_p::Box as a pointer to a string in memory. To turn this into a valid rust string, we just need to run .to_string() on it, which will take ownership of the pointer.

#[no_mangle]
pub extern "C" fn setting_changed_bool(name: char_p::Box, value: bool) {
    let name = name.to_string();

    info!("Setting changed: {} = {}", name, value);
}

#[no_mangle]
pub extern "C" fn setting_changed_int(name: char_p::Box, value: i32) {
    let name = name.to_string();

    info!("Setting changed: {} = {}", name, value);
}

#[no_mangle]
pub extern "C" fn setting_changed_float(name: char_p::Box, value: f32) {
    let name = name.to_string();

    info!("Setting changed: {} = {}", name, value);
}

#[no_mangle]
pub extern "C" fn setting_changed_string(name: char_p::Box, value: char_p::Box) {
    let name = name.to_string();
    let value = value.to_string();

    info!("Setting changed: {} = {}", name, value);
}