From 85c67baf72269a9dbb0d434199db4f0b9bdf18c0 Mon Sep 17 00:00:00 2001 From: arse09 Date: Wed, 22 Jul 2026 18:29:25 +0200 Subject: [PATCH 1/3] basic plugins --- Cargo.lock | 120 +++++++++++++++++++++--- Cargo.toml | 16 +++- justfile | 1 + plugin-api/Cargo.toml | 9 ++ plugin-api/src/lib.rs | 85 +++++++++++++++++ plugin-api/src/meta.rs | 24 +++++ plugin-example/Cargo.toml | 12 +++ plugin-example/src/lib.rs | 14 +++ plugin-loader/Cargo.toml | 14 +++ plugin-loader/src/lib.rs | 174 +++++++++++++++++++++++++++++++++++ plugin-macros/Cargo.toml | 12 +++ plugin-macros/src/lib.rs | 11 +++ plugin-macros/src/plugin.rs | 31 +++++++ pomme-client/Cargo.toml | 4 +- pomme-client/src/app/core.rs | 11 +++ pomme-client/src/app/mod.rs | 1 + 16 files changed, 522 insertions(+), 17 deletions(-) create mode 100644 plugin-api/Cargo.toml create mode 100644 plugin-api/src/lib.rs create mode 100644 plugin-api/src/meta.rs create mode 100644 plugin-example/Cargo.toml create mode 100644 plugin-example/src/lib.rs create mode 100644 plugin-loader/Cargo.toml create mode 100644 plugin-loader/src/lib.rs create mode 100644 plugin-macros/Cargo.toml create mode 100644 plugin-macros/src/lib.rs create mode 100644 plugin-macros/src/plugin.rs diff --git a/Cargo.lock b/Cargo.lock index d3c88b6b..b7cd80ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,7 +205,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -216,7 +216,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1895,7 +1895,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2272,7 +2272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4082,7 +4082,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4175,7 +4175,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4833,7 +4833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -5061,6 +5061,45 @@ dependencies = [ "time", ] +[[package]] +name = "plugin-api" +version = "0.1.0" +dependencies = [ + "plugin-macros", + "stabby", + "tracing-shared", +] + +[[package]] +name = "plugin-example" +version = "0.1.0" +dependencies = [ + "plugin-api", + "stabby", + "tracing", +] + +[[package]] +name = "plugin-loader" +version = "0.1.0" +dependencies = [ + "libloading 0.8.9", + "plugin-api", + "stabby", + "thiserror 2.0.18", + "tracing", + "tracing-shared", +] + +[[package]] +name = "plugin-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "png" version = "0.17.16" @@ -5132,6 +5171,7 @@ dependencies = [ "mimalloc", "open", "parking_lot", + "plugin-loader", "png 0.17.16", "pomme-gpu-allocator", "pomme-protocol", @@ -5150,6 +5190,7 @@ dependencies = [ "tokio", "tracing", "tracing-appender", + "tracing-shared", "tracing-subscriber", "uuid 1.24.0", "winit", @@ -5939,7 +5980,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5996,7 +6037,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6408,6 +6449,12 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + [[package]] name = "shaderc" version = "0.10.1" @@ -6634,7 +6681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6770,6 +6817,41 @@ dependencies = [ "der", ] +[[package]] +name = "stabby" +version = "72.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d53d2428934c46277fafd2d41e39357595aa1e47954c75db2b14ed90632f3cc" +dependencies = [ + "libloading 0.8.9", + "rustversion", + "stabby-abi", +] + +[[package]] +name = "stabby-abi" +version = "72.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f375eae680bb54203ee5e47d4cd2ae7b79c0a79ed90919279f38f500ad53f190" +dependencies = [ + "rustc_version", + "rustversion", + "sha2-const-stable", + "stabby-macros", +] + +[[package]] +name = "stabby-macros" +version = "72.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea664671a576c5f7e32fee291ac123d82af5e92b0689beb3555347c00c76eef1" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -7482,7 +7564,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7925,6 +8007,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-shared" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6ce98e2ac6c82f52c702ecdf07740259fc665e0e78bacf9c888ac39bec085c" +dependencies = [ + "log", + "tracing", +] + [[package]] name = "tracing-subscriber" version = "0.3.23" @@ -7962,7 +8054,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8009,7 +8101,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8623,7 +8715,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bd37ed8e..8712c823 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,12 @@ members = [ "pomme-block", "pomme-protocol", "tools/blockgen", - "tools/protogen" + "tools/protogen", + + "plugin-api", + "plugin-loader", + "plugin-macros", + "plugin-example", ] [workspace.dependencies] @@ -28,7 +33,14 @@ glam = "0.33" # client pyronyx = "=0.3.2" tracing = "0.1" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-shared = "0.1" + +plugin-api = { path = "./plugin-api" } +plugin-loader = { path = "./plugin-loader" } +plugin-macros = { path = "./plugin-macros" } + +stabby = "=72.1.16" # azalea lags crates.io releases; track the 26.2 branch from git. # azalea-block is replaced by the pomme-block shim (pomme owns block data); diff --git a/justfile b/justfile index 1a710a29..00aded20 100644 --- a/justfile +++ b/justfile @@ -13,6 +13,7 @@ launcher-pre-pr: @pnpm --filter pomme-launcher pre-pr client-dev *args: + @cargo build -p plugin-example @cargo run -p pomme-client {{ args }} # Optimized release client for accurate benchmarking (supplies the launch token the guard needs). diff --git a/plugin-api/Cargo.toml b/plugin-api/Cargo.toml new file mode 100644 index 00000000..6885be85 --- /dev/null +++ b/plugin-api/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "plugin-api" +version = "0.1.0" +edition = "2024" + +[dependencies] +stabby.workspace = true +tracing-shared.workspace = true +plugin-macros.workspace = true diff --git a/plugin-api/src/lib.rs b/plugin-api/src/lib.rs new file mode 100644 index 00000000..b36a4f79 --- /dev/null +++ b/plugin-api/src/lib.rs @@ -0,0 +1,85 @@ +use std::fmt; + +use stabby::boxed::Box; +use stabby::dynptr; +use stabby::str::Str; + +pub mod meta; + +pub use plugin_macros::plugin; +pub use stabby; + +#[stabby::stabby] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Version { + pub major: u32, + pub minor: u32, + pub patch: u32, +} +impl Version { + pub const fn is_compatible_with(self, host: Self) -> bool { + if self.major == 0 { + return self.major == host.major + && self.minor == host.minor + && self.patch == host.patch; + } + + self.major == host.major && self.minor <= host.minor + } +} +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +pub const fn parse_u32(s: &str) -> u32 { + let bytes = s.as_bytes(); + if bytes.is_empty() { + panic!("string empty"); + } + let mut i = 0; + let mut n = 0u32; + while i < bytes.len() { + let b = bytes[i]; + if b < b'0' || b > b'9' { + panic!("not a digit"); + } + let digit = (b - b'0') as u32; + n = match n.checked_mul(10) { + Some(v) => v, + None => panic!("u32 overflow"), + }; + n = match n.checked_add(digit) { + Some(v) => v, + None => panic!("u32 overflow"), + }; + i += 1; + } + n +} + +pub trait Plugin { + fn new() -> Self; + + #[inline] + fn on_init(&mut self) {} +} + +#[stabby::stabby] +pub trait SPlugin { + extern "C" fn on_init(&mut self); +} + +impl SPlugin for T { + extern "C" fn on_init(&mut self) { + self.on_init() + } +} + +#[stabby::stabby] +pub struct PluginModule { + pub name: Str<'static>, + pub version: Version, + pub plugin: dynptr!(Box), +} diff --git a/plugin-api/src/meta.rs b/plugin-api/src/meta.rs new file mode 100644 index 00000000..ee6081c0 --- /dev/null +++ b/plugin-api/src/meta.rs @@ -0,0 +1,24 @@ +use tracing_shared::SharedLogger; + +use crate::{PluginModule, Version, parse_u32}; + +pub const PLUGIN_MARKER_SYMBOL_NAME: &str = "PLUGIN_MARKER"; +pub type PluginMarker = u64; +pub const PLUGIN_MARKER_VALUE: PluginMarker = 0x504F_4D4D_4550_4C47; + +pub const PLUGIN_API_VERSION_SYMBOL_NAME: &str = "PLUGIN_API_VERSION"; +pub type PluginApiVersion = Version; +pub const PLUGIN_API_VERSION_VALUE: PluginApiVersion = PluginApiVersion { + major: parse_u32(env!("CARGO_PKG_VERSION_MAJOR")), + minor: parse_u32(env!("CARGO_PKG_VERSION_MINOR")), + patch: parse_u32(env!("CARGO_PKG_VERSION_PATCH")), +}; + +// Must match the generated fn in plugin macro +pub const LOAD_PLUGIN_FN_NAME: &str = "load_plugin"; +pub type LoadPluginFn = extern "C" fn() -> PluginModule; + +pub const SETUP_LOGGER_FN_NAME: &str = "setup_shared_logger_ref"; +pub type SetupLoggerFn = extern "C" fn(logger: &SharedLogger); + +pub use tracing_shared::setup_shared_logger_ref; diff --git a/plugin-example/Cargo.toml b/plugin-example/Cargo.toml new file mode 100644 index 00000000..c52160a3 --- /dev/null +++ b/plugin-example/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "plugin-example" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +plugin-api.workspace = true +tracing.workspace = true +stabby.workspace = true diff --git a/plugin-example/src/lib.rs b/plugin-example/src/lib.rs new file mode 100644 index 00000000..2c8125b6 --- /dev/null +++ b/plugin-example/src/lib.rs @@ -0,0 +1,14 @@ +use plugin_api::{Plugin, plugin}; + +#[plugin] +struct ExamplePlugin {} + +impl Plugin for ExamplePlugin { + fn new() -> Self { + Self {} + } + + fn on_init(&mut self) { + tracing::info!("Hello from plugin!"); + } +} diff --git a/plugin-loader/Cargo.toml b/plugin-loader/Cargo.toml new file mode 100644 index 00000000..e3393838 --- /dev/null +++ b/plugin-loader/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "plugin-loader" +version = "0.1.0" +edition = "2024" + +[dependencies] +stabby = { workspace = true, features = ["libloading"] } +thiserror.workspace = true +libloading = ">=0.7.3, <0.10" # Should match stabby's version of libloading + +plugin-api.workspace = true + +tracing.workspace = true +tracing-shared.workspace = true diff --git a/plugin-loader/src/lib.rs b/plugin-loader/src/lib.rs new file mode 100644 index 00000000..9c9fa483 --- /dev/null +++ b/plugin-loader/src/lib.rs @@ -0,0 +1,174 @@ +use std::path::Path; +use std::{fmt, fs}; + +use libloading::Library; +use plugin_api::meta::{ + LOAD_PLUGIN_FN_NAME, LoadPluginFn, PLUGIN_API_VERSION_SYMBOL_NAME, PLUGIN_API_VERSION_VALUE, + PLUGIN_MARKER_SYMBOL_NAME, PLUGIN_MARKER_VALUE, PluginApiVersion, PluginMarker, + SETUP_LOGGER_FN_NAME, SetupLoggerFn, +}; +use plugin_api::{SPlugin, SPluginDynMut as _, Version}; +use stabby::boxed::Box as SBox; +use stabby::dynptr; +use stabby::libloading::StabbyLibrary; +use tracing_shared::SharedLogger; + +#[cfg(target_os = "windows")] +const LIB_EXT: &str = "dll"; +#[cfg(target_os = "macos")] +const LIB_EXT: &str = "dylib"; +#[cfg(all(unix, not(target_os = "macos")))] +const LIB_EXT: &str = "so"; + +pub struct LoadedPlugin { + name: &'static str, + version: Version, + plugin: dynptr!(SBox), + _library: Library, +} + +pub struct Plugins { + plugins: Vec, +} + +impl Plugins { + pub fn load(directory: &Path, shared_logger: &SharedLogger) -> Self { + let mut loaded = Vec::new(); + + let entries = match fs::read_dir(directory) { + Ok(entries) => entries, + Err(err) => { + tracing::error!("Failed to read plugin directory {directory:?}: {err}"); + return Self { plugins: loaded }; + } + }; + + for entry in entries { + let entry = match entry { + Ok(entry) => entry, + Err(err) => { + tracing::warn!("Failed to read directory entry: {err}"); + continue; + } + }; + + let path = entry.path(); + if !path.is_file() { + continue; + } + let name = entry.file_name(); + + let is_lib = path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case(LIB_EXT)) + .unwrap_or(false); + if !is_lib { + continue; + } + + let lib = match unsafe { Library::new(&path) } { + Ok(lib) => lib, + Err(err) => { + tracing::error!("Failed to load plugin {name:?}: {err}"); + continue; + } + }; + + let is_plugin = unsafe { + lib.get::<&PluginMarker>(PLUGIN_MARKER_SYMBOL_NAME.as_bytes()) + .map(|val| **val == PLUGIN_MARKER_VALUE) + .unwrap_or(false) + }; + if !is_plugin { + tracing::debug!("Skipping non-plugin library {path:?}"); + continue; + } + + let plugin_api_version = unsafe { + match lib.get::<&PluginApiVersion>(PLUGIN_API_VERSION_SYMBOL_NAME.as_bytes()) { + Ok(v) => *v, + Err(err) => { + tracing::error!( + "Plugin {name:?} is missing required symbol `{}`: {err}", + PLUGIN_API_VERSION_SYMBOL_NAME, + ); + continue; + } + } + }; + if !plugin_api_version.is_compatible_with(PLUGIN_API_VERSION_VALUE) { + tracing::error!( + "Incompatible plugin API version: {name:?} v{}, client v{}", + plugin_api_version, + PLUGIN_API_VERSION_VALUE, + ); + continue; + } + + let load_plugin = + match unsafe { lib.get_stabbied::(LOAD_PLUGIN_FN_NAME.as_bytes()) } { + Ok(f) => f, + Err(err) => { + tracing::error!( + "Plugin {name:?} is missing required symbol `{}`: {err}", + LOAD_PLUGIN_FN_NAME, + ); + continue; + } + }; + let plugin = load_plugin(); + + let setup_logger = + match unsafe { lib.get::(SETUP_LOGGER_FN_NAME.as_bytes()) } { + Ok(f) => f, + Err(err) => { + tracing::error!( + "Plugin {name:?} is missing required symbol `{}`: {err}", + SETUP_LOGGER_FN_NAME, + ); + continue; + } + }; + setup_logger(shared_logger); + + loaded.push(LoadedPlugin { + name: plugin.name.as_str(), + version: plugin.version, + plugin: plugin.plugin, + _library: lib, + }); + } + + let slf = Self { plugins: loaded }; + + tracing::info!("Loaded plugins: {}", slf); + + slf + } + + pub fn init_all(&mut self) { + for plugin in &mut self.plugins { + plugin.plugin.on_init(); + } + } +} + +impl fmt::Display for Plugins { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.plugins.is_empty() { + return write!(f, ""); + } + + writeln!(f)?; + for (i, plugin) in self.plugins.iter().enumerate() { + if i != 0 { + writeln!(f)?; + } + + write!(f, "- {} v{}", plugin.name, plugin.version,)?; + } + + Ok(()) + } +} diff --git a/plugin-macros/Cargo.toml b/plugin-macros/Cargo.toml new file mode 100644 index 00000000..b70dc0c7 --- /dev/null +++ b/plugin-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "plugin-macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" diff --git a/plugin-macros/src/lib.rs b/plugin-macros/src/lib.rs new file mode 100644 index 00000000..6d051ccb --- /dev/null +++ b/plugin-macros/src/lib.rs @@ -0,0 +1,11 @@ +mod plugin; + +#[proc_macro_attribute] +pub fn plugin( + attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + plugin::plugin(attr.into(), item.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} diff --git a/plugin-macros/src/plugin.rs b/plugin-macros/src/plugin.rs new file mode 100644 index 00000000..ead2ef64 --- /dev/null +++ b/plugin-macros/src/plugin.rs @@ -0,0 +1,31 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::ItemStruct; + +pub fn plugin(_attr: TokenStream, item: TokenStream) -> syn::Result { + let input = syn::parse2::(item)?; + + Ok(quote! { + #input + + #[::stabby::export] + pub extern "C" fn load_plugin() -> ::plugin_api::PluginModule { + ::plugin_api::PluginModule { + name: env!("CARGO_PKG_NAME").into(), + version: ::plugin_api::Version { + major: ::plugin_api::parse_u32(env!("CARGO_PKG_VERSION_MAJOR")), + minor: ::plugin_api::parse_u32(env!("CARGO_PKG_VERSION_MINOR")), + patch: ::plugin_api::parse_u32(env!("CARGO_PKG_VERSION_PATCH")), + }, + plugin: ::stabby::boxed::Box::new(::new()).into(), + } + } + + #[unsafe(no_mangle)] + pub static PLUGIN_MARKER: ::plugin_api::meta::PluginMarker = ::plugin_api::meta::PLUGIN_MARKER_VALUE; + #[unsafe(no_mangle)] + pub static PLUGIN_API_VERSION: ::plugin_api::meta::PluginApiVersion = ::plugin_api::meta::PLUGIN_API_VERSION_VALUE; + + pub use ::plugin_api::meta::setup_shared_logger_ref; + }) +} diff --git a/pomme-client/Cargo.toml b/pomme-client/Cargo.toml index 38afde6c..e02e97ac 100644 --- a/pomme-client/Cargo.toml +++ b/pomme-client/Cargo.toml @@ -19,8 +19,10 @@ zip = { workspace = true } tokio = { workspace = true, features = ["tracing", "net", "time"] } reqwest = { workspace = true, features = ["blocking"] } pyronyx = { workspace = true, features = ["rwh_06"] } +plugin-loader = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-subscriber = { workspace = true } +tracing-shared = { workspace = true } tracing-appender = "0.2" # Per-thread-heap allocator: the chunk-mesh worker pool churns vertex/index Vecs diff --git a/pomme-client/src/app/core.rs b/pomme-client/src/app/core.rs index cb71f743..1438a21c 100644 --- a/pomme-client/src/app/core.rs +++ b/pomme-client/src/app/core.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::ops::Add; +use std::path::Path; use std::sync::Arc; use std::time::Instant; @@ -7,6 +8,8 @@ use azalea_protocol::packets::game::{ ServerboundClientCommand, ServerboundGamePacket, s_client_command, s_client_tick_end, }; use glam::{FloatExt, dvec3}; +use plugin_loader::Plugins; +use tracing_shared::SharedLogger; use winit::keyboard::KeyCode; use winit::window::{CursorGrabMode, Window}; @@ -177,6 +180,7 @@ pub struct AppCore { pub audio: crate::audio::AudioEngine, pub tick_accumulator: f32, pub time_tick_accumulator: f32, + pub plugins: Plugins, player_skin_tx: crossbeam_channel::Sender, player_skin_rx: crossbeam_channel::Receiver, requested_player_skins: HashMap>, @@ -210,6 +214,12 @@ impl AppCore { ); let (player_skin_tx, player_skin_rx) = crossbeam_channel::unbounded(); + let logger = SharedLogger::new(); + let plugins = Plugins::load( + Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../target/debug/")), + &logger, + ); + Self { user, presence, @@ -225,6 +235,7 @@ impl AppCore { audio, tick_accumulator: 0.0, time_tick_accumulator: 0.0, + plugins, player_skin_tx, player_skin_rx, requested_player_skins: HashMap::new(), diff --git a/pomme-client/src/app/mod.rs b/pomme-client/src/app/mod.rs index d7a1880e..85518580 100644 --- a/pomme-client/src/app/mod.rs +++ b/pomme-client/src/app/mod.rs @@ -127,6 +127,7 @@ impl App { } pub fn run(&mut self) -> Result<(), WindowError> { + self.core.plugins.init_all(); let event_loop = EventLoop::new()?; event_loop.run_app(self)?; Ok(()) From f2e06a2da2cc689305a6b29710c20d2efa93d8cd Mon Sep 17 00:00:00 2001 From: arse09 Date: Thu, 23 Jul 2026 12:46:29 +0200 Subject: [PATCH 2/3] add some client events --- plugin-api/src/lib.rs | 79 +++++++---------------- plugin-api/src/meta.rs | 54 +++++++++++++++- plugin-example/src/lib.rs | 57 ++++++++++++++-- plugin-loader/src/lib.rs | 45 ++++++++++--- pomme-client/src/app/core.rs | 10 ++- pomme-client/src/app/mod.rs | 14 ++-- pomme-client/src/app/phases/connecting.rs | 23 +++++++ pomme-client/src/app/phases/in_game.rs | 4 ++ pomme-client/src/app/phases/in_menu.rs | 10 +++ 9 files changed, 214 insertions(+), 82 deletions(-) diff --git a/plugin-api/src/lib.rs b/plugin-api/src/lib.rs index b36a4f79..e285a224 100644 --- a/plugin-api/src/lib.rs +++ b/plugin-api/src/lib.rs @@ -1,5 +1,3 @@ -use std::fmt; - use stabby::boxed::Box; use stabby::dynptr; use stabby::str::Str; @@ -9,71 +7,40 @@ pub mod meta; pub use plugin_macros::plugin; pub use stabby; -#[stabby::stabby] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Version { - pub major: u32, - pub minor: u32, - pub patch: u32, -} -impl Version { - pub const fn is_compatible_with(self, host: Self) -> bool { - if self.major == 0 { - return self.major == host.major - && self.minor == host.minor - && self.patch == host.patch; - } - - self.major == host.major && self.minor <= host.minor - } -} -impl fmt::Display for Version { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}.{}.{}", self.major, self.minor, self.patch) - } -} - -pub const fn parse_u32(s: &str) -> u32 { - let bytes = s.as_bytes(); - if bytes.is_empty() { - panic!("string empty"); - } - let mut i = 0; - let mut n = 0u32; - while i < bytes.len() { - let b = bytes[i]; - if b < b'0' || b > b'9' { - panic!("not a digit"); - } - let digit = (b - b'0') as u32; - n = match n.checked_mul(10) { - Some(v) => v, - None => panic!("u32 overflow"), - }; - n = match n.checked_add(digit) { - Some(v) => v, - None => panic!("u32 overflow"), - }; - i += 1; - } - n -} +use crate::meta::Version; pub trait Plugin { fn new() -> Self; - #[inline] - fn on_init(&mut self) {} + fn on_client_started(&mut self) {} + fn on_client_stopping(&mut self) {} + + fn on_client_tick_start(&mut self) {} + fn on_client_tick_end(&mut self) {} } #[stabby::stabby] pub trait SPlugin { - extern "C" fn on_init(&mut self); + extern "C" fn on_client_started(&mut self); + extern "C" fn on_client_stopping(&mut self); + + extern "C" fn on_client_tick_start(&mut self); + extern "C" fn on_client_tick_end(&mut self); } impl SPlugin for T { - extern "C" fn on_init(&mut self) { - self.on_init() + extern "C" fn on_client_started(&mut self) { + ::on_client_started(self); + } + extern "C" fn on_client_stopping(&mut self) { + ::on_client_stopping(self); + } + + extern "C" fn on_client_tick_start(&mut self) { + ::on_client_tick_start(self); + } + extern "C" fn on_client_tick_end(&mut self) { + ::on_client_tick_end(self); } } diff --git a/plugin-api/src/meta.rs b/plugin-api/src/meta.rs index ee6081c0..cb04d2b6 100644 --- a/plugin-api/src/meta.rs +++ b/plugin-api/src/meta.rs @@ -1,6 +1,58 @@ +use std::fmt; + use tracing_shared::SharedLogger; -use crate::{PluginModule, Version, parse_u32}; +use crate::PluginModule; + +#[stabby::stabby] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Version { + pub major: u32, + pub minor: u32, + pub patch: u32, +} +impl Version { + pub const fn is_compatible_with(self, host: Self) -> bool { + if self.major == 0 { + return self.major == host.major + && self.minor == host.minor + && self.patch == host.patch; + } + + self.major == host.major && self.minor <= host.minor + } +} +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +pub const fn parse_u32(s: &str) -> u32 { + let bytes = s.as_bytes(); + if bytes.is_empty() { + panic!("string empty"); + } + let mut i = 0; + let mut n = 0u32; + while i < bytes.len() { + let b = bytes[i]; + if b < b'0' || b > b'9' { + panic!("not a digit"); + } + let digit = (b - b'0') as u32; + n = match n.checked_mul(10) { + Some(v) => v, + None => panic!("u32 overflow"), + }; + n = match n.checked_add(digit) { + Some(v) => v, + None => panic!("u32 overflow"), + }; + i += 1; + } + n +} pub const PLUGIN_MARKER_SYMBOL_NAME: &str = "PLUGIN_MARKER"; pub type PluginMarker = u64; diff --git a/plugin-example/src/lib.rs b/plugin-example/src/lib.rs index 2c8125b6..16f8eaad 100644 --- a/plugin-example/src/lib.rs +++ b/plugin-example/src/lib.rs @@ -1,14 +1,59 @@ -use plugin_api::{Plugin, plugin}; +use plugin_api::Plugin; -#[plugin] -struct ExamplePlugin {} +// #[plugin] +struct ExamplePlugin { + total_ticks: u64, +} impl Plugin for ExamplePlugin { fn new() -> Self { - Self {} + Self { total_ticks: 0 } + } + + fn on_client_started(&mut self) { + let span = tracing::info_span!("on_client_started"); + let _enter = span.enter(); + tracing::info!("Client is about to tick."); + } + + fn on_client_stopping(&mut self) { + let span = tracing::info_span!("on_client_stopping"); + let _enter = span.enter(); + tracing::info!("Client is stopping. Total ticks: {}", self.total_ticks); + } + + fn on_client_tick_start(&mut self) { + let span = tracing::info_span!("on_client_tick_start"); + let _enter = span.enter(); + tracing::info!("Client tick started."); + } + + fn on_client_tick_end(&mut self) { + let span = tracing::info_span!("on_client_tick_end"); + let _enter = span.enter(); + tracing::info!("Client tick ended"); + self.total_ticks += 1; } +} - fn on_init(&mut self) { - tracing::info!("Hello from plugin!"); +#[::stabby::export] +pub extern "C" fn load_plugin() -> ::plugin_api::PluginModule { + ::plugin_api::PluginModule { + name: env!("CARGO_PKG_NAME").into(), + version: ::plugin_api::meta::Version { + major: ::plugin_api::meta::parse_u32(env!("CARGO_PKG_VERSION_MAJOR")), + minor: ::plugin_api::meta::parse_u32(env!("CARGO_PKG_VERSION_MINOR")), + patch: ::plugin_api::meta::parse_u32(env!("CARGO_PKG_VERSION_PATCH")), + }, + plugin: ::stabby::boxed::Box::new(::new()).into(), } } + +#[unsafe(no_mangle)] +pub static PLUGIN_MARKER: ::plugin_api::meta::PluginMarker = + ::plugin_api::meta::PLUGIN_MARKER_VALUE; +#[unsafe(no_mangle)] +pub static PLUGIN_API_VERSION: ::plugin_api::meta::PluginApiVersion = + ::plugin_api::meta::PLUGIN_API_VERSION_VALUE; + +pub use ::plugin_api::meta::setup_shared_logger_ref; diff --git a/plugin-loader/src/lib.rs b/plugin-loader/src/lib.rs index 9c9fa483..9bf1c844 100644 --- a/plugin-loader/src/lib.rs +++ b/plugin-loader/src/lib.rs @@ -5,9 +5,9 @@ use libloading::Library; use plugin_api::meta::{ LOAD_PLUGIN_FN_NAME, LoadPluginFn, PLUGIN_API_VERSION_SYMBOL_NAME, PLUGIN_API_VERSION_VALUE, PLUGIN_MARKER_SYMBOL_NAME, PLUGIN_MARKER_VALUE, PluginApiVersion, PluginMarker, - SETUP_LOGGER_FN_NAME, SetupLoggerFn, + SETUP_LOGGER_FN_NAME, SetupLoggerFn, Version, }; -use plugin_api::{SPlugin, SPluginDynMut as _, Version}; +use plugin_api::{SPlugin, SPluginDynMut as _}; use stabby::boxed::Box as SBox; use stabby::dynptr; use stabby::libloading::StabbyLibrary; @@ -32,17 +32,22 @@ pub struct Plugins { } impl Plugins { - pub fn load(directory: &Path, shared_logger: &SharedLogger) -> Self { + fn new(plugins: Vec) -> Self { + Self { plugins } + } + + pub fn load(directory: &Path) -> Self { let mut loaded = Vec::new(); let entries = match fs::read_dir(directory) { Ok(entries) => entries, Err(err) => { tracing::error!("Failed to read plugin directory {directory:?}: {err}"); - return Self { plugins: loaded }; + return Self::new(loaded); } }; + let logger = SharedLogger::new(); for entry in entries { let entry = match entry { Ok(entry) => entry, @@ -130,7 +135,7 @@ impl Plugins { continue; } }; - setup_logger(shared_logger); + setup_logger(&logger); loaded.push(LoadedPlugin { name: plugin.name.as_str(), @@ -140,16 +145,38 @@ impl Plugins { }); } - let slf = Self { plugins: loaded }; + let slf = Self::new(loaded); - tracing::info!("Loaded plugins: {}", slf); + tracing::info!("Loaded {} plugins: {}", slf.plugins.len(), slf); slf } - pub fn init_all(&mut self) { + #[inline] + pub fn fire_client_started(&mut self) { + for plugin in &mut self.plugins { + plugin.plugin.on_client_started(); + } + } + + #[inline] + pub fn fire_client_stopping(&mut self) { + for plugin in &mut self.plugins { + plugin.plugin.on_client_stopping(); + } + } + + #[inline] + pub fn fire_client_tick_start(&mut self) { + for plugin in &mut self.plugins { + plugin.plugin.on_client_tick_start(); + } + } + + #[inline] + pub fn fire_client_tick_end(&mut self) { for plugin in &mut self.plugins { - plugin.plugin.on_init(); + plugin.plugin.on_client_tick_end(); } } } diff --git a/pomme-client/src/app/core.rs b/pomme-client/src/app/core.rs index 1438a21c..a854c50a 100644 --- a/pomme-client/src/app/core.rs +++ b/pomme-client/src/app/core.rs @@ -9,7 +9,6 @@ use azalea_protocol::packets::game::{ }; use glam::{FloatExt, dvec3}; use plugin_loader::Plugins; -use tracing_shared::SharedLogger; use winit::keyboard::KeyCode; use winit::window::{CursorGrabMode, Window}; @@ -214,11 +213,10 @@ impl AppCore { ); let (player_skin_tx, player_skin_rx) = crossbeam_channel::unbounded(); - let logger = SharedLogger::new(); - let plugins = Plugins::load( - Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../target/debug/")), - &logger, - ); + let plugins = Plugins::load(Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../target/debug/" + ))); Self { user, diff --git a/pomme-client/src/app/mod.rs b/pomme-client/src/app/mod.rs index 85518580..94b546cd 100644 --- a/pomme-client/src/app/mod.rs +++ b/pomme-client/src/app/mod.rs @@ -127,7 +127,6 @@ impl App { } pub fn run(&mut self) -> Result<(), WindowError> { - self.core.plugins.init_all(); let event_loop = EventLoop::new()?; event_loop.run_app(self)?; Ok(()) @@ -209,6 +208,7 @@ impl ApplicationHandler for App { } self.core.apply_cursor_grab(&window, None); + self.core.plugins.fire_client_started(); if let Some(server_ip) = quick_access_multiplayer { let connection = spawn_connection( @@ -266,12 +266,12 @@ impl ApplicationHandler for App { event: WindowEvent, ) { match event { - WindowEvent::CloseRequested | WindowEvent::Destroyed => { + WindowEvent::CloseRequested => { event_loop.exit(); } WindowEvent::Resized(new_size) => { - if let Some(app_rt) = self.phase.gfx_mut() { - app_rt.renderer.resize(new_size); + if let Some(gfx) = self.phase.gfx_mut() { + gfx.renderer.resize(new_size); } } WindowEvent::ModifiersChanged(mods) => { @@ -661,3 +661,9 @@ impl ApplicationHandler for App { event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); } } + +impl Drop for App { + fn drop(&mut self) { + self.core.plugins.fire_client_stopping(); + } +} diff --git a/pomme-client/src/app/phases/connecting.rs b/pomme-client/src/app/phases/connecting.rs index b5805825..c4153e01 100644 --- a/pomme-client/src/app/phases/connecting.rs +++ b/pomme-client/src/app/phases/connecting.rs @@ -1,5 +1,6 @@ use azalea_protocol::packets::game::ServerboundGamePacket; +use crate::app::TICK_RATE; use crate::app::core::AppCore; use crate::app::phases::in_game::GameState; use crate::app::phases::{ConnectionPhase, Gfx, Panorama}; @@ -61,6 +62,28 @@ pub fn update_connecting( } } + core.tick_accumulator += dt; + while core.tick_accumulator >= TICK_RATE { + core.plugins.fire_client_tick_start(); + + game.tick_count = game.tick_count.wrapping_add(1); + core.tick_physics(&mut gfx.renderer, connection, game); + game.item_entity_store.tick(&game.chunk_store); + game.particle_store.tick(&game.chunk_store); + game.block_entity_anim.tick(); + if let Some(c) = &mut game.open_container + && let Some(state) = &mut c.enchant + { + state.tick(&c.slots, &c.data); + // Vanilla `EnchantmentScreen.containerTick` keeps the XP bar + // prioritized while the screen is open. + game.xp_display_start_tick = game.tick_count as i64; + } + core.tick_accumulator -= TICK_RATE; + + core.plugins.fire_client_tick_end(); + } + let status_text = match connect_phase { ConnectionPhase::Loading => "Loading terrain...", ConnectionPhase::Connecting => "Connecting to the server...", diff --git a/pomme-client/src/app/phases/in_game.rs b/pomme-client/src/app/phases/in_game.rs index e64e2d17..4d1e3109 100644 --- a/pomme-client/src/app/phases/in_game.rs +++ b/pomme-client/src/app/phases/in_game.rs @@ -1122,6 +1122,8 @@ pub fn update_game( // Menus never pause the simulation; tick_physics substitutes neutral input. core.tick_accumulator += dt; while core.tick_accumulator >= TICK_RATE { + core.plugins.fire_client_tick_start(); + game.tick_count = game.tick_count.wrapping_add(1); core.tick_physics(&mut gfx.renderer, connection, game); game.item_entity_store.tick(&game.chunk_store); @@ -1136,6 +1138,8 @@ pub fn update_game( game.xp_display_start_tick = game.tick_count as i64; } core.tick_accumulator -= TICK_RATE; + + core.plugins.fire_client_tick_end(); } // Once per frame after the frame's ticks, where vanilla `Minecraft.runTick` diff --git a/pomme-client/src/app/phases/in_menu.rs b/pomme-client/src/app/phases/in_menu.rs index 67e81960..57bb1465 100644 --- a/pomme-client/src/app/phases/in_menu.rs +++ b/pomme-client/src/app/phases/in_menu.rs @@ -1,3 +1,4 @@ +use crate::app::TICK_RATE; use crate::app::core::AppCore; use crate::app::phases::{Gfx, Panorama}; use crate::net::connection::ConnectArgs; @@ -20,6 +21,15 @@ pub fn update_menu( core.audio.start_menu_music(); core.audio.update_menu_music(dt); + core.tick_accumulator += dt; + while core.tick_accumulator >= TICK_RATE { + core.plugins.fire_client_tick_start(); + + core.tick_accumulator -= TICK_RATE; + + core.plugins.fire_client_tick_end(); + } + let sw = gfx.renderer.screen_width() as f32; let sh = gfx.renderer.screen_height() as f32; From 22ef6ab9cd3edc7388ef67a1dc914cbba0075dab Mon Sep 17 00:00:00 2001 From: arse09 Date: Thu, 23 Jul 2026 13:58:18 +0200 Subject: [PATCH 3/3] use plugin macro for example --- plugin-example/src/lib.rs | 76 +++++++++++++++++++------------------ plugin-macros/src/plugin.rs | 8 ++-- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/plugin-example/src/lib.rs b/plugin-example/src/lib.rs index 16f8eaad..c9827c37 100644 --- a/plugin-example/src/lib.rs +++ b/plugin-example/src/lib.rs @@ -1,59 +1,63 @@ -use plugin_api::Plugin; +use std::time::{Duration, Instant}; -// #[plugin] +use plugin_api::{Plugin, plugin}; + +#[plugin] struct ExamplePlugin { total_ticks: u64, + + second_start: Instant, + ticks_this_second: u32, + + tick_start: Instant, + total_tick_time: Duration, } impl Plugin for ExamplePlugin { fn new() -> Self { - Self { total_ticks: 0 } + let now = Instant::now(); + + Self { + total_ticks: 0, + + second_start: now, + ticks_this_second: 0, + + tick_start: now, + total_tick_time: Duration::ZERO, + } } fn on_client_started(&mut self) { - let span = tracing::info_span!("on_client_started"); - let _enter = span.enter(); - tracing::info!("Client is about to tick."); + tracing::info!("Started"); } fn on_client_stopping(&mut self) { - let span = tracing::info_span!("on_client_stopping"); - let _enter = span.enter(); - tracing::info!("Client is stopping. Total ticks: {}", self.total_ticks); + tracing::info!("Stopping"); + tracing::info!("Total ticks: {}", self.total_ticks); } fn on_client_tick_start(&mut self) { - let span = tracing::info_span!("on_client_tick_start"); - let _enter = span.enter(); - tracing::info!("Client tick started."); + self.tick_start = Instant::now(); } fn on_client_tick_end(&mut self) { - let span = tracing::info_span!("on_client_tick_end"); - let _enter = span.enter(); - tracing::info!("Client tick ended"); self.total_ticks += 1; - } -} + self.ticks_this_second += 1; -#[::stabby::export] -pub extern "C" fn load_plugin() -> ::plugin_api::PluginModule { - ::plugin_api::PluginModule { - name: env!("CARGO_PKG_NAME").into(), - version: ::plugin_api::meta::Version { - major: ::plugin_api::meta::parse_u32(env!("CARGO_PKG_VERSION_MAJOR")), - minor: ::plugin_api::meta::parse_u32(env!("CARGO_PKG_VERSION_MINOR")), - patch: ::plugin_api::meta::parse_u32(env!("CARGO_PKG_VERSION_PATCH")), - }, - plugin: ::stabby::boxed::Box::new(::new()).into(), - } -} + self.total_tick_time += self.tick_start.elapsed(); + + let elapsed = self.second_start.elapsed(); -#[unsafe(no_mangle)] -pub static PLUGIN_MARKER: ::plugin_api::meta::PluginMarker = - ::plugin_api::meta::PLUGIN_MARKER_VALUE; -#[unsafe(no_mangle)] -pub static PLUGIN_API_VERSION: ::plugin_api::meta::PluginApiVersion = - ::plugin_api::meta::PLUGIN_API_VERSION_VALUE; + if elapsed >= Duration::from_secs(1) { + let tps = self.ticks_this_second as f64 / elapsed.as_secs_f64(); + let mspt = self.total_tick_time.as_secs_f64() * 1000.0 / self.ticks_this_second as f64; -pub use ::plugin_api::meta::setup_shared_logger_ref; + tracing::info!("TPS: {:.2} | MSPT: {:.2}", tps, mspt); + + self.second_start = Instant::now(); + self.ticks_this_second = 0; + self.total_tick_time = Duration::ZERO; + } + } +} diff --git a/plugin-macros/src/plugin.rs b/plugin-macros/src/plugin.rs index ead2ef64..c2d73d9c 100644 --- a/plugin-macros/src/plugin.rs +++ b/plugin-macros/src/plugin.rs @@ -12,10 +12,10 @@ pub fn plugin(_attr: TokenStream, item: TokenStream) -> syn::Result pub extern "C" fn load_plugin() -> ::plugin_api::PluginModule { ::plugin_api::PluginModule { name: env!("CARGO_PKG_NAME").into(), - version: ::plugin_api::Version { - major: ::plugin_api::parse_u32(env!("CARGO_PKG_VERSION_MAJOR")), - minor: ::plugin_api::parse_u32(env!("CARGO_PKG_VERSION_MINOR")), - patch: ::plugin_api::parse_u32(env!("CARGO_PKG_VERSION_PATCH")), + version: ::plugin_api::meta::Version { + major: ::plugin_api::meta::parse_u32(env!("CARGO_PKG_VERSION_MAJOR")), + minor: ::plugin_api::meta::parse_u32(env!("CARGO_PKG_VERSION_MINOR")), + patch: ::plugin_api::meta::parse_u32(env!("CARGO_PKG_VERSION_PATCH")), }, plugin: ::stabby::boxed::Box::new(::new()).into(), }