generated from bitsquid_dt/dt-plugin-template
Compare commits
6 commits
feat/libp2
...
master
Author | SHA1 | Date | |
---|---|---|---|
d4974d9128 | |||
bdd3f2e7d4 | |||
73873cf2ad | |||
ae58bccedf | |||
a233d3b9cb | |||
bf376bb174 |
10 changed files with 76 additions and 4949 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,3 @@
|
||||||
.wineprefix
|
|
||||||
.envrc
|
.envrc
|
||||||
/target
|
/target
|
||||||
/msvc
|
/msvc
|
||||||
|
|
3581
Cargo.lock
generated
3581
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -6,16 +6,11 @@ edition = "2024"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bincode = "2.0.1"
|
|
||||||
bstr = "1.12.0"
|
|
||||||
color-eyre = "0.6.4"
|
|
||||||
libc = "0.2.144"
|
libc = "0.2.144"
|
||||||
libp2p = { version = "0.55.0", features = ["tokio", "tcp", "noise", "yamux", "ping", "gossipsub"] }
|
|
||||||
log = { version = "0.4.27", features = ["release_max_level_info"] }
|
log = { version = "0.4.27", features = ["release_max_level_info"] }
|
||||||
tokio = { version = "1.45.0", features = ["macros", "rt", "sync"] }
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
bindgen = "0.71.0"
|
bindgen = "0.72.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "lib"]
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
2
Justfile
2
Justfile
|
@ -7,7 +7,7 @@ image_name := "dt-p2p-builder"
|
||||||
|
|
||||||
steam_library := env("steam_library")
|
steam_library := env("steam_library")
|
||||||
game_dir := steam_library / "steamapps" / "common" / "Warhammer 40,000 DARKTIDE"
|
game_dir := steam_library / "steamapps" / "common" / "Warhammer 40,000 DARKTIDE"
|
||||||
log_dir := env("APPDATA") / "Fatshark" / "Darktide" / "console_logs"
|
log_dir := env("appdata") / "Fatshark" / "Darktide" / "console_logs"
|
||||||
|
|
||||||
proton_dir := env("HOME") / ".steam" / "steam"
|
proton_dir := env("HOME") / ".steam" / "steam"
|
||||||
|
|
||||||
|
|
47
src/lib.rs
47
src/lib.rs
|
@ -1,25 +1,20 @@
|
||||||
use std::ffi::{CStr, c_char};
|
use std::ffi::{CString, c_char};
|
||||||
use std::sync::{OnceLock, RwLock};
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
mod lua;
|
mod lua;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
mod rpc;
|
|
||||||
mod stingray_sdk;
|
mod stingray_sdk;
|
||||||
|
|
||||||
use log::error;
|
|
||||||
use plugin::Plugin;
|
use plugin::Plugin;
|
||||||
use stingray_sdk::{GetApiFunction, LoggingApi, LuaApi, PluginApi, PluginApiID};
|
use stingray_sdk::{GetApiFunction, LoggingApi, LuaApi, PluginApi, PluginApiID};
|
||||||
|
|
||||||
/// The name that the plugin is registered to the engine as.
|
/// The name that the plugin is registered to the engine as.
|
||||||
/// Must be unique across all plugins.
|
/// Must be unique across all plugins.
|
||||||
pub const PLUGIN_NAME: &CStr = c"dt_p2p";
|
pub const PLUGIN_NAME: &str = "dt_p2p";
|
||||||
/// The module that Lua functions are assigned to.
|
/// The module that Lua functions are assigned to.
|
||||||
pub const MODULE_NAME: &CStr = c"dt_p2p";
|
pub const MODULE_NAME: &str = "dt_p2p";
|
||||||
|
|
||||||
const CHANNEL_BUFFER_SIZE: usize = 100;
|
static PLUGIN: OnceLock<Plugin> = OnceLock::new();
|
||||||
const EVENTS_PER_TICK: usize = 25;
|
|
||||||
|
|
||||||
static PLUGIN: OnceLock<RwLock<Plugin>> = OnceLock::new();
|
|
||||||
static LOGGER: OnceLock<LoggingApi> = OnceLock::new();
|
static LOGGER: OnceLock<LoggingApi> = OnceLock::new();
|
||||||
static LUA: OnceLock<LuaApi> = OnceLock::new();
|
static LUA: OnceLock<LuaApi> = OnceLock::new();
|
||||||
|
|
||||||
|
@ -35,22 +30,10 @@ macro_rules! global {
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A macro to make accessing the plugin instance as mutable a little more convenient.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! plugin_mut {
|
|
||||||
() => {{
|
|
||||||
let lock = $crate::PLUGIN.get().and_then(|l| l.write().ok());
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
lock.expect("failed to acquire lock on plugin global")
|
|
||||||
} else {
|
|
||||||
unsafe { lock.unwrap_unchecked() }
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "C" fn get_name() -> *const c_char {
|
pub extern "C" fn get_name() -> *const c_char {
|
||||||
PLUGIN_NAME.as_ptr()
|
let s = CString::new(PLUGIN_NAME).expect("Failed to create CString from plugin name");
|
||||||
|
s.as_ptr()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
|
@ -61,29 +44,19 @@ pub extern "C" fn setup_game(get_engine_api: GetApiFunction) {
|
||||||
|
|
||||||
let _ = LUA.get_or_init(|| LuaApi::get(get_engine_api));
|
let _ = LUA.get_or_init(|| LuaApi::get(get_engine_api));
|
||||||
|
|
||||||
let plugin = match Plugin::new() {
|
let plugin = PLUGIN.get_or_init(Plugin::new);
|
||||||
Ok(plugin) => plugin,
|
|
||||||
Err(err) => {
|
|
||||||
error!("Failed to initialize plugin:\n{:?}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
plugin.setup_game();
|
plugin.setup_game();
|
||||||
PLUGIN
|
|
||||||
.set(RwLock::new(plugin))
|
|
||||||
.expect("Failed to set global plugin instance");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "C" fn shutdown_game() {
|
pub extern "C" fn shutdown_game() {
|
||||||
let plugin = plugin_mut!();
|
let plugin = global!(PLUGIN);
|
||||||
plugin.shutdown_game();
|
plugin.shutdown_game();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "C" fn update_game(dt: f32) {
|
pub extern "C" fn update_game(dt: f32) {
|
||||||
let mut plugin = plugin_mut!();
|
let plugin = global!(PLUGIN);
|
||||||
plugin.update_game(dt);
|
plugin.update_game(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
397
src/lua.rs
397
src/lua.rs
|
@ -1,393 +1,16 @@
|
||||||
use std::collections::HashMap;
|
use crate::stingray_sdk::lua_State;
|
||||||
use std::ffi::CStr;
|
use crate::{LUA, global};
|
||||||
use std::panic::catch_unwind;
|
|
||||||
|
|
||||||
use bstr::BStr;
|
pub extern "C" fn do_something(l: *mut lua_State) -> i32 {
|
||||||
use bstr::ByteSlice as _;
|
|
||||||
use color_eyre::Result;
|
|
||||||
use color_eyre::eyre;
|
|
||||||
use color_eyre::eyre::Context as _;
|
|
||||||
|
|
||||||
use crate::plugin::Identifier;
|
|
||||||
use crate::plugin::ModCallbacks;
|
|
||||||
use crate::plugin::Plugin;
|
|
||||||
use crate::stingray_sdk::LUA_REGISTRYINDEX;
|
|
||||||
use crate::stingray_sdk::{LuaState, LuaType, lua_State};
|
|
||||||
use crate::{LUA, global, plugin_mut};
|
|
||||||
|
|
||||||
pub const NAMESPACE_SEPARATOR: char = '.';
|
|
||||||
|
|
||||||
// As a minor optimization, only `catch_unwind` in debug mode.
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
fn lua_wrapper<F>(l: *mut lua_State, f: F) -> i32
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Plugin, &LuaState) -> Result<i32> + std::panic::UnwindSafe,
|
|
||||||
{
|
|
||||||
let lua = global!(LUA);
|
let lua = global!(LUA);
|
||||||
|
|
||||||
// We need to drop as many things as possible before the `lua_error` call
|
if let Some(name) = lua.tolstring(l, 1) {
|
||||||
// will longjmp and thereby ignoring Rust's cleanup.
|
lua.pushstring(
|
||||||
{
|
l,
|
||||||
let lua = LuaState::new(l, lua);
|
format!("[do_something] Hello from Rust, {}", name.to_string_lossy()),
|
||||||
|
|
||||||
let res = catch_unwind(|| {
|
|
||||||
let mut plugin = plugin_mut!();
|
|
||||||
f(&mut plugin, &lua)
|
|
||||||
});
|
|
||||||
match res {
|
|
||||||
Ok(Ok(i)) => {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
Ok(Err(err)) => lua.pushstring(format!("{:?}", err)),
|
|
||||||
Err(err) => lua.pushstring(format!("{:?}", err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lua.error(l);
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
fn lua_wrapper<F>(l: *mut lua_State, f: F) -> i32
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Plugin, &LuaState) -> Result<i32>,
|
|
||||||
{
|
|
||||||
let lua = global!(LUA);
|
|
||||||
|
|
||||||
// We need to drop as many things as possible before the `lua_error` call
|
|
||||||
// will longjmp and thereby ignoring Rust's cleanup.
|
|
||||||
{
|
|
||||||
let lua = LuaState::new(l, lua);
|
|
||||||
|
|
||||||
let mut plugin = plugin_mut!();
|
|
||||||
match f(&mut plugin, &lua) {
|
|
||||||
Ok(Ok(i)) => {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
Ok(Err(err)) => lua.pushstring(format!("{:?}", err)),
|
|
||||||
Err(err) => lua.pushstring(format!("{:?}", err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lua.error(l);
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the namespace and name for a RPC.
|
|
||||||
///
|
|
||||||
/// The namespace may either be part of the name, separated by `NAMESPACE_SEPARATOR`
|
|
||||||
/// or omitted, in which case the mod name is used as fallback.
|
|
||||||
fn get_identifier<'a>(lua: &'a LuaState) -> Result<Identifier<'a>> {
|
|
||||||
lua.getfield(1, c"name");
|
|
||||||
if !lua.isstring(-1) {
|
|
||||||
eyre::bail!("bad argument #1, not a mod object");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mod_name = lua.tostring(-1);
|
|
||||||
let name = lua.tostring(2);
|
|
||||||
|
|
||||||
let (namespace, name) = name
|
|
||||||
.split_once_str(&[NAMESPACE_SEPARATOR as u8])
|
|
||||||
.map(|(namespace, name)| (BStr::new(namespace), BStr::new(name)))
|
|
||||||
.unwrap_or((mod_name, name));
|
|
||||||
|
|
||||||
Ok(Identifier::new(namespace, name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called with `(mod: table, callbacks: table)`
|
|
||||||
pub extern "C" fn register_mod(l: *mut lua_State) -> i32 {
|
|
||||||
lua_wrapper(l, |plugin, lua| {
|
|
||||||
if !lua.istable(1) {
|
|
||||||
eyre::bail!("bad argument #1, expected a mod object");
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = {
|
|
||||||
lua.getfield(1, c"name");
|
|
||||||
if !lua.isstring(-1) {
|
|
||||||
eyre::bail!("bad argument #1, not a mod object");
|
|
||||||
}
|
|
||||||
|
|
||||||
lua.tostring(-1)
|
|
||||||
};
|
|
||||||
|
|
||||||
let callbacks = match lua.r#type(2) {
|
|
||||||
LuaType::Nil => ModCallbacks::default(),
|
|
||||||
LuaType::Table => {
|
|
||||||
let get_callback = |name: &CStr| {
|
|
||||||
lua.getfield(2, name);
|
|
||||||
|
|
||||||
match lua.r#type(-1) {
|
|
||||||
LuaType::Nil => Ok(None),
|
|
||||||
LuaType::Function => Ok(Some(lua.lib_ref(LUA_REGISTRYINDEX))),
|
|
||||||
x => {
|
|
||||||
eyre::bail!(
|
|
||||||
"bad argument #2, expected a function for field {}, got {}",
|
|
||||||
name.to_string_lossy(),
|
|
||||||
x
|
|
||||||
);
|
);
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
ModCallbacks {
|
|
||||||
on_session_joined: get_callback(c"on_session_joined")?,
|
|
||||||
on_session_left: get_callback(c"on_session_left")?,
|
|
||||||
on_user_joined: get_callback(c"on_user_joined")?,
|
|
||||||
on_user_left: get_callback(c"on_user_left")?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
x => {
|
|
||||||
eyre::bail!("bad argument #2, expected a table got {}", x);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
plugin.register_mod(name, callbacks);
|
|
||||||
|
|
||||||
// TODO: Register unload and disable handlers?
|
|
||||||
// Will likely have to monkey-patch the mod object.
|
|
||||||
// DMF doesn't expose a good way to hook into it, and it wouldn't be feasible to expect a
|
|
||||||
// DMF update to allow it.
|
|
||||||
|
|
||||||
Ok(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called with `(mod: table, name: string, opts: table)`
|
|
||||||
pub extern "C" fn create_rpc(l: *mut lua_State) -> i32 {
|
|
||||||
lua_wrapper(l, |plugin, lua| {
|
|
||||||
lua.getfield(1, c"version");
|
|
||||||
if !lua.istable(1) {
|
|
||||||
eyre::bail!("bad argument #1, expected a mod object");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !lua.isstring(2) {
|
|
||||||
eyre::bail!("bad argument #2, expected a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !lua.istable(3) {
|
|
||||||
eyre::bail!("bad argument #3, expected a table");
|
|
||||||
}
|
|
||||||
|
|
||||||
let _version = match lua.r#type(-1) {
|
|
||||||
LuaType::Nil => {
|
|
||||||
eyre::bail!("bad argument #1, mod object doesn't provide a 'version' field");
|
|
||||||
}
|
|
||||||
LuaType::Number => lua.tostring(-1),
|
|
||||||
LuaType::String => {
|
|
||||||
let version = lua.tostring(-1);
|
|
||||||
// TODO: parse as date or semver-like
|
|
||||||
version
|
|
||||||
}
|
|
||||||
x => {
|
|
||||||
eyre::bail!(
|
|
||||||
"invalid type {} for 'version' field, must be number or a version string",
|
|
||||||
x
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let id = get_identifier(lua).wrap_err("failed to determine RPC name")?;
|
|
||||||
plugin.create_rpc(id);
|
|
||||||
|
|
||||||
Ok(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type LuaMap<'a> = HashMap<LuaKey<'a>, LuaValue<'a>>;
|
|
||||||
|
|
||||||
#[derive(bincode::BorrowDecode, bincode::Encode, PartialEq)]
|
|
||||||
pub enum LuaKey<'a> {
|
|
||||||
String(&'a [u8]),
|
|
||||||
Number(f64),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for LuaKey<'_> {}
|
|
||||||
|
|
||||||
impl std::hash::Hash for LuaKey<'_> {
|
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
match self {
|
|
||||||
LuaKey::String(s) => s.hash(state),
|
|
||||||
LuaKey::Number(n) => (*n as u128).hash(state),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(bincode::BorrowDecode, bincode::Encode)]
|
|
||||||
pub enum LuaValue<'a> {
|
|
||||||
Nil,
|
|
||||||
Boolean(bool),
|
|
||||||
Number(f64),
|
|
||||||
String(&'a [u8]),
|
|
||||||
Table(LuaMap<'a>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> LuaValue<'a> {
|
|
||||||
fn parse_table(lua: &'a LuaState, idx: i32) -> Result<Self> {
|
|
||||||
let mut values = LuaMap::default();
|
|
||||||
|
|
||||||
// `nil` as initial key tells `lua_next` to pick the first key.
|
|
||||||
lua.pushnil();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if lua.next(idx) <= 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = match lua.r#type(-2) {
|
|
||||||
LuaType::Number => LuaKey::Number(lua.tonumber(-2)),
|
|
||||||
LuaType::String => LuaKey::String(lua.tostring(-2)),
|
|
||||||
t => eyre::bail!("Unsupported type {} for args table key", t),
|
|
||||||
};
|
|
||||||
|
|
||||||
let value = Self::get_from_stack(lua, -1)?;
|
|
||||||
|
|
||||||
values.insert(key, value);
|
|
||||||
|
|
||||||
// Pop the value, but keep the key so `lua_next` knows where to continue
|
|
||||||
lua.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop the last key
|
|
||||||
lua.pop();
|
|
||||||
|
|
||||||
Ok(LuaValue::Table(values))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the value from the stack at `idx`
|
|
||||||
pub fn get_from_stack(lua: &'a LuaState, idx: i32) -> Result<Self> {
|
|
||||||
match lua.r#type(idx) {
|
|
||||||
LuaType::None | LuaType::Nil => Ok(LuaValue::Nil),
|
|
||||||
LuaType::Boolean => Ok(LuaValue::Boolean(lua.toboolean(-1))),
|
|
||||||
LuaType::Number => Ok(LuaValue::Number(lua.tonumber(-1))),
|
|
||||||
LuaType::String => Ok(LuaValue::String(lua.tostring(-1))),
|
|
||||||
LuaType::Table => Self::parse_table(lua, -1),
|
|
||||||
x => eyre::bail!("Unknown or unsupported Lua type {}", x),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pushes the value onto a Lua stack
|
|
||||||
pub fn push_to_stack(&self, lua: &LuaState) {
|
|
||||||
match self {
|
|
||||||
LuaValue::Nil => lua.pushnil(),
|
|
||||||
LuaValue::Boolean(bool) => lua.pushboolean(*bool),
|
|
||||||
LuaValue::Number(num) => lua.pushnumber(*num),
|
|
||||||
LuaValue::String(s) => lua.pushstring(*s),
|
|
||||||
LuaValue::Table(table) => Self::push_table(lua, table),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_table(lua: &LuaState, table: &LuaMap) {
|
|
||||||
let (narr, nrec) = table.iter().fold((0, 0), |(narr, nrec), (k, _)| match k {
|
|
||||||
LuaKey::String(_) => (narr, nrec + 1),
|
|
||||||
LuaKey::Number(_) => (narr + 1, nrec),
|
|
||||||
});
|
|
||||||
|
|
||||||
lua.createtable(narr, nrec);
|
|
||||||
let tbl_index = lua.gettop();
|
|
||||||
|
|
||||||
for (k, v) in table.iter() {
|
|
||||||
match k {
|
|
||||||
LuaKey::String(s) => lua.pushstring(*s),
|
|
||||||
LuaKey::Number(num) => lua.pushnumber(*num),
|
|
||||||
}
|
|
||||||
|
|
||||||
v.push_to_stack(lua);
|
|
||||||
|
|
||||||
lua.settable(tbl_index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called with `(mod: table, name: string, args: table)`
|
|
||||||
pub extern "C" fn send_rpc(l: *mut lua_State) -> i32 {
|
|
||||||
lua_wrapper(l, |plugin, lua| {
|
|
||||||
if !lua.istable(1) {
|
|
||||||
eyre::bail!("bad argument #1, expected a mod object");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !lua.isstring(2) {
|
|
||||||
eyre::bail!("bad argument #2, expected a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !lua.istable(3) {
|
|
||||||
eyre::bail!("bad argument #3, expected a table");
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = get_identifier(lua).wrap_err("failed to determine RPC name")?;
|
|
||||||
let args = LuaValue::parse_table(lua, 3).wrap_err("Failed to parse args table")?;
|
|
||||||
|
|
||||||
plugin.send_rpc(id, args).wrap_err("Failed to send RPC")?;
|
|
||||||
|
|
||||||
Ok(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called with `(mod: table, name: string, callback: Fn(args))`
|
|
||||||
pub extern "C" fn receive_rpc(l: *mut lua_State) -> i32 {
|
|
||||||
lua_wrapper(l, |plugin, lua| {
|
|
||||||
if !lua.istable(1) {
|
|
||||||
eyre::bail!("bad argument #1, expected a mod object");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !lua.isstring(2) {
|
|
||||||
eyre::bail!("bad argument #2, expected a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !lua.isfunction(3) {
|
|
||||||
eyre::bail!("bad argument #3, expected a function");
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = get_identifier(lua).wrap_err("failed to determine RPC name")?;
|
|
||||||
|
|
||||||
// We will utilize Lua's _references_ to store the callback functions. Since we're doing
|
|
||||||
// the bookkeeping on the Rust side, we don't need to worry about any structured layout on
|
|
||||||
// the Lua side.
|
|
||||||
|
|
||||||
lua.pushvalue(3);
|
|
||||||
let fn_ref = lua.lib_ref(LUA_REGISTRYINDEX);
|
|
||||||
|
|
||||||
plugin
|
|
||||||
.add_rpc_listener(id, fn_ref)
|
|
||||||
.wrap_err("Failed to add RPC listener")?;
|
|
||||||
|
|
||||||
Ok(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called with `(server_name: string, peer_id: string, player_id: string)`
|
|
||||||
pub extern "C" fn join_session(l: *mut lua_State) -> i32 {
|
|
||||||
lua_wrapper(l, |plugin, lua| {
|
|
||||||
if !lua.isstring(1) {
|
|
||||||
eyre::bail!("bad argument #1, expected a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !lua.isstring(2) {
|
|
||||||
eyre::bail!("bad argument #2, expected a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !lua.isstring(3) {
|
|
||||||
eyre::bail!("bad argument #3, expected a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
let server_name = lua.tostring(1);
|
|
||||||
let peer_id = lua.tostring(2);
|
|
||||||
let player_id = lua.tostring(3);
|
|
||||||
|
|
||||||
plugin
|
|
||||||
.join_session(lua, server_name, peer_id, player_id)
|
|
||||||
.wrap_err("Failed to join session")?;
|
|
||||||
|
|
||||||
Ok(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called with `()`
|
|
||||||
pub extern "C" fn leave_session(l: *mut lua_State) -> i32 {
|
|
||||||
lua_wrapper(l, |plugin, lua| {
|
|
||||||
plugin
|
|
||||||
.leave_session(lua)
|
|
||||||
.wrap_err("Failed to leave session")?;
|
|
||||||
|
|
||||||
Ok(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
671
src/plugin.rs
671
src/plugin.rs
|
@ -1,671 +1,26 @@
|
||||||
use std::collections::HashMap;
|
use log::info;
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher as _};
|
|
||||||
use std::mem;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::thread;
|
|
||||||
use std::thread::JoinHandle;
|
|
||||||
|
|
||||||
use bstr::{BStr, BString};
|
use crate::{LUA, MODULE_NAME, PLUGIN_NAME, global, lua};
|
||||||
use color_eyre::Result;
|
|
||||||
use color_eyre::eyre;
|
|
||||||
use color_eyre::eyre::Context as _;
|
|
||||||
use libp2p::PeerId;
|
|
||||||
use libp2p::futures::StreamExt;
|
|
||||||
use libp2p::gossipsub;
|
|
||||||
use libp2p::gossipsub::{
|
|
||||||
AllowAllSubscriptionFilter, Event, IdentTopic, IdentityTransform, Message, TopicHash,
|
|
||||||
};
|
|
||||||
use libp2p::noise;
|
|
||||||
use libp2p::swarm::SwarmEvent;
|
|
||||||
use libp2p::tcp;
|
|
||||||
use libp2p::yamux;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio::sync::mpsc::Receiver;
|
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
|
||||||
use tokio::sync::mpsc::error::TryRecvError;
|
|
||||||
|
|
||||||
use crate::lua;
|
pub(crate) struct Plugin {}
|
||||||
use crate::lua::{LuaValue, NAMESPACE_SEPARATOR};
|
|
||||||
use crate::rpc::RPC;
|
|
||||||
use crate::stingray_sdk::{LUA_REGISTRYINDEX, LuaRef, LuaState};
|
|
||||||
use crate::{CHANNEL_BUFFER_SIZE, EVENTS_PER_TICK};
|
|
||||||
use crate::{LUA, MODULE_NAME, global};
|
|
||||||
|
|
||||||
type ModName = BString;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct Identifier<'a> {
|
|
||||||
name: &'a BStr,
|
|
||||||
namespace: &'a BStr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Identifier<'a> {
|
|
||||||
pub fn new(namespace: &'a BStr, name: &'a BStr) -> Self {
|
|
||||||
Self { namespace, name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Identifier<'_>> for String {
|
|
||||||
fn from(value: Identifier<'_>) -> Self {
|
|
||||||
format!("{}", value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Identifier<'_> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}{}{}", self.namespace, NAMESPACE_SEPARATOR, self.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, bincode::Encode, bincode::Decode)]
|
|
||||||
// To avoid having to implement `bincode` for `bstr` types, we use raw `Vec<u8>` here,
|
|
||||||
// which can be turned into `BString` cheaply.
|
|
||||||
pub(crate) enum SwarmMessage {
|
|
||||||
// To avoid extra cloning, or having to deal with lifetimes,
|
|
||||||
// this message type contains the Lua args already encoded.
|
|
||||||
Rpc(Vec<u8>),
|
|
||||||
SessionJoin {
|
|
||||||
/// The in-game player ID for this peer
|
|
||||||
player_id: Vec<u8>,
|
|
||||||
/// A list of mod names this peer has enabled
|
|
||||||
mods: Vec<Vec<u8>>,
|
|
||||||
},
|
|
||||||
SessionLeave,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) enum SwarmTask {
|
|
||||||
Message { topic: Topic, msg: SwarmMessage },
|
|
||||||
Subscribe(Topic),
|
|
||||||
Unsubscribe(Vec<Topic>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct Topic(IdentTopic);
|
|
||||||
|
|
||||||
impl Eq for Topic {}
|
|
||||||
impl PartialEq for Topic {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.0.hash() == other.0.hash()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hash for Topic {
|
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
self.0.hash().hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Topic {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for Topic {
|
|
||||||
type Target = IdentTopic;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Topic> for TopicHash {
|
|
||||||
fn from(value: Topic) -> Self {
|
|
||||||
Self::from(value.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Player {
|
|
||||||
id: BString,
|
|
||||||
mods: Vec<ModName>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub(crate) struct ModCallbacks {
|
|
||||||
pub on_session_joined: Option<LuaRef>,
|
|
||||||
pub on_session_left: Option<LuaRef>,
|
|
||||||
pub on_user_joined: Option<LuaRef>,
|
|
||||||
pub on_user_left: Option<LuaRef>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Session {
|
|
||||||
online: bool,
|
|
||||||
address: String,
|
|
||||||
peer_id: BString,
|
|
||||||
topics: HashMap<TopicHash, Topic>,
|
|
||||||
peers: HashMap<PeerId, Player>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Session {
|
|
||||||
pub fn new_topic(&mut self, topic: impl Into<String>) -> &Topic {
|
|
||||||
self.topics
|
|
||||||
.entry(TopicHash::from_raw(topic.into()))
|
|
||||||
.or_insert_with_key(|topic| {
|
|
||||||
Topic(IdentTopic::new(format!("{}/{}", self.address, topic)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_topic(&self, topic: impl Into<String>) -> Option<&Topic> {
|
|
||||||
let hash = TopicHash::from_raw(topic.into());
|
|
||||||
self.topics.get(&hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct Plugin {
|
|
||||||
swarm_thread: JoinHandle<Result<()>>,
|
|
||||||
event_rx: Receiver<SwarmEvent<Event>>,
|
|
||||||
send_tx: UnboundedSender<SwarmTask>,
|
|
||||||
session: Session,
|
|
||||||
rpcs: HashMap<Topic, RPC>,
|
|
||||||
mods: HashMap<ModName, ModCallbacks>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Plugin {
|
impl Plugin {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Self {
|
||||||
let mut swarm = libp2p::SwarmBuilder::with_new_identity()
|
Self {}
|
||||||
.with_tokio()
|
|
||||||
.with_tcp(
|
|
||||||
tcp::Config::default(),
|
|
||||||
noise::Config::new,
|
|
||||||
yamux::Config::default,
|
|
||||||
)
|
|
||||||
.wrap_err("Failed to configure transport")?
|
|
||||||
.with_behaviour(|key| {
|
|
||||||
let message_id_fn = |msg: &gossipsub::Message| {
|
|
||||||
let mut s = DefaultHasher::new();
|
|
||||||
msg.data.hash(&mut s);
|
|
||||||
gossipsub::MessageId::from(s.finish().to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = gossipsub::ConfigBuilder::default()
|
|
||||||
.validation_mode(gossipsub::ValidationMode::Strict)
|
|
||||||
.message_id_fn(message_id_fn)
|
|
||||||
.build()
|
|
||||||
.wrap_err("Failed to create gossipsub config")?;
|
|
||||||
|
|
||||||
// TODO: Benchmark if compression would be beneficial.
|
|
||||||
let behavior: gossipsub::Behaviour<IdentityTransform, AllowAllSubscriptionFilter> =
|
|
||||||
gossipsub::Behaviour::new(
|
|
||||||
gossipsub::MessageAuthenticity::Signed(key.clone()),
|
|
||||||
config,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(behavior)
|
|
||||||
})
|
|
||||||
.wrap_err("Failed to configure behavior")?
|
|
||||||
.build();
|
|
||||||
|
|
||||||
swarm
|
|
||||||
.listen_on(
|
|
||||||
"/ipv4/0.0.0.0/tcp/0"
|
|
||||||
.parse()
|
|
||||||
.expect("listen address invalid"),
|
|
||||||
)
|
|
||||||
.wrap_err("failed to listen on IPv4 socket")?;
|
|
||||||
|
|
||||||
// TODO: Connect to first hard coded relay server(s), which should then
|
|
||||||
// fan out to online peers.
|
|
||||||
// Maybe a cheap server (e.g. AWS free tier instance) is already enough to handle all
|
|
||||||
// session negotiations. If not, then a system is needed where each mod user also becomes
|
|
||||||
// available for that, and the hosted server merely acts as load balancer between them.
|
|
||||||
//
|
|
||||||
// swarm.dial(RELAY_SERVER)
|
|
||||||
|
|
||||||
let (event_tx, event_rx) = mpsc::channel::<SwarmEvent<Event>>(CHANNEL_BUFFER_SIZE);
|
|
||||||
// Since the `Sender` will be on the sync side, and we want to avoid blocking, we make this
|
|
||||||
// unbounded to limit the chance for it to actually block.
|
|
||||||
let (send_tx, send_rx) = mpsc::unbounded_channel::<SwarmTask>();
|
|
||||||
|
|
||||||
let swarm_thread = thread::Builder::new()
|
|
||||||
.name("p2p-swarm".into())
|
|
||||||
.spawn(move || {
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.build()
|
|
||||||
.wrap_err("Failed to create tokio runtime")?;
|
|
||||||
|
|
||||||
let mut swarm = swarm;
|
|
||||||
let mut send_rx = send_rx;
|
|
||||||
|
|
||||||
rt.block_on(async move {
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
event = swarm.select_next_some() => {
|
|
||||||
event_tx.send(event).await.wrap_err("Failed to queue event")?;
|
|
||||||
}
|
|
||||||
task = send_rx.recv() => {
|
|
||||||
let Some(task) = task else {
|
|
||||||
eyre::bail!("Task queue closed prematurely");
|
|
||||||
};
|
|
||||||
|
|
||||||
let behavior = swarm.behaviour_mut();
|
|
||||||
|
|
||||||
match task{
|
|
||||||
SwarmTask::Message { topic, msg } => {
|
|
||||||
let data = bincode::encode_to_vec(msg, bincode::config::standard())
|
|
||||||
.wrap_err("Failed to encode swarm message")?;
|
|
||||||
behavior
|
|
||||||
.publish(topic, data)
|
|
||||||
.wrap_err("Failed to send message")?;
|
|
||||||
},
|
|
||||||
SwarmTask::Subscribe(topic) => {
|
|
||||||
behavior
|
|
||||||
.subscribe(&topic)
|
|
||||||
.wrap_err_with(|| format!("Failed to subscribe to topic {}", topic))?;
|
|
||||||
},
|
|
||||||
SwarmTask::Unsubscribe(topics) => {
|
|
||||||
for topic in topics {
|
|
||||||
behavior.unsubscribe(&topic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.wrap_err("Failed to create p2p-swarm thread")?;
|
|
||||||
|
|
||||||
log::info!("Plugin initialized");
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
swarm_thread,
|
|
||||||
event_rx,
|
|
||||||
send_tx,
|
|
||||||
session: Session {
|
|
||||||
online: false,
|
|
||||||
address: Default::default(),
|
|
||||||
peer_id: Default::default(),
|
|
||||||
topics: Default::default(),
|
|
||||||
peers: Default::default(),
|
|
||||||
},
|
|
||||||
rpcs: Default::default(),
|
|
||||||
mods: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_game(&self) {
|
pub fn setup_game(&self) {
|
||||||
|
info!("[setup_game] Hello, world! This is {}!", PLUGIN_NAME);
|
||||||
|
|
||||||
let lua = global!(LUA);
|
let lua = global!(LUA);
|
||||||
// Internal API
|
lua.add_module_function(MODULE_NAME, "do_something", lua::do_something);
|
||||||
// TODO: Maybe useful to move these to a separate module, e.g. `P2PInternal`?
|
|
||||||
lua.add_module_function(MODULE_NAME, c"join_session", lua::join_session);
|
|
||||||
lua.add_module_function(MODULE_NAME, c"leave_session", lua::leave_session);
|
|
||||||
|
|
||||||
// User-facing API
|
|
||||||
lua.add_module_function(MODULE_NAME, c"register_mod", lua::register_mod);
|
|
||||||
lua.add_module_function(MODULE_NAME, c"create_rpc", lua::create_rpc);
|
|
||||||
lua.add_module_function(MODULE_NAME, c"send_rpc", lua::send_rpc);
|
|
||||||
lua.add_module_function(MODULE_NAME, c"receive_rpc", lua::receive_rpc);
|
|
||||||
|
|
||||||
log::debug!("Lua functions registered");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shutdown_game(&self) {
|
pub fn shutdown_game(&self) {
|
||||||
// TODO: Find a way to send a close command, and make it blocking.
|
info!("[shutdown_game] Goodbye, world!");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_game(&mut self, dt: f32) {
|
pub fn update_game(&self, _dt: f32) {}
|
||||||
if let Err(err) = self.update(dt) {
|
|
||||||
log::error!("{:?}", err);
|
|
||||||
// TODO: Exit application or find a better way to report the error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self, _dt: f32) -> Result<()> {
|
|
||||||
if self.swarm_thread.is_finished() {
|
|
||||||
eyre::bail!("p2p-swarm thread terminated prematurely",);
|
|
||||||
// TODO: Move `self.swarm_thread` into a data structure that I can move it out of here,
|
|
||||||
// so that I can call `.join` and get the error it ended with.
|
|
||||||
}
|
|
||||||
|
|
||||||
let lua = {
|
|
||||||
let lua = global!(LUA);
|
|
||||||
let l = lua.getscriptenvironmentstate();
|
|
||||||
LuaState::new(l, lua)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Limit the amount of events we handle per tick to avoid potential large spikes in
|
|
||||||
// frame time.
|
|
||||||
// TODO: Add a system to monitor the amount of incoming vs processed events, and add
|
|
||||||
// warnings when this cannot catch up.
|
|
||||||
// TODO: Check if it might become necessary to create a more elaborate system that can
|
|
||||||
// change this limit dynamically based on the amount of incoming messages.
|
|
||||||
// This must be able to eventually catch up.
|
|
||||||
for _ in 0..EVENTS_PER_TICK {
|
|
||||||
let event = match self.event_rx.try_recv() {
|
|
||||||
Ok(event) => event,
|
|
||||||
Err(TryRecvError::Empty) => break,
|
|
||||||
Err(TryRecvError::Disconnected) => {
|
|
||||||
eyre::bail!("p2p-swarm channel disconnected prematurely");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match event {
|
|
||||||
SwarmEvent::Behaviour(Event::Message {
|
|
||||||
propagation_source,
|
|
||||||
message_id,
|
|
||||||
message:
|
|
||||||
Message {
|
|
||||||
topic,
|
|
||||||
data,
|
|
||||||
source,
|
|
||||||
sequence_number: _,
|
|
||||||
},
|
|
||||||
}) => {
|
|
||||||
log::debug!(
|
|
||||||
"Received message {message_id} from {propagation_source} for topic {}",
|
|
||||||
topic
|
|
||||||
);
|
|
||||||
|
|
||||||
let Some(topic) = self.session.topics.get(&topic) else {
|
|
||||||
log::warn!("Received message for unknown topic {}", topic);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok((msg, _)) = bincode::borrow_decode_from_slice::<SwarmMessage, _>(
|
|
||||||
&data,
|
|
||||||
bincode::config::standard(),
|
|
||||||
) else {
|
|
||||||
log::error!(
|
|
||||||
"Failed to decode data for message {message_id} on topic {}",
|
|
||||||
topic
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
match msg {
|
|
||||||
SwarmMessage::Rpc(rpc_args) => {
|
|
||||||
let Some(rpc) = self.rpcs.get(topic) else {
|
|
||||||
log::warn!("Topic {} does not have an RPC", topic);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok((args, _)) = bincode::borrow_decode_from_slice::<LuaValue<'_>, _>(
|
|
||||||
&rpc_args,
|
|
||||||
bincode::config::standard(),
|
|
||||||
) else {
|
|
||||||
log::error!(
|
|
||||||
"Failed to decode data for message {message_id} on topic {}",
|
|
||||||
topic
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
args.push_to_stack(&lua);
|
|
||||||
let args_index = lua.gettop();
|
|
||||||
|
|
||||||
for fn_ref in rpc.listeners() {
|
|
||||||
lua.rawgeti(LUA_REGISTRYINDEX, *fn_ref);
|
|
||||||
lua.pushvalue(args_index);
|
|
||||||
lua.pcall(1, 0).wrap_err_with(|| {
|
|
||||||
format!("Failed to call listener for RPC '{}'", topic)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SwarmMessage::SessionJoin { player_id, mods } => match source {
|
|
||||||
Some(peer_id) => {
|
|
||||||
let id = BString::new(player_id);
|
|
||||||
let mods = mods.into_iter().map(ModName::new).collect::<Vec<_>>();
|
|
||||||
let peer = Player { id, mods };
|
|
||||||
|
|
||||||
// TODO: Check if a corresponding player is in the lobby
|
|
||||||
|
|
||||||
{
|
|
||||||
let callbacks = self
|
|
||||||
.mods
|
|
||||||
.iter()
|
|
||||||
.filter(|(name, _)| peer.mods.contains(name))
|
|
||||||
.filter_map(|(_, cbs)| cbs.on_user_joined);
|
|
||||||
|
|
||||||
lua.pushstring(peer.id.clone());
|
|
||||||
let arg_index = lua.gettop();
|
|
||||||
|
|
||||||
for fn_ref in callbacks {
|
|
||||||
lua.pushnumber(fn_ref as f64);
|
|
||||||
lua.gettable(LUA_REGISTRYINDEX);
|
|
||||||
lua.pushvalue(arg_index);
|
|
||||||
lua.pcall(1, 0)
|
|
||||||
.wrap_err("on_user_joined handler failed")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.session.peers.insert(peer_id, peer);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
log::warn!(
|
|
||||||
"Got SessionJoin event without a source peer_id. Don't know how to handle."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SwarmMessage::SessionLeave => match source {
|
|
||||||
Some(peer_id) => {
|
|
||||||
if let Some(peer) = self.session.peers.remove(&peer_id) {
|
|
||||||
let callbacks = self
|
|
||||||
.mods
|
|
||||||
.iter()
|
|
||||||
.filter(|(name, _)| peer.mods.contains(name))
|
|
||||||
.filter_map(|(_, cbs)| cbs.on_user_left);
|
|
||||||
|
|
||||||
lua.pushstring(peer.id);
|
|
||||||
let arg_index = lua.gettop();
|
|
||||||
|
|
||||||
for fn_ref in callbacks {
|
|
||||||
lua.pushnumber(fn_ref as f64);
|
|
||||||
lua.gettable(LUA_REGISTRYINDEX);
|
|
||||||
lua.pushvalue(arg_index);
|
|
||||||
lua.pcall(1, 0).wrap_err("on_user_left handler failed")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
log::warn!(
|
|
||||||
"Got SessionLeave event without a source peer_id. Don't know how to handle."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SwarmEvent::Behaviour(Event::GossipsubNotSupported { peer_id }) => {
|
|
||||||
log::warn!(
|
|
||||||
"Peer {peer_id} does not support Gossipsub. Are they really a dt-p2p user?"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
SwarmEvent::ConnectionEstablished {
|
|
||||||
peer_id,
|
|
||||||
connection_id,
|
|
||||||
endpoint: _,
|
|
||||||
num_established,
|
|
||||||
concurrent_dial_errors: _,
|
|
||||||
established_in,
|
|
||||||
} => {
|
|
||||||
log::info!(
|
|
||||||
"Connection {} established with {} in {} ms. Total: {}",
|
|
||||||
connection_id,
|
|
||||||
peer_id,
|
|
||||||
established_in.as_millis(),
|
|
||||||
num_established
|
|
||||||
);
|
|
||||||
// TODO: Start a timeout to wait for a `SwarmMessage::SessionJoin` to show a
|
|
||||||
// warning when a client connected but didn't join the session.
|
|
||||||
// Not sure if that should ever happen.
|
|
||||||
}
|
|
||||||
SwarmEvent::ConnectionClosed {
|
|
||||||
peer_id,
|
|
||||||
connection_id,
|
|
||||||
endpoint: _,
|
|
||||||
num_established,
|
|
||||||
cause,
|
|
||||||
} => {
|
|
||||||
log::info!(
|
|
||||||
"Connection {} with {} closed for reason {:?}. Total: {}",
|
|
||||||
connection_id,
|
|
||||||
peer_id,
|
|
||||||
cause,
|
|
||||||
num_established
|
|
||||||
);
|
|
||||||
if self.session.peers.contains_key(&peer_id) {
|
|
||||||
log::warn!("Peer dropped connection without properly leaving session!");
|
|
||||||
// TODO: Start a timeout and if the peer doesn't come back, remove the peer
|
|
||||||
// and trigger "user left" callbacks.
|
|
||||||
// TODO: Maybe also check if a corresponding player is still in the game
|
|
||||||
// lobby.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SwarmEvent::IncomingConnectionError {
|
|
||||||
connection_id,
|
|
||||||
local_addr: _,
|
|
||||||
send_back_addr: _,
|
|
||||||
error,
|
|
||||||
} => {
|
|
||||||
log::error!("Error on incoming connection {connection_id}: {error}");
|
|
||||||
}
|
|
||||||
SwarmEvent::OutgoingConnectionError {
|
|
||||||
connection_id,
|
|
||||||
peer_id: _,
|
|
||||||
error,
|
|
||||||
} => {
|
|
||||||
log::error!("Error on outgoing connection {connection_id}: {error}");
|
|
||||||
}
|
|
||||||
SwarmEvent::ListenerError { listener_id, error } => {
|
|
||||||
log::error!("Listener {listener_id} failed: {error}");
|
|
||||||
}
|
|
||||||
SwarmEvent::NewListenAddr {
|
|
||||||
listener_id,
|
|
||||||
address,
|
|
||||||
} => {
|
|
||||||
log::debug!("Listening on {address} with ID {listener_id}");
|
|
||||||
}
|
|
||||||
// TODO: Maybe add tracing information for the events we don't handle
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn register_mod(&mut self, name: &BStr, callbacks: ModCallbacks) {
|
|
||||||
self.mods.insert(name.to_owned(), callbacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn create_rpc(&mut self, id: Identifier) {
|
|
||||||
let rpc = RPC::new();
|
|
||||||
let topic = self.session.new_topic(id);
|
|
||||||
self.rpcs.insert(topic.clone(), rpc);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn send_rpc(&self, id: Identifier, args: LuaValue<'_>) -> Result<()> {
|
|
||||||
let Some(topic) = self.session.get_topic(id) else {
|
|
||||||
eyre::bail!("Topic for this RPC does not exist");
|
|
||||||
};
|
|
||||||
|
|
||||||
if !self.rpcs.contains_key(topic) {
|
|
||||||
eyre::bail!("RPC '{}' does not exist", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = bincode::encode_to_vec(args, bincode::config::standard())
|
|
||||||
.wrap_err("Failed to encode RPC args")?;
|
|
||||||
|
|
||||||
// TODO: Add metrics/profiling for size of the encoded args
|
|
||||||
|
|
||||||
let msg = SwarmTask::Message {
|
|
||||||
topic: topic.clone(),
|
|
||||||
msg: SwarmMessage::Rpc(args),
|
|
||||||
};
|
|
||||||
self.send_tx
|
|
||||||
.send(msg)
|
|
||||||
.wrap_err("Failed to queue RPC call")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add_rpc_listener(&mut self, id: Identifier, fn_ref: LuaRef) -> Result<()> {
|
|
||||||
let topic = self.session.new_topic(id);
|
|
||||||
let Some(rpc) = self.rpcs.get_mut(topic) else {
|
|
||||||
eyre::bail!("RPC '{}' does not exist", id);
|
|
||||||
};
|
|
||||||
|
|
||||||
rpc.add_listener(fn_ref);
|
|
||||||
self.send_tx
|
|
||||||
.send(SwarmTask::Subscribe(topic.clone()))
|
|
||||||
.wrap_err("Failed to subscribe to RPC topic")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn join_session(
|
|
||||||
&mut self,
|
|
||||||
lua: &LuaState,
|
|
||||||
server_name: &BStr,
|
|
||||||
peer_id: &BStr,
|
|
||||||
player_id: &BStr,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.session.peer_id = peer_id.to_owned();
|
|
||||||
self.session.address = format!("/{:08X}", hash(server_name));
|
|
||||||
|
|
||||||
let session_topic = self.session.new_topic("session").clone();
|
|
||||||
|
|
||||||
self.send_tx
|
|
||||||
.send(SwarmTask::Subscribe(session_topic.clone()))
|
|
||||||
.wrap_err("Failed to subscribe to session topic")?;
|
|
||||||
|
|
||||||
// TODO: Is there a way to do this without the intermediate `Vec`?
|
|
||||||
// Probably not, since we cannot keep an immutable reference on `self.rpcs`, while
|
|
||||||
// also trying to get a mutable reference on `self` for `self.subscribe`.
|
|
||||||
let topics = self.rpcs.keys().cloned().collect::<Vec<_>>();
|
|
||||||
for topic in topics {
|
|
||||||
self.send_tx
|
|
||||||
.send(SwarmTask::Subscribe(topic.clone()))
|
|
||||||
.wrap_err_with(|| format!("Failed to subscribe to {}", topic))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.session.online = true;
|
|
||||||
|
|
||||||
let msg = SwarmTask::Message {
|
|
||||||
topic: session_topic,
|
|
||||||
msg: SwarmMessage::SessionJoin {
|
|
||||||
player_id: player_id.to_vec(),
|
|
||||||
mods: self.mods.keys().map(|name| name.to_vec()).collect(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
self.send_tx
|
|
||||||
.send(msg)
|
|
||||||
.wrap_err("Failed to queue RPC call")?;
|
|
||||||
|
|
||||||
// TODO: Do I want to wait for a reply or some sort of confirmation?
|
|
||||||
// Currently, we just assume this works always.
|
|
||||||
|
|
||||||
for fn_ref in self.mods.values().filter_map(|cbs| cbs.on_session_joined) {
|
|
||||||
lua.pushnumber(fn_ref as f64);
|
|
||||||
lua.gettable(LUA_REGISTRYINDEX);
|
|
||||||
lua.pcall(0, 0)
|
|
||||||
.wrap_err("on_session_joined handler failed")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn leave_session(&mut self, lua: &LuaState) -> Result<()> {
|
|
||||||
if !self.session.online {
|
|
||||||
log::warn!("There is no session to leave");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let topics = mem::take(&mut self.session.topics);
|
|
||||||
self.send_tx
|
|
||||||
.send(SwarmTask::Unsubscribe(topics.into_values().collect()))
|
|
||||||
.wrap_err("Failed to queue unsubscribe task")?;
|
|
||||||
|
|
||||||
self.session.online = false;
|
|
||||||
|
|
||||||
for fn_ref in self.mods.values().filter_map(|cbs| cbs.on_session_left) {
|
|
||||||
lua.pushnumber(fn_ref as f64);
|
|
||||||
lua.gettable(LUA_REGISTRYINDEX);
|
|
||||||
lua.pcall(0, 0).wrap_err("on_session_left handler failed")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Plugin {
|
impl std::fmt::Debug for Plugin {
|
||||||
|
@ -673,9 +28,3 @@ impl std::fmt::Debug for Plugin {
|
||||||
f.write_str("PluginApi")
|
f.write_str("PluginApi")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hash(val: impl AsRef<[u8]>) -> u64 {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
hasher.write(val.as_ref());
|
|
||||||
hasher.finish()
|
|
||||||
}
|
|
||||||
|
|
|
@ -155,17 +155,6 @@ typedef int (*lua_Writer)(lua_State *L, const void *p, size_t sz, void *ud);
|
||||||
typedef void *(*lua_Alloc)(void *ud, void *ptr, size_t osize, size_t nsize);
|
typedef void *(*lua_Alloc)(void *ud, void *ptr, size_t osize, size_t nsize);
|
||||||
typedef struct luaL_Reg luaL_Reg;
|
typedef struct luaL_Reg luaL_Reg;
|
||||||
|
|
||||||
#define LUA_REGISTRYINDEX (-10000)
|
|
||||||
#define LUA_ENVIRONINDEX (-10001)
|
|
||||||
#define LUA_GLOBALSINDEX (-10002)
|
|
||||||
|
|
||||||
#define LUA_OK 0
|
|
||||||
#define LUA_YIELD 1
|
|
||||||
#define LUA_ERRRUN 2
|
|
||||||
#define LUA_ERRSYNTAX 3
|
|
||||||
#define LUA_ERRMEM 4
|
|
||||||
#define LUA_ERRERR 5
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Interface to access Lua services.
|
Interface to access Lua services.
|
||||||
*/
|
*/
|
||||||
|
|
24
src/rpc.rs
24
src/rpc.rs
|
@ -1,24 +0,0 @@
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use crate::stingray_sdk::LuaRef;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct RPC {
|
|
||||||
listeners: HashSet<LuaRef>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RPC {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
listeners: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add_listener(&mut self, fn_ref: LuaRef) {
|
|
||||||
self.listeners.insert(fn_ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn listeners(&self) -> impl Iterator<Item = &LuaRef> {
|
|
||||||
self.listeners.iter()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,22 +11,13 @@ mod bindings {
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::os::raw::c_char;
|
use std::os::raw::c_char;
|
||||||
use std::os::raw::c_int;
|
|
||||||
use std::os::raw::c_void;
|
use std::os::raw::c_void;
|
||||||
|
|
||||||
pub use bindings::GetApiFunction;
|
pub use bindings::GetApiFunction;
|
||||||
use bindings::LUA_OK;
|
|
||||||
pub use bindings::PluginApi;
|
pub use bindings::PluginApi;
|
||||||
pub use bindings::PluginApiID;
|
pub use bindings::PluginApiID;
|
||||||
use bindings::lua_CFunction;
|
use bindings::lua_CFunction;
|
||||||
use bindings::lua_Integer;
|
|
||||||
use bindings::lua_Number;
|
|
||||||
pub use bindings::lua_State;
|
pub use bindings::lua_State;
|
||||||
pub use bindings::{LUA_ENVIRONINDEX, LUA_GLOBALSINDEX, LUA_REGISTRYINDEX};
|
|
||||||
|
|
||||||
use bstr::BStr;
|
|
||||||
use color_eyre::Result;
|
|
||||||
use color_eyre::eyre;
|
|
||||||
use log::Level;
|
use log::Level;
|
||||||
|
|
||||||
impl std::default::Default for PluginApi {
|
impl std::default::Default for PluginApi {
|
||||||
|
@ -140,17 +131,6 @@ impl log::Log for LoggingApi {
|
||||||
fn flush(&self) {}
|
fn flush(&self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(i32)]
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum LuaStatus {
|
|
||||||
Ok = bindings::LUA_OK as i32,
|
|
||||||
Yield = bindings::LUA_YIELD as i32,
|
|
||||||
ErrRun = bindings::LUA_ERRRUN as i32,
|
|
||||||
ErrSyntax = bindings::LUA_ERRSYNTAX as i32,
|
|
||||||
ErrMem = bindings::LUA_ERRMEM as i32,
|
|
||||||
ErrErr = bindings::LUA_ERRERR as i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(i32)]
|
#[repr(i32)]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum LuaType {
|
pub enum LuaType {
|
||||||
|
@ -203,40 +183,10 @@ impl std::fmt::Display for LuaType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type LuaRef = i32;
|
|
||||||
|
|
||||||
pub struct LuaApi {
|
pub struct LuaApi {
|
||||||
add_module_function: unsafe extern "C" fn(*const c_char, *const c_char, lua_CFunction),
|
add_module_function: unsafe extern "C" fn(*const c_char, *const c_char, lua_CFunction),
|
||||||
createtable: unsafe extern "C" fn(*mut lua_State, i32, i32),
|
|
||||||
error: unsafe extern "C" fn(*mut lua_State) -> i32,
|
|
||||||
getscriptenvironmentstate: unsafe extern "C" fn() -> *mut lua_State,
|
|
||||||
getfield: unsafe extern "C" fn(*mut lua_State, i32, *const c_char),
|
|
||||||
gettable: unsafe extern "C" fn(*mut lua_State, i32),
|
|
||||||
gettop: unsafe extern "C" fn(*mut lua_State) -> i32,
|
|
||||||
isnumber: unsafe extern "C" fn(*mut lua_State, i32) -> c_int,
|
|
||||||
isnil: unsafe extern "C" fn(*mut lua_State, i32) -> c_int,
|
|
||||||
isstring: unsafe extern "C" fn(*mut lua_State, i32) -> c_int,
|
|
||||||
istable: unsafe extern "C" fn(*mut lua_State, i32) -> c_int,
|
|
||||||
lib_argerror: unsafe extern "C" fn(*mut lua_State, i32, *const c_char) -> i32,
|
|
||||||
lib_checklstring: unsafe extern "C" fn(*mut lua_State, i32, *mut usize) -> *const c_char,
|
|
||||||
lib_error: unsafe extern "C" fn(*mut lua_State, *const c_char, ...) -> i32,
|
|
||||||
lib_ref: unsafe extern "C" fn(*mut lua_State, i32) -> i32,
|
|
||||||
next: unsafe extern "C" fn(*mut lua_State, i32) -> i32,
|
|
||||||
objlen: unsafe extern "C" fn(*mut lua_State, i32) -> usize,
|
|
||||||
pcall: unsafe extern "C" fn(*mut lua_State, i32, i32, i32) -> i32,
|
|
||||||
pop: unsafe extern "C" fn(*mut lua_State),
|
|
||||||
pushboolean: unsafe extern "C" fn(*mut lua_State, i32),
|
|
||||||
pushinteger: unsafe extern "C" fn(*mut lua_State, lua_Integer),
|
|
||||||
pushnil: unsafe extern "C" fn(*mut lua_State),
|
|
||||||
pushnumber: unsafe extern "C" fn(*mut lua_State, lua_Number),
|
|
||||||
pushstring: unsafe extern "C" fn(*mut lua_State, *const c_char),
|
|
||||||
pushvalue: unsafe extern "C" fn(*mut lua_State, i32),
|
|
||||||
rawgeti: unsafe extern "C" fn(*mut lua_State, i32, i32),
|
|
||||||
r#type: unsafe extern "C" fn(*mut lua_State, i32) -> i32,
|
|
||||||
settable: unsafe extern "C" fn(*mut lua_State, i32),
|
|
||||||
toboolean: unsafe extern "C" fn(*mut lua_State, i32) -> i32,
|
|
||||||
tolstring: unsafe extern "C" fn(*mut lua_State, i32, *mut usize) -> *const c_char,
|
tolstring: unsafe extern "C" fn(*mut lua_State, i32, *mut usize) -> *const c_char,
|
||||||
tonumber: unsafe extern "C" fn(*mut lua_State, i32) -> f64,
|
pushstring: unsafe extern "C" fn(*mut lua_State, *const c_char),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LuaApi {
|
impl LuaApi {
|
||||||
|
@ -249,226 +199,40 @@ impl LuaApi {
|
||||||
unsafe {
|
unsafe {
|
||||||
Self {
|
Self {
|
||||||
add_module_function: (*api).add_module_function.unwrap_unchecked(),
|
add_module_function: (*api).add_module_function.unwrap_unchecked(),
|
||||||
createtable: (*api).createtable.unwrap_unchecked(),
|
|
||||||
error: (*api).error.unwrap_unchecked(),
|
|
||||||
getscriptenvironmentstate: (*api).getscriptenvironmentstate.unwrap_unchecked(),
|
|
||||||
getfield: (*api).getfield.unwrap_unchecked(),
|
|
||||||
gettable: (*api).gettable.unwrap_unchecked(),
|
|
||||||
gettop: (*api).gettop.unwrap_unchecked(),
|
|
||||||
isnumber: (*api).isnumber.unwrap_unchecked(),
|
|
||||||
isnil: (*api).isnil.unwrap_unchecked(),
|
|
||||||
isstring: (*api).isstring.unwrap_unchecked(),
|
|
||||||
istable: (*api).istable.unwrap_unchecked(),
|
|
||||||
lib_argerror: (*api).lib_argerror.unwrap_unchecked(),
|
|
||||||
lib_checklstring: (*api).lib_checklstring.unwrap_unchecked(),
|
|
||||||
lib_error: (*api).lib_error.unwrap_unchecked(),
|
|
||||||
lib_ref: (*api).lib_ref.unwrap_unchecked(),
|
|
||||||
next: (*api).next.unwrap_unchecked(),
|
|
||||||
objlen: (*api).objlen.unwrap_unchecked(),
|
|
||||||
pcall: (*api).pcall.unwrap_unchecked(),
|
|
||||||
pop: (*api).pop.unwrap_unchecked(),
|
|
||||||
pushboolean: (*api).pushboolean.unwrap_unchecked(),
|
|
||||||
pushinteger: (*api).pushinteger.unwrap_unchecked(),
|
|
||||||
pushnil: (*api).pushnil.unwrap_unchecked(),
|
|
||||||
pushnumber: (*api).pushnumber.unwrap_unchecked(),
|
|
||||||
pushstring: (*api).pushstring.unwrap_unchecked(),
|
|
||||||
pushvalue: (*api).pushvalue.unwrap_unchecked(),
|
|
||||||
rawgeti: (*api).rawgeti.unwrap_unchecked(),
|
|
||||||
r#type: (*api).type_.unwrap_unchecked(),
|
|
||||||
settable: (*api).settable.unwrap_unchecked(),
|
|
||||||
toboolean: (*api).toboolean.unwrap_unchecked(),
|
|
||||||
tolstring: (*api).tolstring.unwrap_unchecked(),
|
tolstring: (*api).tolstring.unwrap_unchecked(),
|
||||||
tonumber: (*api).tonumber.unwrap_unchecked(),
|
pushstring: (*api).pushstring.unwrap_unchecked(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_module_function(
|
pub fn add_module_function(
|
||||||
&self,
|
&self,
|
||||||
module: impl AsRef<CStr>,
|
module: impl Into<Vec<u8>>,
|
||||||
name: impl AsRef<CStr>,
|
name: impl Into<Vec<u8>>,
|
||||||
cb: extern "C" fn(*mut lua_State) -> i32,
|
cb: extern "C" fn(*mut lua_State) -> i32,
|
||||||
) {
|
) {
|
||||||
unsafe {
|
let module = CString::new(module).expect("Invalid CString");
|
||||||
(self.add_module_function)(module.as_ref().as_ptr(), name.as_ref().as_ptr(), Some(cb))
|
let name = CString::new(name).expect("Invalid CString");
|
||||||
|
|
||||||
|
unsafe { (self.add_module_function)(module.as_ptr(), name.as_ptr(), Some(cb)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tolstring(&self, L: *mut lua_State, idx: i32) -> Option<&CStr> {
|
||||||
|
let mut len: usize = 0;
|
||||||
|
|
||||||
|
let c = unsafe { (self.tolstring)(L, idx, &mut len as *mut _) };
|
||||||
|
|
||||||
|
if len == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// Safety: As long as `len > 0`, Lua guarantees the constraints that `CStr::from_ptr`
|
||||||
|
// requires.
|
||||||
|
Some(unsafe { CStr::from_ptr(c) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getscriptenvironmentstate(&self) -> *mut lua_State {
|
pub fn pushstring(&self, L: *mut lua_State, s: impl Into<Vec<u8>>) {
|
||||||
unsafe { (self.getscriptenvironmentstate)() }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error(&self, l: *mut lua_State) {
|
|
||||||
unsafe { (self.error)(l) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A wrapper that combines a lua_State and a LuaApi to make calling functions more convenient.
|
|
||||||
pub struct LuaState<'a> {
|
|
||||||
l: *mut lua_State,
|
|
||||||
api: &'a LuaApi,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> LuaState<'a> {
|
|
||||||
pub fn new(l: *mut lua_State, api: &'a LuaApi) -> Self {
|
|
||||||
Self { l, api }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gettop(&self) -> i32 {
|
|
||||||
unsafe { (self.api.gettop)(self.l) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pushvalue(&self, idx: i32) {
|
|
||||||
unsafe { (self.api.pushvalue)(self.l, idx) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isnil(&self, idx: i32) -> bool {
|
|
||||||
let is_nil = unsafe { (self.api.isnil)(self.l, idx) };
|
|
||||||
is_nil > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isnumber(&self, idx: i32) -> bool {
|
|
||||||
let is_number = unsafe { (self.api.isnumber)(self.l, idx) };
|
|
||||||
is_number > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isstring(&self, idx: i32) -> bool {
|
|
||||||
let is_string = unsafe { (self.api.isstring)(self.l, idx) };
|
|
||||||
is_string > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isfunction(&self, idx: i32) -> bool {
|
|
||||||
matches!(self.r#type(idx), LuaType::Function)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn istable(&self, idx: i32) -> bool {
|
|
||||||
let is_table = unsafe { (self.api.istable)(self.l, idx) };
|
|
||||||
is_table > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn r#type(&self, idx: i32) -> LuaType {
|
|
||||||
let t = unsafe { (self.api.r#type)(self.l, idx) };
|
|
||||||
t.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tonumber(&self, idx: i32) -> f64 {
|
|
||||||
unsafe { (self.api.tonumber)(self.l, idx) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toboolean(&self, idx: i32) -> bool {
|
|
||||||
let n = unsafe { (self.api.toboolean)(self.l, idx) };
|
|
||||||
n != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tostring(&self, idx: i32) -> &BStr {
|
|
||||||
let bytes = unsafe {
|
|
||||||
let mut len = 0usize;
|
|
||||||
// `c_char` is `i8` by default, but printable characters are all > 0, so we don't care.
|
|
||||||
let c = (self.api.tolstring)(self.l, idx, &mut len) as *const u8;
|
|
||||||
std::slice::from_raw_parts(c, len)
|
|
||||||
};
|
|
||||||
BStr::new(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn objlen(&self, idx: i32) -> usize {
|
|
||||||
unsafe { (self.api.objlen)(self.l, idx) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pushnil(&self) {
|
|
||||||
unsafe { (self.api.pushnil)(self.l) };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pushnumber(&self, n: f64) {
|
|
||||||
unsafe { (self.api.pushnumber)(self.l, n) };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pushinteger(&self, n: isize) {
|
|
||||||
unsafe { (self.api.pushinteger)(self.l, n) };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pushstring(&self, s: impl Into<Vec<u8>>) {
|
|
||||||
let s = CString::new(s).expect("Invalid CString");
|
let s = CString::new(s).expect("Invalid CString");
|
||||||
unsafe { (self.api.pushstring)(self.l, s.as_ptr()) };
|
unsafe { (self.pushstring)(L, s.as_ptr()) }
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pushboolean(&self, b: bool) {
|
|
||||||
unsafe { (self.api.pushboolean)(self.l, b as i32) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gettable(&self, idx: i32) {
|
|
||||||
unsafe { (self.api.gettable)(self.l, idx) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getfield(&self, idx: i32, k: impl AsRef<CStr>) {
|
|
||||||
unsafe { (self.api.getfield)(self.l, idx, k.as_ref().as_ptr()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn createtable(&self, narr: i32, nrec: i32) {
|
|
||||||
unsafe { (self.api.createtable)(self.l, narr, nrec) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn settable(&self, idx: i32) {
|
|
||||||
unsafe { (self.api.settable)(self.l, idx) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rawgeti(&self, idx: i32, n: i32) {
|
|
||||||
unsafe { (self.api.rawgeti)(self.l, idx, n) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn call(&self, nargs: i32, nresults: i32) {
|
|
||||||
self.pcall(nargs, nresults).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pcall(&self, nargs: i32, nresults: i32) -> Result<()> {
|
|
||||||
// TODO: Re-create the engine's error handler function to populate the stack trace and
|
|
||||||
// local variables.
|
|
||||||
let res = unsafe { (self.api.pcall)(self.l, nargs, nresults, 0) };
|
|
||||||
if res as u32 == LUA_OK {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let err = self.tostring(-1);
|
|
||||||
eyre::bail!("pcall failed: {}", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error(&self) {
|
|
||||||
self.api.error(self.l)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next(&self, idx: i32) -> i32 {
|
|
||||||
unsafe { (self.api.next)(self.l, idx) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lib_argerror(&self, narg: i32, msg: impl AsRef<CStr>) -> i32 {
|
|
||||||
unsafe { (self.api.lib_argerror)(self.l, narg, msg.as_ref().as_ptr()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lib_checklstring(&self, idx: i32) -> &BStr {
|
|
||||||
let bytes = unsafe {
|
|
||||||
let mut len = 0usize;
|
|
||||||
// `c_char` is `i8` by default, but printable characters are all > 0, so we don't care.
|
|
||||||
let c = (self.api.lib_checklstring)(self.l, idx, &mut len) as *const u8;
|
|
||||||
std::slice::from_raw_parts(c, len)
|
|
||||||
};
|
|
||||||
BStr::new(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lua's `luaL_error` does have printf-like formatting capabilities,
|
|
||||||
// but since we can just use `format!()`, we don't need to bother handling the varargs here.
|
|
||||||
pub fn lib_error(&self, msg: impl Into<Vec<u8>>) -> i32 {
|
|
||||||
let s = CString::new(msg).expect("Invalid CString");
|
|
||||||
unsafe { (self.api.lib_error)(self.l, s.as_ptr()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates and returns a reference, in the table at index t, for the object at the top of
|
|
||||||
/// the stack (and pops the object).
|
|
||||||
pub fn lib_ref(&self, t: i32) -> LuaRef {
|
|
||||||
unsafe { (self.api.lib_ref)(self.l, t) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: This is not like the standard `lua_pop`, it only pops one value at a time.
|
|
||||||
// If more values need to be popped, use `settop(-x)`.
|
|
||||||
pub fn pop(&self) {
|
|
||||||
unsafe { (self.api.pop)(self.l) };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue