Crate arma_rs

Source
Expand description

§arma-rs

Join the arma-rs Discord! codecov

The best way to make Arma 3 Extensions.

§Usage

[dependencies]
arma-rs = "1.11.10"

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

§Hello World

use arma_rs::{arma, Extension};

#[arma]
fn init() -> Extension {
    Extension::build()
        .command("hello", hello)
        .command("welcome", welcome)
        .finish()
}

pub fn hello() -> &'static str {
    "Hello"
}

pub fn welcome(name: String) -> String {
    format!("Welcome {}", name)
}
"my_extension" callExtension ["hello", []]; // Returns ["Hello", 0, 0]
"my_extension" callExtension ["welcome", ["John"]]; // Returns ["Welcome John", 0, 0]

§Command Groups

Commands can be grouped together, making your large projects much easier to manage.

use arma_rs::{arma, Extension, Group};

#[arma]
fn init() -> Extension {
    Extension::build()
        .group("hello",
            Group::new()
                .command("english", hello::english)
                .group("english",
                    Group::new()
                        .command("casual", hello::english_casual)
                )
                .command("french", hello::french),
        )
        .group("welcome",
            Group::new()
                .command("english", welcome::english)
                .command("french", welcome::french),
        )
        .finish()
}

mod hello {
    pub fn english() -> &'static str {
        "Hello"
    }
    pub fn english_casual() -> &'static str {
        "Hey"
    }
    pub fn french() -> &'static str {
        "Bonjour"
    }
}

mod welcome {
    pub fn english(name: String) -> String {
        format!("Welcome {}", name)
    }
    pub fn french(name: String) -> String {
        format!("Bienvenue {}", name)
    }
}

Commands groups are called by using the format group:command. You can nest groups as much as you want.

"my_extension" callExtension ["hello:english", []]; // Returns ["Hello", 0, 0]
"my_extension" callExtension ["hello:english:casual", []]; // Returns ["Hey", 0, 0]
"my_extension" callExtension ["hello:french", []]; // Returns ["Bonjour", 0, 0]

§Callbacks

Extension callbacks can be invoked anywhere in the extension by adding a variable of type Context to the start of a handler.

use arma_rs::Context;

pub fn sleep(ctx: Context, duration: u64, id: String) {
    std::thread::spawn(move || {
        std::thread::sleep(std::time::Duration::from_secs(duration));
        ctx.callback_data("example_timer", "done", Some(id));
    });
}

pub fn group() -> arma_rs::Group {
    arma_rs::Group::new().command("sleep", sleep)
}

§Call Context

Since Arma v2.11 additional context is provided each time the extension is called. This context can be accessed through the optional ArmaCallContext argument.

Since Arma v2.18 the context is only requested from Arma when the functionh has ArmaCallContext as an argument.

use arma_rs::{CallContext, CallContextStackTrace};

pub fn call_context(call_context: CallContext) -> String {
    format!(
        "{:?},{:?},{:?},{:?},{:?}",
        call_context.caller(),
        call_context.source(),
        call_context.mission(),
        call_context.server(),
        call_context.remote_exec_owner(),
    )
}

pub fn stack_trace(call_context: CallContextStackTrace) -> String {
    format!(
        "{:?}\n{:?}",
        call_context.source(),
        call_context.stack_trace()
    )
}

pub fn group() -> arma_rs::Group {
    arma_rs::Group::new()
        .command("call_context", call_context)
        .command("stack_trace", stack_trace)
}

§Persistent State

Both the extension and command groups allow for type based persistent state values with at most one instance per type. These state values can then be accessed through the optional Context argument.

§Global State

Extension state is accessible from any command handler.

use arma_rs::{arma, Context, ContextState, Extension};

use std::sync::atomic::{AtomicU32, Ordering};

#[arma]
fn init() -> Extension {
    Extension::build()
        .command("counter_increment", increment)
        .state(AtomicU32::new(0))
        .finish()
}

pub fn increment(ctx: Context) -> Result<(), ()> {
    let Some(counter) = ctx.global().get::<AtomicU32>() else {
        return Err(());
    };
    counter.fetch_add(1, Ordering::SeqCst);
    Ok(())
}

§Group State

Command group state is only accessible from command handlers within the same group.

use arma_rs::{Context, ContextState, Extension};

use std::sync::atomic::{AtomicU32, Ordering};

pub fn increment(ctx: Context) -> Result<(), ()> {
    let Some(counter) = ctx.group().get::<AtomicU32>() else {
        return Err(());
    };
    counter.fetch_add(1, Ordering::SeqCst);
    Ok(())
}

pub fn group() -> arma_rs::Group {
    arma_rs::Group::new()
        .command("increment", increment)
        .state(AtomicU32::new(0))
}

§Custom Types

If you’re bringing your existing Rust library with your own types, you can easily define how they are converted to and from Arma.

use arma_rs::{FromArma, IntoArma, Value, FromArmaError};

pub struct MemoryReport {
    total: u64,
    free: u64,
    avail: u64,
}

impl FromArma for MemoryReport {
    fn from_arma(s: String) -> Result<Self, FromArmaError> {
        let (total, free, avail) = <(u64, u64, u64)>::from_arma(s)?;
        Ok(Self { total, free, avail })
    }
}

impl IntoArma for MemoryReport {
    fn to_arma(&self) -> Value {
        Value::Array(
            vec![self.total, self.free, self.avail]
                .into_iter()
                .map(|v| v.to_string().to_arma())
                .collect(),
        )
    }
}

§Derive

Alternatively you can derive these traits. Note that the derive and manual implementation examples slightly differ, as when deriving map like structs its represented as an hashmap rather than an array. For more information on data representation and attributes see: FromArma and IntoArma.

use arma_rs::{FromArma, IntoArma};

#[derive(FromArma, IntoArma)]
struct MemoryReport {
    #[arma(to_string)]
    total: u64,
    #[arma(to_string)]
    free: u64,
    #[arma(to_string)]
    avail: u64,
}

Deriving is currently only supported for structs, this might change in the future.

§Error Codes

By default arma-rs will only allow commands via RvExtensionArgs. Using callExtension with only a function name will return an empty string.

"my_extension" callExtension "hello:english" // returns ""
"my_extension" callExtension ["hello:english", []] // returns ["Hello", 0, 0]

This behaviour can be changed by calling .allow_no_args() when building the extension. It is recommended not to use this, and to implement error handling instead.

CodeDescription
0Success
1Command not found
2xInvalid argument count, x is received count
3xInvalid argument type, x is argument position
4Attempted to write a value larger than the buffer
9Application error, from using a Result

§Error Examples

use arma_rs::Context;

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn overflow(ctx: Context) -> String {
    "X".repeat(ctx.buffer_len() + 1)
}

pub fn should_error(error: bool) -> Result<String, String> {
  if error {
    Err(String::from("told to error"))
  } else {
    Ok(String::from("told to succeed"))
  }
}
"my_extension" callExtension ["add", [1, 2]]; // Returns ["3", 0, 0]
"my_extension" callExtension ["sub", [1, 2]]; // Returns ["", 1, 0]
"my_extension" callExtension ["add", [1, 2, 3]]; // Returns ["", 23, 0], didn't expect 3 elements
"my_extension" callExtension ["add", [1, "two"]]; // Returns ["", 31, 0], unable to parse the second argument
"my_extension" callExtension ["overflow", []]; // Returns ["", 4, 0], the return size was larger than the buffer
"my_extension" callExtension ["should_error", [true]]; // Returns ["told to error", 9, 0]
"my_extension" callExtension ["should_error", [false]]; // Returns ["told to succeed", 0, 0]

§Testing

Tests can be created utilizing the extension.call() method.

mod tests {
    #[test]
    fn hello() {
        let extension = init().testing();
        let (output, _) = extension.call("hello:english", None);
        assert_eq!(output, "hello");
    }

    #[test]
    fn welcome() {
        let extension = init().testing();
        let (output, _) =
            extension.call("welcome:english", Some(vec!["John".to_string()]));
        assert_eq!(output, "Welcome John");
    }

    #[test]
    fn sleep_1sec() {
        let extension = Extension::build()
            .group("timer", super::group())
            .finish()
            .testing();
        let (_, code) = extension.call(
            "timer:sleep",
            Some(vec!["1".to_string(), "test".to_string()]),
        );
        assert_eq!(code, 0);
        let result = extension.callback_handler(
            |name, func, data| {
                assert_eq!(name, "timer:sleep");
                assert_eq!(func, "done");
                if let Some(Value::String(s)) = data {
                    Result::Ok(s)
                } else {
                    Result::Err("Data was not a string".to_string())
                }
            },
            Duration::from_secs(2),
        );
        assert_eq!(Result::Ok("test".to_string()), result);
    }
}

§Unit Loadout Array

arma-rs includes a loadout module to assist with the handling of Arma’s Unit Loadout Array.

use arma_rs::{FromArma, loadout::{Loadout, InventoryItem, Weapon, Magazine}};

let l = r#"[[],[],[],["U_Marshal",[]],[],[],"H_Cap_headphones","G_Aviator",[],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]]"#;
let mut loadout = Loadout::from_arma(l.to_string()).unwrap();
loadout.set_secondary({
    let mut weapon = Weapon::new("launch_B_Titan_short_F".to_string());
    weapon.set_primary_magazine(Magazine::new("Titan_AT".to_string(), 1));
    weapon
});
loadout.set_primary({
    let mut weapon = Weapon::new("arifle_MXC_F".to_string());
    weapon.set_optic("optic_Holosight".to_string());
    weapon
});
let uniform = loadout.uniform_mut();
uniform.set_class("U_B_CombatUniform_mcam".to_string());
let uniform_items = uniform.items_mut().unwrap();
uniform_items.push(InventoryItem::new_item("FirstAidKit".to_string(), 3));
uniform_items.push(InventoryItem::new_magazine("30Rnd_65x39_caseless_mag".to_string(), 5, 30));

§Common Rust Libraries

arma-rs supports some common Rust libraries. You can enable their support by adding their name to the features of arma-rs.

arma-rs = { version = "1.8.0", features = ["chrono"] }

Please create an issue first if you would like to add support for a new library.

§chrono

crates.io

§chrono - Convert to Arma

NaiveDateTime and DateTime<TimeZone> will be converted to Arma’s date array. The timezone will always be converted to UTC.

§chrono - Convert From Arma

Arma’s date array can be converted to NaiveDateTime.

§uuid

crates.io

§uuid - Convert To Arma

Uuid will be converted to a string.

§serde_json

crates.io

§serde_json - Convert To Arma

Any variant of serde_json::Value will be converted to the appropriate Arma type.

§Building for x86 (32 Bit)

rustup toolchain install stable-i686-pc-windows-msvc
cargo +stable-i686-pc-windows-msvc build

§Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Re-exports§

pub use testing::Result;
pub use libc;
pub use context::*;

Modules§

context
Contextual execution information.
loadout
For working with Arma’s unit loadout array
testing
For testing your extension.

Structs§

DirectReturn
Extension
Contains all the information about your extension This is used by the generated code to interface with Arma
ExtensionBuilder
Used to build an extension.
Group
A group of commands. Called from Arma using [group]:[command].

Enums§

Caller
Identification of the player calling your extension.
FromArmaError
Error type for FromArma
Mission
Current mission.
Server
Current server.
Source
Source of the extension call.
Value
A value that can be converted to and from Arma types.

Statics§

RVExtensionFeatureFlags
Feature flags read on each callExtension call.

Traits§

FromArma
A trait for converting a value from Arma to a Rust value.
IntoArma
Convert a type to a value that can be sent into Arma
IntoExtResult
Convert a type to a successful or failed extension result

Type Aliases§

CallContext
Context of the callExtension, provided by Arma.
CallContextStackTrace
Context of the callExtension, provided by Arma, with a stack trace.
ContextRequest
Requests a call context from Arma
State
State TypeMap that can hold at most one value per type key.

Attribute Macros§

arma
Used to generate the necessary boilerplate for an Arma extension. It should be applied to a function that takes no arguments and returns an extension.

Derive Macros§

FromArma
Derive implementation of FromArma, only supports structs.
IntoArma
Derive implementation of IntoArma, only supports structs.