From 6c2ee8ea173a594bde650099a5422cfcc28aee85 Mon Sep 17 00:00:00 2001 From: Timoteo Rider Date: Wed, 26 Aug 2026 22:09:22 -0600 Subject: [PATCH 1/6] Add stable mod logging API --- Cargo.toml | 4 + crates/highfleet-mod-api/Cargo.toml | 10 + crates/highfleet-mod-api/src/ffi.rs | 187 ++++++++++++++ crates/highfleet-mod-api/src/lib.rs | 62 +++++ crates/highfleet-mod-api/src/logging.rs | 312 ++++++++++++++++++++++++ 5 files changed, 575 insertions(+) create mode 100644 crates/highfleet-mod-api/Cargo.toml create mode 100644 crates/highfleet-mod-api/src/ffi.rs create mode 100644 crates/highfleet-mod-api/src/lib.rs create mode 100644 crates/highfleet-mod-api/src/logging.rs diff --git a/Cargo.toml b/Cargo.toml index 4b6763b..2b71a7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,10 @@ version = "0.1.0" edition = "2021" # build = "build.rs" +[workspace] +members = ["crates/highfleet-mod-api"] +resolver = "2" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/crates/highfleet-mod-api/Cargo.toml b/crates/highfleet-mod-api/Cargo.toml new file mode 100644 index 0000000..b339ba2 --- /dev/null +++ b/crates/highfleet-mod-api/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "highfleet-mod-api" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Stable FFI API and Rust logging adapter for Highfleet mods" + +[dependencies] +log = { version = "0.4.28", features = ["std"] } +thiserror = "2.0" diff --git a/crates/highfleet-mod-api/src/ffi.rs b/crates/highfleet-mod-api/src/ffi.rs new file mode 100644 index 0000000..e4cad90 --- /dev/null +++ b/crates/highfleet-mod-api/src/ffi.rs @@ -0,0 +1,187 @@ +//! Raw, C-compatible host API definitions. +//! +//! These definitions intentionally contain no Rust references, slices, trait +//! objects, enums, strings, or ownership-bearing values. + +use core::ffi::c_void; + +/// Version implemented by [`HfmHostApiV1`]. +pub const HFM_HOST_ABI_VERSION_1: u32 = 1; + +/// Setup completed successfully. +pub const HFM_STATUS_OK: u32 = 0; +/// The host API pointer was null. +pub const HFM_STATUS_NULL_API: u32 = 1; +/// The host API uses an unsupported ABI version. +pub const HFM_STATUS_UNSUPPORTED_ABI: u32 = 2; +/// The supplied host API is smaller than the requested ABI version. +pub const HFM_STATUS_API_TOO_SMALL: u32 = 3; +/// A required callback was null. +pub const HFM_STATUS_MISSING_CALLBACK: u32 = 4; +/// The mod already installed a logger. +pub const HFM_STATUS_LOGGER_ALREADY_SET: u32 = 5; +/// Setup panicked before it could return normally. +pub const HFM_STATUS_PANIC: u32 = u32::MAX; + +/// Error log level. +pub const HFM_LOG_LEVEL_ERROR: u32 = 1; +/// Warning log level. +pub const HFM_LOG_LEVEL_WARN: u32 = 2; +/// Informational log level. +pub const HFM_LOG_LEVEL_INFO: u32 = 3; +/// Debug log level. +pub const HFM_LOG_LEVEL_DEBUG: u32 = 4; +/// Trace log level. +pub const HFM_LOG_LEVEL_TRACE: u32 = 5; + +/// A borrowed UTF-8 string passed across the host boundary. +/// +/// The bytes are only valid for the duration of the callback receiving this +/// value. A null pointer is valid only when `len` is zero. The receiver must +/// not retain or free the pointer. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct HfmStr { + /// Pointer to the first UTF-8 byte. + pub ptr: *const u8, + /// Number of bytes, excluding any null terminator. + pub len: usize, +} + +impl HfmStr { + /// Creates an empty string with no backing allocation. + pub const fn empty() -> Self { + Self { + ptr: core::ptr::null(), + len: 0, + } + } +} + +impl From<&str> for HfmStr { + /// Borrows a Rust string for use during an immediate FFI call. + fn from(value: &str) -> Self { + Self { + ptr: value.as_ptr(), + len: value.len(), + } + } +} + +impl Default for HfmStr { + fn default() -> Self { + Self::empty() + } +} + +/// A single, already-formatted log record. +/// +/// The mod owns every referenced string. All strings cease to be valid when +/// the `log_write` callback returns. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct HfmLogRecordV1 { + /// Size of this record in bytes, for forward-compatible extension. + pub struct_size: u32, + /// One of the `HFM_LOG_LEVEL_*` constants. + pub level: u32, + /// Original [`log`] target selected by the mod. + pub target: HfmStr, + /// Formatted log message. + pub message: HfmStr, + /// Rust module path, or an empty string when unavailable. + pub module_path: HfmStr, + /// Source filename, or an empty string when unavailable. + pub file: HfmStr, + /// One-based source line, or zero when unavailable. + pub line: u32, + /// Must be zero. + pub reserved: u32, +} + +/// Returns nonzero when a record at this level and target should be formatted. +pub type HfmLogEnabledV1 = + unsafe extern "C" fn(context: *mut c_void, level: u32, target: HfmStr) -> u8; + +/// Writes one log record synchronously. +/// +/// The host must copy any data it needs before this function returns. +pub type HfmLogWriteV1 = unsafe extern "C" fn(context: *mut c_void, record: *const HfmLogRecordV1); + +/// Flushes any buffered log output. +pub type HfmLogFlushV1 = unsafe extern "C" fn(context: *mut c_void); + +/// Version 1 of the services supplied by the Highfleet modloader. +/// +/// The mod copies this table during setup; the table itself does not need to +/// remain alive. `context`, and the code behind every callback, must remain +/// valid and safe to call from any mod thread for as long as the mod can log. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct HfmHostApiV1 { + /// Must be [`HFM_HOST_ABI_VERSION_1`]. + pub abi_version: u32, + /// Size of this table in bytes. + pub struct_size: u32, + /// Opaque, loader-owned context identifying this mod. + pub context: *mut c_void, + /// Optional filter callback. When null, all levels are considered enabled. + pub log_enabled: Option, + /// Required record callback. + pub log_write: Option, + /// Optional flush callback. + pub log_flush: Option, +} + +#[cfg(test)] +mod tests { + use core::mem::{align_of, offset_of, size_of}; + + use super::*; + + #[test] + fn ffi_layout_is_pointer_width_independent() { + let pointer_size = size_of::<*const ()>(); + + assert_eq!(offset_of!(HfmStr, ptr), 0); + assert_eq!(offset_of!(HfmStr, len), pointer_size); + assert_eq!(size_of::(), pointer_size * 2); + assert_eq!(align_of::(), pointer_size); + + assert_eq!(offset_of!(HfmLogRecordV1, struct_size), 0); + assert_eq!(offset_of!(HfmLogRecordV1, level), 4); + assert_eq!(offset_of!(HfmLogRecordV1, target), 8); + assert_eq!(offset_of!(HfmLogRecordV1, message), 8 + pointer_size * 2); + assert_eq!( + offset_of!(HfmLogRecordV1, module_path), + 8 + pointer_size * 4 + ); + assert_eq!(offset_of!(HfmLogRecordV1, file), 8 + pointer_size * 6); + assert_eq!(offset_of!(HfmLogRecordV1, line), 8 + pointer_size * 8); + assert_eq!(size_of::(), 16 + pointer_size * 8); + + assert_eq!(offset_of!(HfmHostApiV1, abi_version), 0); + assert_eq!(offset_of!(HfmHostApiV1, struct_size), 4); + assert_eq!(offset_of!(HfmHostApiV1, context), 8); + assert_eq!(offset_of!(HfmHostApiV1, log_enabled), 8 + pointer_size); + assert_eq!(offset_of!(HfmHostApiV1, log_write), 8 + pointer_size * 2); + assert_eq!(offset_of!(HfmHostApiV1, log_flush), 8 + pointer_size * 3); + assert_eq!(size_of::(), 8 + pointer_size * 4); + } + + #[test] + fn optional_callbacks_have_nullable_pointer_layout() { + assert_eq!( + size_of::>(), + size_of::() + ); + assert_eq!( + size_of::>(), + size_of::() + ); + assert_eq!( + size_of::>(), + size_of::() + ); + } +} diff --git a/crates/highfleet-mod-api/src/lib.rs b/crates/highfleet-mod-api/src/lib.rs new file mode 100644 index 0000000..540a5bd --- /dev/null +++ b/crates/highfleet-mod-api/src/lib.rs @@ -0,0 +1,62 @@ +//! Stable host API definitions and Rust adapters for Highfleet mods. +//! +//! The types in [`ffi`] are the only values that cross the dynamic-library +//! boundary. Rust mods can invoke [`export_logger!`] once and continue using +//! the standard macros from the [`log`] crate. + +pub mod ffi; +mod logging; + +pub use logging::{install_logger, InstallError}; + +/// Exports the version 1 host setup function and connects the [`log`] facade +/// in the mod to the modloader. +/// +/// Invoke this macro exactly once in the root of a mod crate: +/// +/// ```no_run +/// highfleet_mod_api::export_logger!(); +/// +/// fn initialize() { +/// log::info!("Logging through the Highfleet modloader"); +/// } +/// ``` +/// +/// The generated export catches Rust panics so they cannot unwind into the +/// modloader. When an old modloader does not call the export, the mod still +/// loads normally but its [`log`] records are discarded. +#[macro_export] +macro_rules! export_logger { + () => { + /// Installs the logging callbacks supplied by the Highfleet modloader. + /// + /// # Safety + /// + /// `host` must point to a readable + /// [`highfleet_mod_api::ffi::HfmHostApiV1`] for the duration of this + /// call. The callback context and callback code must remain valid + /// afterward, for as long as this mod can emit a log record. + #[unsafe(no_mangle)] + pub unsafe extern "C" fn highfleet_mod_setup_v1( + host: *const $crate::ffi::HfmHostApiV1, + ) -> u32 { + unsafe { $crate::__setup_logging_v1(host) } + } + }; +} + +/// Implements the exported setup function without exposing implementation +/// details through the macro expansion. +/// +/// This is public because macros expanded in another crate must be able to +/// reach it. Mod code should use [`export_logger!`] instead. +#[doc(hidden)] +pub unsafe fn __setup_logging_v1(host: *const ffi::HfmHostApiV1) -> u32 { + use std::panic::{catch_unwind, AssertUnwindSafe}; + + match catch_unwind(AssertUnwindSafe(|| unsafe { install_logger(host) })) { + Ok(Ok(())) => ffi::HFM_STATUS_OK, + Ok(Err(error)) => error.status_code(), + Err(_) => ffi::HFM_STATUS_PANIC, + } +} diff --git a/crates/highfleet-mod-api/src/logging.rs b/crates/highfleet-mod-api/src/logging.rs new file mode 100644 index 0000000..4b9e886 --- /dev/null +++ b/crates/highfleet-mod-api/src/logging.rs @@ -0,0 +1,312 @@ +use std::mem::size_of; +use std::sync::OnceLock; + +use log::{Level, Log, Metadata, Record}; +use thiserror::Error; + +use crate::ffi::{ + HfmHostApiV1, HfmLogRecordV1, HfmStr, HFM_HOST_ABI_VERSION_1, HFM_LOG_LEVEL_DEBUG, + HFM_LOG_LEVEL_ERROR, HFM_LOG_LEVEL_INFO, HFM_LOG_LEVEL_TRACE, HFM_LOG_LEVEL_WARN, + HFM_STATUS_API_TOO_SMALL, HFM_STATUS_LOGGER_ALREADY_SET, HFM_STATUS_MISSING_CALLBACK, + HFM_STATUS_NULL_API, HFM_STATUS_UNSUPPORTED_ABI, +}; + +static LOGGER: OnceLock = OnceLock::new(); + +/// An error encountered while installing the modloader logger. +#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)] +pub enum InstallError { + /// The host API pointer was null. + #[error("the host API pointer is null")] + NullApi, + /// The host implements a different ABI version. + #[error("the host API version is unsupported")] + UnsupportedAbi, + /// The host table is too small to contain version 1. + #[error("the host API table is too small")] + ApiTooSmall, + /// The host did not provide the required write callback. + #[error("the host API is missing the log_write callback")] + MissingCallback, + /// This mod already installed a global logger. + #[error("this mod already installed a logger")] + LoggerAlreadySet, +} + +impl InstallError { + /// Returns the stable status code used by `highfleet_mod_setup_v1`. + pub const fn status_code(self) -> u32 { + match self { + Self::NullApi => HFM_STATUS_NULL_API, + Self::UnsupportedAbi => HFM_STATUS_UNSUPPORTED_ABI, + Self::ApiTooSmall => HFM_STATUS_API_TOO_SMALL, + Self::MissingCallback => HFM_STATUS_MISSING_CALLBACK, + Self::LoggerAlreadySet => HFM_STATUS_LOGGER_ALREADY_SET, + } + } +} + +/// Installs a [`log::Log`] implementation backed by modloader callbacks. +/// +/// The host table is copied. Its callback context and callback code must +/// nevertheless remain valid for the remainder of the mod's logging lifetime. +/// The callbacks must be thread-safe because [`log`] can invoke them from any +/// thread. +/// +/// # Safety +/// +/// `host` must be null or point to readable memory containing at least its +/// `abi_version` and `struct_size` fields. When it declares a complete version +/// 1 table, the entire [`HfmHostApiV1`] must be readable and properly aligned. +pub unsafe fn install_logger(host: *const HfmHostApiV1) -> Result<(), InstallError> { + let host = unsafe { copy_and_validate_api(host)? }; + let logger = LOGGER.get_or_init(|| HostLogger { host }); + + log::set_logger(logger).map_err(|_| InstallError::LoggerAlreadySet)?; + + // Leave the facade open to every level. The host's enabled callback owns + // dynamic filtering and can change it without reaching into this DLL. + log::set_max_level(log::LevelFilter::Trace); + Ok(()) +} + +unsafe fn copy_and_validate_api(host: *const HfmHostApiV1) -> Result { + if host.is_null() { + return Err(InstallError::NullApi); + } + + // Read the common two-field header before accessing the rest of the table. + let abi_version = unsafe { core::ptr::addr_of!((*host).abi_version).read() }; + let struct_size = unsafe { core::ptr::addr_of!((*host).struct_size).read() }; + + if abi_version != HFM_HOST_ABI_VERSION_1 { + return Err(InstallError::UnsupportedAbi); + } + if struct_size < size_of::() as u32 { + return Err(InstallError::ApiTooSmall); + } + + let host = unsafe { host.read() }; + if host.log_write.is_none() { + return Err(InstallError::MissingCallback); + } + + Ok(host) +} + +struct HostLogger { + host: HfmHostApiV1, +} + +// The host contract requires its opaque context and callbacks to remain valid +// and thread-safe for the mod's entire logging lifetime. +unsafe impl Send for HostLogger {} +unsafe impl Sync for HostLogger {} + +impl Log for HostLogger { + fn enabled(&self, metadata: &Metadata<'_>) -> bool { + let Some(enabled) = self.host.log_enabled else { + return true; + }; + + unsafe { + enabled( + self.host.context, + level_to_ffi(metadata.level()), + HfmStr::from(metadata.target()), + ) != 0 + } + } + + fn log(&self, record: &Record<'_>) { + if !self.enabled(record.metadata()) { + return; + } + + let message = record.args().to_string(); + let ffi_record = HfmLogRecordV1 { + struct_size: size_of::() as u32, + level: level_to_ffi(record.level()), + target: HfmStr::from(record.target()), + message: HfmStr::from(message.as_str()), + module_path: record.module_path().map(HfmStr::from).unwrap_or_default(), + file: record.file().map(HfmStr::from).unwrap_or_default(), + line: record.line().unwrap_or(0), + reserved: 0, + }; + + // Validation during installation guarantees that this is present. + let write = self.host.log_write.expect("validated log_write callback"); + unsafe { + write(self.host.context, &ffi_record); + } + } + + fn flush(&self) { + if let Some(flush) = self.host.log_flush { + unsafe { + flush(self.host.context); + } + } + } +} + +const fn level_to_ffi(level: Level) -> u32 { + match level { + Level::Error => HFM_LOG_LEVEL_ERROR, + Level::Warn => HFM_LOG_LEVEL_WARN, + Level::Info => HFM_LOG_LEVEL_INFO, + Level::Debug => HFM_LOG_LEVEL_DEBUG, + Level::Trace => HFM_LOG_LEVEL_TRACE, + } +} + +#[cfg(test)] +mod tests { + use std::ffi::c_void; + use std::sync::Mutex; + + use log::Level; + + use super::*; + + #[derive(Debug, Eq, PartialEq)] + struct CapturedRecord { + level: u32, + target: String, + message: String, + module_path: String, + file: String, + line: u32, + } + + struct Capture { + enabled: bool, + records: Mutex>, + flushes: Mutex, + } + + unsafe extern "C" fn enabled(context: *mut c_void, _level: u32, _target: HfmStr) -> u8 { + let capture = unsafe { &*(context.cast::()) }; + capture.enabled.into() + } + + unsafe extern "C" fn write(context: *mut c_void, record: *const HfmLogRecordV1) { + let capture = unsafe { &*(context.cast::()) }; + let record = unsafe { &*record }; + capture.records.lock().unwrap().push(CapturedRecord { + level: record.level, + target: unsafe { copy_string(record.target) }, + message: unsafe { copy_string(record.message) }, + module_path: unsafe { copy_string(record.module_path) }, + file: unsafe { copy_string(record.file) }, + line: record.line, + }); + } + + unsafe extern "C" fn flush(context: *mut c_void) { + let capture = unsafe { &*(context.cast::()) }; + *capture.flushes.lock().unwrap() += 1; + } + + unsafe fn copy_string(value: HfmStr) -> String { + if value.len == 0 { + return String::new(); + } + let bytes = unsafe { std::slice::from_raw_parts(value.ptr, value.len) }; + std::str::from_utf8(bytes).unwrap().to_owned() + } + + fn host_for(capture: &mut Capture) -> HfmHostApiV1 { + HfmHostApiV1 { + abi_version: HFM_HOST_ABI_VERSION_1, + struct_size: size_of::() as u32, + context: (capture as *mut Capture).cast(), + log_enabled: Some(enabled), + log_write: Some(write), + log_flush: Some(flush), + } + } + + #[test] + fn forwards_formatted_records_and_source_metadata() { + let mut capture = Capture { + enabled: true, + records: Mutex::new(Vec::new()), + flushes: Mutex::new(0), + }; + let logger = HostLogger { + host: host_for(&mut capture), + }; + logger.log( + &Record::builder() + .args(format_args!("value = {}", 42)) + .level(Level::Warn) + .target("qol::config") + .module_path(Some("qol::config")) + .file(Some("src/config.rs")) + .line(Some(27)) + .build(), + ); + logger.flush(); + + assert_eq!( + *capture.records.lock().unwrap(), + vec![CapturedRecord { + level: HFM_LOG_LEVEL_WARN, + target: "qol::config".to_owned(), + message: "value = 42".to_owned(), + module_path: "qol::config".to_owned(), + file: "src/config.rs".to_owned(), + line: 27, + }] + ); + assert_eq!(*capture.flushes.lock().unwrap(), 1); + } + + #[test] + fn disabled_records_are_not_formatted_or_written() { + let mut capture = Capture { + enabled: false, + records: Mutex::new(Vec::new()), + flushes: Mutex::new(0), + }; + let logger = HostLogger { + host: host_for(&mut capture), + }; + let arguments = format_args!("discarded"); + let record = Record::builder().args(arguments).level(Level::Info).build(); + + logger.log(&record); + + assert!(capture.records.lock().unwrap().is_empty()); + } + + #[test] + fn rejects_invalid_api_headers_without_installing() { + assert_eq!( + unsafe { copy_and_validate_api(core::ptr::null()) }.unwrap_err(), + InstallError::NullApi + ); + + let mut too_old = HfmHostApiV1 { + abi_version: 99, + struct_size: size_of::() as u32, + context: core::ptr::null_mut(), + log_enabled: None, + log_write: Some(write), + log_flush: None, + }; + assert_eq!( + unsafe { copy_and_validate_api(&too_old) }.unwrap_err(), + InstallError::UnsupportedAbi + ); + + too_old.abi_version = HFM_HOST_ABI_VERSION_1; + too_old.struct_size = 8; + assert_eq!( + unsafe { copy_and_validate_api(&too_old) }.unwrap_err(), + InstallError::ApiTooSmall + ); + } +} From 3fede9954b3b88e575efbc348280fad63f10371c Mon Sep 17 00:00:00 2001 From: Timoteo Rider Date: Wed, 26 Aug 2026 22:30:42 -0600 Subject: [PATCH 2/6] Route mod logs through stable host API --- Cargo.toml | 1 + README.md | 22 +++- src/host_logging.rs | 274 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/loader.rs | 15 +-- 5 files changed, 299 insertions(+), 15 deletions(-) create mode 100644 src/host_logging.rs diff --git a/Cargo.toml b/Cargo.toml index 2b71a7f..962deb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ proxygen-macros = "0.5.1" libc = "0.2.149" libloading = "0.8" log = "0.4.*" +highfleet-mod-api = { path = "crates/highfleet-mod-api" } sha2 = "0.10.*" flexi_logger = "0.29.0" winapi = { version = "0.3.9", features = [ diff --git a/README.md b/README.md index 1a2d42d..0418755 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,24 @@ The versions that can be passed in are: If your mod implements this function, it should return either true or false depending on if your mod supports the given version. You **may** choose to not initialize your mod if you do not support the game version. -#### `setup_logger(&Log, LevelFilter)` -The modloader passes in a reference to a logger object and the LevelFilter so that the mod can configure the logger from their end. -Logs will be outputed in the main modloader terminal window. +#### Logging +Rust mods can use the standard `log` macros while sending their output through the modloader's console and log files. Add the API and `log` crates to the mod: -This function can only be used from Rust based mods using the same compiler version as is used with the modloader. -I'm currently working on a FFI safe implementation which will likely require linking to a shared logging library. +```toml +[dependencies] +highfleet-mod-api = { git = "https://github.com/logdot/Highfleet-Modloader.git" } +log = "0.4" +``` + +Then export the logging bridge once from the mod's library root: + +```rust +highfleet_mod_api::export_logger!(); +``` + +Calls such as `log::error!`, `log::warn!`, and `log::info!` then use the modloader's formatting and filtering. The bridge uses a versioned C ABI; no Rust trait objects or Rust-owned strings cross the DLL boundary. + +Logging remains optional for compatibility. Old mods without the bridge continue to load in new modloaders without logging, and new mods continue to load in old modloaders without logging. #### `init() -> bool` The modloader calls this function last. diff --git a/src/host_logging.rs b/src/host_logging.rs new file mode 100644 index 0000000..0d1139c --- /dev/null +++ b/src/host_logging.rs @@ -0,0 +1,274 @@ +use std::ffi::c_void; +use std::io::Write; +use std::mem::size_of; +use std::path::Path; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; + +use flexi_logger::DeferredNow; +use highfleet_mod_api::ffi::{ + HfmHostApiV1, HfmLogRecordV1, HfmStr, HFM_HOST_ABI_VERSION_1, HFM_LOG_LEVEL_DEBUG, + HFM_LOG_LEVEL_ERROR, HFM_LOG_LEVEL_INFO, HFM_LOG_LEVEL_TRACE, HFM_LOG_LEVEL_WARN, + HFM_STATUS_OK, +}; +use libloading::Library; +use log::{debug, error, warn, Level, Metadata, Record}; + +const MOD_TARGET_PREFIX: &str = "highfleet_mod::"; +const MAX_LOG_STRING_BYTES: usize = 1024 * 1024; + +type SetupModV1 = unsafe extern "C" fn(host: *const HfmHostApiV1) -> u32; + +struct ModLogContext { + name: String, + target: String, + enabled: AtomicBool, + max_level: AtomicU8, +} + +impl ModLogContext { + fn from_path(path: &Path) -> Self { + let name = path + .file_stem() + .and_then(|name| name.to_str()) + .unwrap_or("unknown-mod"); + let name = sanitize_name(name); + + Self { + target: format!("{MOD_TARGET_PREFIX}{name}"), + name, + enabled: AtomicBool::new(true), + max_level: AtomicU8::new(HFM_LOG_LEVEL_TRACE as u8), + } + } + + fn allows(&self, level: Level) -> bool { + if !self.enabled.load(Ordering::Relaxed) + || level_to_ffi(level) > u32::from(self.max_level.load(Ordering::Relaxed)) + || level > log::max_level() + { + return false; + } + + log::logger().enabled( + &Metadata::builder() + .level(level) + .target(&self.target) + .build(), + ) + } +} + +/// Installs the host logging API in a loaded mod when it exports the version 1 +/// setup function. +/// +/// # Safety +/// +/// `library` must represent a loaded mod and must not be unloaded concurrently +/// while its setup function is being resolved and called. +pub unsafe fn setup_mod_logging(library: &Library, path: &Path) { + let setup = match unsafe { library.get::(b"highfleet_mod_setup_v1") } { + Ok(setup) => setup, + Err(error) => { + warn!( + "Mod {} does not expose highfleet_mod_setup_v1; mod logging is unavailable: {}", + path.display(), + error + ); + return; + } + }; + + // The callbacks can be retained by the mod's global logger, so the context + // intentionally has process lifetime, just like the loaded mod library. + let context: &'static ModLogContext = Box::leak(Box::new(ModLogContext::from_path(path))); + let host = HfmHostApiV1 { + abi_version: HFM_HOST_ABI_VERSION_1, + struct_size: size_of::() as u32, + context: (context as *const ModLogContext).cast_mut().cast(), + log_enabled: Some(mod_log_enabled), + log_write: Some(mod_log_write), + log_flush: Some(mod_log_flush), + }; + + let status = unsafe { setup(&host) }; + if status == HFM_STATUS_OK { + debug!("Configured modloader logging for {}", context.name); + } else { + context.enabled.store(false, Ordering::Relaxed); + error!( + "Mod {} rejected the version 1 host logging API with status {}", + context.name, status + ); + } +} + +pub fn format_log( + write: &mut dyn Write, + now: &mut DeferredNow, + record: &Record<'_>, +) -> Result<(), std::io::Error> { + let origin = record + .target() + .strip_prefix(MOD_TARGET_PREFIX) + .unwrap_or("modloader"); + + write!( + write, + "{} [{}] {} {}", + now.format("%Y-%m-%dT%H:%M:%S"), + origin, + record.level(), + record.args() + ) +} + +unsafe extern "C" fn mod_log_enabled(context: *mut c_void, level: u32, _target: HfmStr) -> u8 { + std::panic::catch_unwind(|| unsafe { + context_from_ptr(context) + .zip(level_from_ffi(level)) + .is_some_and(|(context, level)| context.allows(level)) + }) + .unwrap_or(false) + .into() +} + +unsafe extern "C" fn mod_log_write(context: *mut c_void, record: *const HfmLogRecordV1) { + let _ = std::panic::catch_unwind(|| unsafe { + route_mod_record(context, record); + }); +} + +unsafe extern "C" fn mod_log_flush(_context: *mut c_void) { + let _ = std::panic::catch_unwind(|| { + log::logger().flush(); + }); +} + +unsafe fn route_mod_record(context: *mut c_void, record: *const HfmLogRecordV1) { + let Some(context) = (unsafe { context_from_ptr(context) }) else { + return; + }; + if record.is_null() { + return; + } + + let struct_size = unsafe { core::ptr::addr_of!((*record).struct_size).read() }; + if struct_size < size_of::() as u32 { + return; + } + + let record = unsafe { &*record }; + let Some(level) = level_from_ffi(record.level) else { + return; + }; + if !context.allows(level) { + return; + } + + let Some(message) = (unsafe { read_ffi_str(record, record.message) }) else { + return; + }; + let module_path = + unsafe { read_ffi_str(record, record.module_path) }.filter(|value| !value.is_empty()); + let file = unsafe { read_ffi_str(record, record.file) }.filter(|value| !value.is_empty()); + let line = (record.line != 0).then_some(record.line); + + log::logger().log( + &Record::builder() + .args(format_args!("{}", message)) + .level(level) + .target(&context.target) + .module_path(module_path) + .file(file) + .line(line) + .build(), + ); +} + +unsafe fn context_from_ptr(context: *mut c_void) -> Option<&'static ModLogContext> { + if context.is_null() { + return None; + } + + Some(unsafe { &*context.cast::() }) +} + +unsafe fn read_ffi_str(_record: &HfmLogRecordV1, value: HfmStr) -> Option<&str> { + if value.len == 0 { + return Some(""); + } + if value.ptr.is_null() || value.len > MAX_LOG_STRING_BYTES { + return None; + } + + let bytes = unsafe { std::slice::from_raw_parts(value.ptr, value.len) }; + std::str::from_utf8(bytes).ok() +} + +fn sanitize_name(name: &str) -> String { + name.chars() + .map(|character| { + if character.is_control() || matches!(character, '[' | ']') { + '_' + } else { + character + } + }) + .collect() +} + +const fn level_from_ffi(level: u32) -> Option { + match level { + HFM_LOG_LEVEL_ERROR => Some(Level::Error), + HFM_LOG_LEVEL_WARN => Some(Level::Warn), + HFM_LOG_LEVEL_INFO => Some(Level::Info), + HFM_LOG_LEVEL_DEBUG => Some(Level::Debug), + HFM_LOG_LEVEL_TRACE => Some(Level::Trace), + _ => None, + } +} + +const fn level_to_ffi(level: Level) -> u32 { + match level { + Level::Error => HFM_LOG_LEVEL_ERROR, + Level::Warn => HFM_LOG_LEVEL_WARN, + Level::Info => HFM_LOG_LEVEL_INFO, + Level::Debug => HFM_LOG_LEVEL_DEBUG, + Level::Trace => HFM_LOG_LEVEL_TRACE, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sanitizes_names_that_could_break_the_log_prefix() { + assert_eq!(sanitize_name("qol[beta]\n"), "qol_beta__"); + } + + #[test] + fn maps_only_known_log_levels() { + assert_eq!(level_from_ffi(HFM_LOG_LEVEL_ERROR), Some(Level::Error)); + assert_eq!(level_from_ffi(HFM_LOG_LEVEL_TRACE), Some(Level::Trace)); + assert_eq!(level_from_ffi(0), None); + assert_eq!(level_from_ffi(u32::MAX), None); + } + + #[test] + fn formats_mod_name_and_level_centrally() { + let mut output = Vec::new(); + let mut now = DeferredNow::new(); + let arguments = format_args!("configuration loaded"); + let record = Record::builder() + .args(arguments) + .level(Level::Info) + .target("highfleet_mod::highfleet-qol") + .build(); + + format_log(&mut output, &mut now, &record).unwrap(); + + let output = String::from_utf8(output).unwrap(); + assert!(output.ends_with("[highfleet-qol] INFO configuration loaded")); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8e602ac..1fcc2a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ #![allow(non_snake_case)] mod export_indices; +mod host_logging; mod intercepted_exports; mod orig_exports; mod proxied_exports; @@ -138,6 +139,7 @@ unsafe extern "system" fn init(_: *mut c_void) -> u32 { .log_to_file(FileSpec::default().directory("Modloader/logs")) .write_mode(WriteMode::BufferAndFlush) .duplicate_to_stderr(Duplicate::Info) + .format(host_logging::format_log) .start() .expect("Failed to start logger"); diff --git a/src/loader.rs b/src/loader.rs index ee97cff..1367251 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -1,7 +1,9 @@ use std::{ffi::CString, fs, os::raw::c_char, path::PathBuf}; use libloading::Library; -use log::{debug, error, info, warn, LevelFilter, Log}; +use log::{debug, error, info, warn}; + +use crate::host_logging; const MOD_FOLDER: &str = "./Modloader/mods"; const CONFIG_FOLDER: &str = "./Modloader/config"; @@ -64,6 +66,8 @@ fn load_mod(path: &PathBuf, version: &str) { } }; + host_logging::setup_mod_logging(&library, path); + match library.get:: bool>(b"version") { Ok(version_func) => { let cstr = CString::new(version).unwrap(); @@ -77,15 +81,6 @@ fn load_mod(path: &PathBuf, version: &str) { } }; - match library.get:: bool>(b"setup_logger") { - Ok(setup_logger) => { - setup_logger(log::logger(), log::max_level()); - }, - Err(e) => { - warn!("No setup_logger function: {}", e); - } - }; - match library.get:: bool>(b"init") { Ok(init) => { if !init() { From ae7d01ade616b2f0c66a00067b6f0ad1f0fad071 Mon Sep 17 00:00:00 2001 From: Timoteo Rider Date: Wed, 26 Aug 2026 22:46:46 -0600 Subject: [PATCH 3/6] Use modern proxygen fork --- Cargo.toml | 2 +- src/lib.rs | 6 ++---- src/loader.rs | 4 ++-- src/orig_exports.rs | 5 ++--- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 962deb6..6aaff8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -proxygen-macros = "0.5.1" +proxygen-macros = { git = "https://github.com/logdot/proxygen.git", rev = "bf8f791f5c208a750a05d8522d223d82a523ccde" } libc = "0.2.149" libloading = "0.8" log = "0.4.*" diff --git a/src/lib.rs b/src/lib.rs index 1fcc2a6..543c6d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -#![feature(naked_functions)] -#![allow(named_asm_labels)] #![allow(non_snake_case)] mod export_indices; @@ -62,7 +60,7 @@ const STRING_BUFF_SIZE: usize = 32767; /// # Safety /// Windows init #[no_mangle] -pub unsafe extern "stdcall" fn DllMain(module: HMODULE, reason: u32, _res: *const c_void) -> i32 { +pub unsafe extern "system" fn DllMain(module: HMODULE, reason: u32, _res: *const c_void) -> i32 { DisableThreadLibraryCalls(module); THIS_HANDLE = Some(module); @@ -123,7 +121,7 @@ unsafe fn show_message(title: &str, message: &str) { /// Called when the thread is spawned unsafe extern "system" fn init(_: *mut c_void) -> u32 { - ORIG_FUNCS_PTR = ORIGINAL_FUNCS.as_ptr(); + ORIG_FUNCS_PTR = (&raw const ORIGINAL_FUNCS).cast::(); AllocConsole(); let stdout = std::io::stdout(); let out_handle = stdout.as_raw_handle(); diff --git a/src/loader.rs b/src/loader.rs index 1367251..daa0ed7 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -68,7 +68,7 @@ fn load_mod(path: &PathBuf, version: &str) { host_logging::setup_mod_logging(&library, path); - match library.get:: bool>(b"version") { + match library.get:: bool>(b"version") { Ok(version_func) => { let cstr = CString::new(version).unwrap(); if !version_func(cstr.as_ptr()) { @@ -81,7 +81,7 @@ fn load_mod(path: &PathBuf, version: &str) { } }; - match library.get:: bool>(b"init") { + match library.get:: bool>(b"init") { Ok(init) => { if !init() { error!("Failed to initialize mod: {}", path.display()); diff --git a/src/orig_exports.rs b/src/orig_exports.rs index e839bbf..5776ae1 100644 --- a/src/orig_exports.rs +++ b/src/orig_exports.rs @@ -18,11 +18,10 @@ unsafe fn load_dll_func(index: usize, h_module: HMODULE, func: &str) { /// Loads the original DLL functions for later use pub unsafe fn load_dll_funcs() { debug!("Loading original DLL functions"); - if ORIG_DLL_HANDLE.is_none() { + let Some(dll_handle) = ORIG_DLL_HANDLE else { warn!("Original DLL handle is none. Cannot load original DLL funcs"); return; - } - let dll_handle = ORIG_DLL_HANDLE.unwrap(); + }; load_dll_func(Index_BASS_Apply3D, dll_handle, "BASS_Apply3D"); load_dll_func(Index_BASS_ChannelBytes2Seconds, dll_handle, "BASS_ChannelBytes2Seconds"); load_dll_func(Index_BASS_ChannelFlags, dll_handle, "BASS_ChannelFlags"); From 9a8d694c2f81b9ea1e6f4ebfc63289c49e6a0152 Mon Sep 17 00:00:00 2001 From: Timoteo Rider Date: Wed, 26 Aug 2026 23:10:51 -0600 Subject: [PATCH 4/6] Add per-mod logging configuration --- Cargo.toml | 2 + README.md | 25 +++++++++ src/host_logging.rs | 122 +++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 3 +- 4 files changed, 149 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6aaff8b..d0c4ccb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ libc = "0.2.149" libloading = "0.8" log = "0.4.*" highfleet-mod-api = { path = "crates/highfleet-mod-api" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" sha2 = "0.10.*" flexi_logger = "0.29.0" winapi = { version = "0.3.9", features = [ diff --git a/README.md b/README.md index 0418755..03d3481 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,31 @@ If you need to install a mod manually, drag and drop it into the `Modloader/mods If the mod comes with configuration files, drag and drop it into the `Modloader/config` folder. Note that some mods may auto generate their config files. +## Logging configuration + +The modloader writes logs to `Modloader/logs` and duplicates `info`, `warn`, and `error` records to its console. It creates `Modloader/config/logging.json` on first launch: + +```json +{ + "default_level": "debug", + "mods": {} +} +``` + +Use a mod DLL's filename without the `.dll` extension to override its log level: + +```json +{ + "default_level": "info", + "mods": { + "highfleet-qol": "debug", + "noisy-mod": "off" + } +} +``` + +Names are matched case-insensitively. Valid levels are `off`, `error`, `warn`, `info`, `debug`, and `trace`. Changes take effect the next time the game starts. + ## Developing mods Thanks to the nature of the modloader you can develop almost any DLL and it will be injected into the game. This means you have almost absolute control over the game from within it's own process. diff --git a/src/host_logging.rs b/src/host_logging.rs index 0d1139c..51b1180 100644 --- a/src/host_logging.rs +++ b/src/host_logging.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; use std::ffi::c_void; +use std::fs; use std::io::Write; use std::mem::size_of; use std::path::Path; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::OnceLock; use flexi_logger::DeferredNow; use highfleet_mod_api::ffi::{ @@ -12,12 +15,55 @@ use highfleet_mod_api::ffi::{ }; use libloading::Library; use log::{debug, error, warn, Level, Metadata, Record}; +use serde::{Deserialize, Serialize}; const MOD_TARGET_PREFIX: &str = "highfleet_mod::"; const MAX_LOG_STRING_BYTES: usize = 1024 * 1024; +const LOGGING_CONFIG_PATH: &str = "./Modloader/config/logging.json"; type SetupModV1 = unsafe extern "C" fn(host: *const HfmHostApiV1) -> u32; +static LOGGING_CONFIG: OnceLock = OnceLock::new(); + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[repr(u8)] +#[serde(rename_all = "lowercase")] +enum ModLogLevel { + Off = 0, + Error = HFM_LOG_LEVEL_ERROR as u8, + Warn = HFM_LOG_LEVEL_WARN as u8, + Info = HFM_LOG_LEVEL_INFO as u8, + Debug = HFM_LOG_LEVEL_DEBUG as u8, + Trace = HFM_LOG_LEVEL_TRACE as u8, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(default)] +struct LoggingConfig { + default_level: ModLogLevel, + mods: HashMap, +} + +impl Default for LoggingConfig { + fn default() -> Self { + Self { + default_level: ModLogLevel::Debug, + mods: HashMap::new(), + } + } +} + +impl LoggingConfig { + fn level_for(&self, name: &str) -> ModLogLevel { + self.mods + .iter() + .find_map(|(configured_name, level)| { + configured_name.eq_ignore_ascii_case(name).then_some(*level) + }) + .unwrap_or(self.default_level) + } +} + struct ModLogContext { name: String, target: String, @@ -32,12 +78,13 @@ impl ModLogContext { .and_then(|name| name.to_str()) .unwrap_or("unknown-mod"); let name = sanitize_name(name); + let level = logging_config().level_for(&name); Self { target: format!("{MOD_TARGET_PREFIX}{name}"), name, - enabled: AtomicBool::new(true), - max_level: AtomicU8::new(HFM_LOG_LEVEL_TRACE as u8), + enabled: AtomicBool::new(level != ModLogLevel::Off), + max_level: AtomicU8::new(level as u8), } } @@ -58,6 +105,51 @@ impl ModLogContext { } } +pub fn load_config() { + let config = match fs::read_to_string(LOGGING_CONFIG_PATH) { + Ok(contents) => match serde_json::from_str(&contents) { + Ok(config) => config, + Err(error) => { + error!( + "Failed to parse logging config at {}: {}. Using defaults", + LOGGING_CONFIG_PATH, error + ); + LoggingConfig::default() + } + }, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + let config = LoggingConfig::default(); + match serde_json::to_string_pretty(&config) + .map_err(|error| error.to_string()) + .and_then(|contents| { + fs::write(LOGGING_CONFIG_PATH, contents).map_err(|error| error.to_string()) + }) { + Ok(()) => debug!("Created default logging config at {}", LOGGING_CONFIG_PATH), + Err(error) => error!( + "Failed to create default logging config at {}: {}", + LOGGING_CONFIG_PATH, error + ), + } + config + } + Err(error) => { + error!( + "Failed to read logging config at {}: {}. Using defaults", + LOGGING_CONFIG_PATH, error + ); + LoggingConfig::default() + } + }; + + if LOGGING_CONFIG.set(config).is_err() { + warn!("Logging config was already loaded; keeping the existing settings"); + } +} + +fn logging_config() -> &'static LoggingConfig { + LOGGING_CONFIG.get_or_init(LoggingConfig::default) +} + /// Installs the host logging API in a loaded mod when it exports the version 1 /// setup function. /// @@ -255,6 +347,32 @@ mod tests { assert_eq!(level_from_ffi(u32::MAX), None); } + #[test] + fn resolves_mod_levels_case_insensitively() { + let config: LoggingConfig = serde_json::from_str( + r#"{ + "default_level": "warn", + "mods": { + "Highfleet-QOL": "trace", + "disabled-mod": "off" + } + }"#, + ) + .unwrap(); + + assert_eq!(config.level_for("highfleet-qol"), ModLogLevel::Trace); + assert_eq!(config.level_for("DISABLED-MOD"), ModLogLevel::Off); + assert_eq!(config.level_for("another-mod"), ModLogLevel::Warn); + } + + #[test] + fn missing_config_fields_use_defaults() { + let config: LoggingConfig = serde_json::from_str("{}").unwrap(); + + assert_eq!(config.default_level, ModLogLevel::Debug); + assert!(config.mods.is_empty()); + } + #[test] fn formats_mod_name_and_level_centrally() { let mut output = Vec::new(); diff --git a/src/lib.rs b/src/lib.rs index 543c6d6..36c8f79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,7 +133,7 @@ unsafe extern "system" fn init(_: *mut c_void) -> u32 { SetStdHandle(STD_ERROR_HANDLE, err_handle); // Start logger - Logger::try_with_env_or_str("debug").expect("Failed configuring logger") + Logger::try_with_env_or_str("debug,highfleet_mod=trace").expect("Failed configuring logger") .log_to_file(FileSpec::default().directory("Modloader/logs")) .write_mode(WriteMode::BufferAndFlush) .duplicate_to_stderr(Duplicate::Info) @@ -166,6 +166,7 @@ unsafe extern "system" fn init(_: *mut c_void) -> u32 { PROXYGEN_READY = true; create_folders(); + host_logging::load_config(); let version = match get_version() { Ok(string) => { From 517b52b5490dead55ea7772e9e3eb4e38b9c019f Mon Sep 17 00:00:00 2001 From: Timoteo Rider Date: Wed, 26 Aug 2026 23:15:36 -0600 Subject: [PATCH 5/6] Add Windows CI workflow --- .cargo/{config => config.toml} | 0 .github/workflows/ci.yml | 62 ++++ .gitignore | 1 - Cargo.lock | 609 +++++++++++++++++++++++++++++++++ 4 files changed, 671 insertions(+), 1 deletion(-) rename .cargo/{config => config.toml} (100%) create mode 100644 .github/workflows/ci.yml create mode 100644 Cargo.lock diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..939d38e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: Check, test, and build + runs-on: windows-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@nightly + with: + components: clippy, rustfmt + + - name: Check API formatting + run: cargo fmt -p highfleet-mod-api -- --check + + - name: Lint API + run: cargo clippy --locked -p highfleet-mod-api --all-targets -- -D warnings + + - name: Test workspace + run: cargo test --workspace --locked + + - name: Check workspace + run: cargo check --workspace --locked + + - name: Build release DLL + run: cargo build --release -p highfleet-loader --locked + + - name: Stage artifact + shell: pwsh + run: | + New-Item -ItemType Directory -Force artifact + Copy-Item target\x86_64-pc-windows-msvc\release\bass.dll artifact\ + Copy-Item README.md, LICENSE artifact\ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: highfleet-modloader-${{ github.sha }} + path: artifact + if-no-files-found: error + retention-days: 14 diff --git a/.gitignore b/.gitignore index ff0d847..81cf465 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target /.vscode -Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5536426 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,609 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flexi_logger" +version = "0.29.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a5a6882b2e137c4f2664562995865084eb5a00611fba30c582ef10354c4ad8" +dependencies = [ + "chrono", + "log", + "nu-ansi-term", + "regex", + "thiserror", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "highfleet-loader" +version = "0.1.0" +dependencies = [ + "flexi_logger", + "highfleet-mod-api", + "libc", + "libloading", + "log", + "proxygen-macros", + "serde", + "serde_json", + "sha2", + "winapi", +] + +[[package]] +name = "highfleet-mod-api" +version = "0.1.0" +dependencies = [ + "log", + "thiserror", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proxygen-macros" +version = "0.5.1" +source = "git+https://github.com/logdot/proxygen.git?rev=bf8f791f5c208a750a05d8522d223d82a523ccde#bf8f791f5c208a750a05d8522d223d82a523ccde" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" From fb0e6190a9bc2687f2c2f9a30d72aad723bacf90 Mon Sep 17 00:00:00 2001 From: Timoteo Rider Date: Wed, 26 Aug 2026 23:20:43 -0600 Subject: [PATCH 6/6] Update CI actions to Node 24 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 939d38e..034ff6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install Rust uses: dtolnay/rust-toolchain@nightly @@ -54,7 +54,7 @@ jobs: Copy-Item README.md, LICENSE artifact\ - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: highfleet-modloader-${{ github.sha }} path: artifact