diff --git a/Cargo.lock b/Cargo.lock index 8ea5eb8..4b84ed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,7 @@ version = "0.1.0" dependencies = [ "bindgen", "libc", + "log", ] [[package]] @@ -110,9 +111,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" diff --git a/Cargo.toml b/Cargo.toml index 36d3a21..bb3f2f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] libc = "0.2.144" +log = { version = "0.4.27", features = ["release_max_level_info"] } [build-dependencies] bindgen = "0.71.0" diff --git a/Dockerfile b/Dockerfile index 1a5a968..92019d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ RUN set -eux; \ # And to keep that to a minimum, we still delete the stuff we don't need. rm -rf /root/.xwin-cache; -FROM rust:slim-bullseye AS final +FROM rust:1.86.0-slim-bullseye AS final ARG LLVM_VERSION=18 ENV KEYRINGS=/usr/local/share/keyrings @@ -37,6 +37,7 @@ RUN set -eux; \ apt-get install -y --no-install-recommends \ ca-certificates \ gpg \ + make \ ; \ mkdir -p $KEYRINGS; \ gpg --dearmor > $KEYRINGS/llvm.gpg < /root/llvm-snapshot.gpg.key; \ @@ -69,7 +70,6 @@ RUN set -eux; \ update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++ 100; \ update-alternatives --install /usr/bin/ld ld /usr/bin/ld.lld 100; \ rustup target add x86_64-pc-windows-msvc; \ - rustup default stable-x86_64-pc-windows-msvc; \ rustup component add rust-src; \ rustup update; \ apt-get remove -y --auto-remove \ diff --git a/Justfile b/Justfile index 4825633..723421a 100644 --- a/Justfile +++ b/Justfile @@ -1,16 +1,103 @@ +set dotenv-load +set dotenv-filename := '.envrc' +set dotenv-required +set allow-duplicate-variables + fly_target := "main" +image_name := "dt-plugin-builder" + +steam_library := env("steam_library") +game_dir := steam_library / "steamapps" / "common" / "Warhammer 40,000 DARKTIDE" +log_dir := env("APPDATA") / "Fatshark" / "Darktide" / "console_logs" + +game_dir := if path_exists(game_dir) == true { + game_dir +} else { + error("Game directory does not exist, please set correct library path: " + game_dir) +} + +proton_dir := home_directory() / ".steam" / "steam" + +export STEAM_COMPAT_DATA_PATH := steam_library / "steamapps" / "compatdata" / "1361210" +export SteamAppId := "1361210" + +release := env("RELEASE", "false") image: - docker build -t dt-plugin-builder . + docker build -t {{image_name}} . -build: +# Compile the Windows DLL through a Docker image +[unix] +compile: docker run \ --rm \ -t \ --user "$(id -u):$(id -g)" \ + -v $HOME/.cargo/registry:/usr/local/cargo/registry \ + -v $HOME/.cargo/git:/usr/local/cargo/git \ + -v $HOME/.rustup/toolchains:/usr/local/rustup/toolchains \ -v ./:/src/plugin \ - dt-plugin-builder \ - cargo build -Zbuild-std --target x86_64-pc-windows-msvc + {{image_name}} \ + cargo build -Zbuild-std --target x86_64-pc-windows-msvc {{ if release != "false" { "--release" } else { "" } }} + +# Compile the DLL and install it to the game's plugin directory +[unix] +build: compile + cp target/x86_64-pc-windows-msvc/{{ if release != "false" { "release" } else { "debug" } }}/dt_p2p.dll "{{game_dir}}/binaries/plugins/dt_p2p_pluginw64_release.dll" + +# Open the newest game log file +[unix] +log: + #!/bin/sh + set -e + file="$(\ls "{{log_dir}}" | tail -1)" + echo "$(tput bold)Opening log file $file$(tput sgr0)" + exec bat "{{log_dir}}/$file" + +# Run the game through Proton +[unix] +game: + #!/bin/sh + set -e + cd "{{game_dir / "binaries"}}" + proton waitforexitandrun ./Darktide.exe \ + -eac-untrusted \ + --bundle-dir ../bundle \ + --ini settings \ + --backend-auth-service-url https://bsp-auth-prod.atoma.cloud \ + --backend-title-service-url https://bsp-td-prod.atoma.cloud \ + -game \ + -launcher_verification_passed_crashify_property false + +# Run the game +# +# Untested! +[windows] +game: + #!cmd.exe + cd "{{game_dir / "binaries"}}" + ./Darktide.exe \ + -eac-untrusted \ + --bundle-dir ../bundle \ + --ini settings \ + --backend-auth-service-url https://bsp-auth-prod.atoma.cloud \ + --backend-title-service-url https://bsp-td-prod.atoma.cloud \ + -game \ + -launcher_verification_passed_crashify_property false + +# Compile the plugin through a Docker image +# +# Make sure to run `just image` first, to build the image. +# Untested: I don't know, if this works with Windows' Docker Desktop. +# I also don't know if/where cargo and rustup keep their caches on Windows. +[windows] +compile-docker: + docker run \ + --rm \ + -t \ + -v ./:/src/plugin \ + {{image_name}} \ + cargo build -Zbuild-std --target x86_64-pc-windows-msvc {{ if release != "false" { "--release" } else { "" } }} set-base-pipeline: fly -t {{fly_target}} set-pipeline \ diff --git a/src/lib.rs b/src/lib.rs index 9cdc2b7..9a6f298 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,12 @@ use std::ffi::{c_char, CString}; use std::sync::OnceLock; +mod lua; mod plugin; mod stingray_sdk; use plugin::Plugin; -use stingray_sdk::{GetApiFunction, PluginApi, PluginApiID}; +use stingray_sdk::{GetApiFunction, LoggingApi, LuaApi, PluginApi, PluginApiID}; // TODO: Change these /// The name that the plugin is registered to the engine as. @@ -15,6 +16,20 @@ pub const PLUGIN_NAME: &str = "dt-plugin-template"; pub const MODULE_NAME: &str = "DTPluginTemplate"; static PLUGIN: OnceLock = OnceLock::new(); +static LOGGER: OnceLock = OnceLock::new(); +static LUA: OnceLock = OnceLock::new(); + +/// A macro to make accessing global statics a little more convenient. +#[macro_export] +macro_rules! global { + ($name:ident) => {{ + if cfg!(debug_assertions) { + $name.get().expect("global has not been initialized") + } else { + unsafe { $name.get().unwrap_unchecked() } + } + }}; +} #[no_mangle] pub extern "C" fn get_name() -> *const c_char { @@ -24,26 +39,25 @@ pub extern "C" fn get_name() -> *const c_char { #[no_mangle] pub extern "C" fn setup_game(get_engine_api: GetApiFunction) { - let plugin = Plugin::new(get_engine_api); + let logger = LOGGER.get_or_init(|| LoggingApi::get(get_engine_api)); + log::set_logger(logger).expect("Failed to set global logger."); + log::set_max_level(log::LevelFilter::Info); + + let _ = LUA.get_or_init(|| LuaApi::get(get_engine_api)); + + let plugin = PLUGIN.get_or_init(Plugin::new); plugin.setup_game(); - PLUGIN - .set(plugin) - .expect("Failed to initalize global plugin object."); } #[no_mangle] pub extern "C" fn shutdown_game() { - // Safety: The engine ensures that `setup_game` was called before this, so `PLUGIN` has been - // initialized. - let plugin = unsafe { PLUGIN.get().unwrap_unchecked() }; + let plugin = global!(PLUGIN); plugin.shutdown_game(); } #[no_mangle] pub extern "C" fn update_game(dt: f32) { - // Safety: The engine ensures that `setup_game` was called before this, so `PLUGIN` has been - // initialized. - let plugin = unsafe { PLUGIN.get().unwrap_unchecked() }; + let plugin = global!(PLUGIN); plugin.update_game(dt); } diff --git a/src/lua.rs b/src/lua.rs new file mode 100644 index 0000000..b4ad22a --- /dev/null +++ b/src/lua.rs @@ -0,0 +1,16 @@ +use crate::stingray_sdk::lua_State; +use crate::{LUA, global}; + +pub extern "C" fn do_something(l: *mut lua_State) -> i32 { + let lua = global!(LUA); + + if let Some(name) = lua.tolstring(l, 1) { + lua.pushstring( + l, + format!("[do_something] Hello from Rust, {}", name.to_string_lossy()), + ); + 1 + } else { + 0 + } +} diff --git a/src/plugin.rs b/src/plugin.rs index ec861eb..9f85d92 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,47 +1,23 @@ -use crate::stingray_sdk::{lua_State, GetApiFunction, LoggingApi, LuaApi}; -use crate::{MODULE_NAME, PLUGIN, PLUGIN_NAME}; +use log::info; -pub(crate) struct Plugin { - pub log: LoggingApi, - pub lua: LuaApi, -} +use crate::{LUA, MODULE_NAME, PLUGIN_NAME, global, lua}; -extern "C" fn do_something(l: *mut lua_State) -> i32 { - // Safety: Plugin must have been initialized for this to be registered as module - // function. - let plugin = unsafe { PLUGIN.get().unwrap_unchecked() }; - - if let Some(name) = plugin.lua.tolstring(l, 1) { - plugin.lua.pushstring( - l, - format!("[do_something] Hello from Rust, {}", name.to_string_lossy()), - ); - 1 - } else { - 0 - } -} +pub(crate) struct Plugin {} impl Plugin { - pub fn new(get_engine_api: GetApiFunction) -> Self { - let log = LoggingApi::get(get_engine_api); - let lua = LuaApi::get(get_engine_api); - Self { log, lua } + pub fn new() -> Self { + Self {} } pub fn setup_game(&self) { - self.log.info( - PLUGIN_NAME, - format!("[setup_game] Hello, world! This is {}!", PLUGIN_NAME), - ); + info!("[setup_game] Hello, world! This is {}!", PLUGIN_NAME); - self.lua - .add_module_function(MODULE_NAME, "do_something", do_something); + let lua = global!(LUA); + lua.add_module_function(MODULE_NAME, "do_something", lua::do_something); } pub fn shutdown_game(&self) { - self.log - .info(PLUGIN_NAME, "[shutdown_game] Goodbye, world!"); + info!("[shutdown_game] Goodbye, world!"); } pub fn update_game(&self, _dt: f32) {} diff --git a/src/stingray_sdk.rs b/src/stingray_sdk.rs index 1631c5c..9fde865 100644 --- a/src/stingray_sdk.rs +++ b/src/stingray_sdk.rs @@ -18,6 +18,7 @@ pub use bindings::PluginApi; pub use bindings::PluginApiID; use bindings::lua_CFunction; pub use bindings::lua_State; +use log::Level; impl std::default::Default for PluginApi { fn default() -> Self { @@ -107,6 +108,29 @@ impl LoggingApi { } } +impl log::Log for LoggingApi { + fn enabled(&self, metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let line = if let Some(s) = record.args().as_str() { + format!("[{}] {}", record.level(), s) + } else { + format!("[{}] {}", record.level(), record.args()) + }; + + self.info(record.target(), line); + } + + // The engine does not expose a method to flush the log + fn flush(&self) {} +} + #[repr(i32)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LuaType {