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