Skip to content

Commit

Permalink
Add bxt_cap_separate_start
Browse files Browse the repository at this point in the history
Thanks khanghugo for doing most of the initial work.
  • Loading branch information
YaLTeR committed Feb 18, 2023
1 parent f1c6863 commit 4b4a31f
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 4 deletions.
25 changes: 24 additions & 1 deletion src/hooks/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ pub static CL_Move: Pointer<unsafe extern "C" fn()> = Pointer::empty_patterns(
]),
my_CL_Move as _,
);
pub static CL_PlayDemo_f: Pointer<unsafe extern "C" fn()> = Pointer::empty_patterns(
b"CL_PlayDemo_f\0",
// To find, search for "playdemo <demoname> <replayspeed>: plays a demo".
Patterns(&[
// 8684
pattern!(55 8B EC 81 EC 00 01 00 00 A1 ?? ?? ?? ?? 53),
]),
my_CL_PlayDemo_f as _,
);
pub static ClientDLL_DemoUpdateClientData: Pointer<unsafe extern "C" fn(*mut c_void)> =
Pointer::empty_patterns(
b"ClientDLL_DemoUpdateClientData\0",
Expand Down Expand Up @@ -823,6 +832,7 @@ static POINTERS: &[&dyn PointerTrait] = &[
&CL_Disconnect,
&CL_GameDir_f,
&CL_Move,
&CL_PlayDemo_f,
&ClientDLL_DemoUpdateClientData,
&ClientDLL_DrawTransparentTriangles,
&ClientDLL_HudRedraw,
Expand Down Expand Up @@ -1930,7 +1940,9 @@ pub mod exported {
abort_on_panic(move || {
let marker = MainThreadMarker::new();

capture::on_cl_disconnect(marker);
if !capture_video_per_demo::on_cl_disconnect(marker) {
capture::on_cl_disconnect(marker);
}

CL_Disconnect.get(marker)();
})
Expand Down Expand Up @@ -2067,6 +2079,17 @@ pub mod exported {
})
}

#[export_name = "CL_PlayDemo_f"]
pub unsafe extern "C" fn my_CL_PlayDemo_f() {
abort_on_panic(move || {
let marker = MainThreadMarker::new();

capture_video_per_demo::on_before_cl_playdemo_f(marker);
CL_PlayDemo_f.get(marker)();
capture_video_per_demo::on_after_cl_playdemo_f(marker);
})
}

#[export_name = "Cbuf_AddText"]
pub unsafe extern "C" fn my_Cbuf_AddText(text: *const c_char) {
abort_on_panic(move || {
Expand Down
8 changes: 5 additions & 3 deletions src/modules/capture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::mem;
use color_eyre::eyre::Context;

use super::cvars::CVar;
use super::Module;
use super::{capture_video_per_demo, Module};
use crate::hooks::engine::{self, con_print};
use crate::modules::commands::Command;
use crate::utils::*;
Expand Down Expand Up @@ -216,7 +216,7 @@ fn cap_start(marker: MainThreadMarker) {
cap_start_with_filename(marker, "output.mp4".to_string());
}

fn cap_start_with_filename(marker: MainThreadMarker, filename: String) {
pub fn cap_start_with_filename(marker: MainThreadMarker, filename: String) {
if !Capture.is_enabled(marker) {
return;
}
Expand Down Expand Up @@ -248,7 +248,7 @@ Stops capturing video.",
),
);

fn cap_stop(marker: MainThreadMarker) {
pub fn cap_stop(marker: MainThreadMarker) {
unsafe {
let mut state = STATE.borrow_mut(marker);
if let State::Recording(ref mut recorder) = *state {
Expand Down Expand Up @@ -279,6 +279,8 @@ fn cap_stop(marker: MainThreadMarker) {
if stopped {
con_print(marker, "Recording stopped.\n");
}

capture_video_per_demo::stop(marker);
}

pub unsafe fn capture_frame(marker: MainThreadMarker) {
Expand Down
211 changes: 211 additions & 0 deletions src/modules/capture_video_per_demo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
//! Video capture into separate videos per demo.

use std::ffi::{CStr, CString, OsStr};
use std::path::PathBuf;
use std::{fs, mem};

use super::commands::{self, Command};
use super::{capture, Module};
use crate::handler;
use crate::hooks::engine::{self, con_print};
use crate::utils::*;

pub struct CaptureVideoPerDemo;
impl Module for CaptureVideoPerDemo {
fn name(&self) -> &'static str {
"Video capture (one video per demo)"
}

fn description(&self) -> &'static str {
"Recording separate video for each demo being played."
}

fn commands(&self) -> &'static [&'static Command] {
static COMMANDS: &[&Command] = &[&BXT_CAP_SEPARATE_START];
COMMANDS
}

fn is_enabled(&self, marker: MainThreadMarker) -> bool {
capture::Capture.is_enabled(marker)
&& commands::Commands.is_enabled(marker)
&& engine::CL_PlayDemo_f.is_set(marker)
&& engine::cls_demos.is_set(marker)
}
}

static BXT_CAP_SEPARATE_START: Command = Command::new(
b"bxt_cap_separate_start\0",
handler!(
"bxt_cap_separate_start [directory]
Starts recording every demo into its own separate video with the same file name. If directory is
provided, stores the videos in that directory, otherwise stores them in the same directory as the
corresponding demo file.
This command should be coupled with `playdemo`, `bxt_play_run` or `bxt_play_folder`. For example:
`bxt_play_folder my_folder_with_demos;bxt_cap_separate_start`.
Use `bxt_cap_stop` to stop the recording.",
cap_separate_start as fn(_),
cap_separate_start_with_dir as fn(_, _)
),
);

/// Name of the demo currently being played back.
static CURRENT_DEMO: MainThreadRefCell<Option<CString>> = MainThreadRefCell::new(None);
/// Name of the demo about to be played back.
///
/// Necessary because CL_PlayDemo_f() calls CL_Disconnect().
static CURRENT_DEMO_PENDING: MainThreadRefCell<Option<CString>> = MainThreadRefCell::new(None);
/// Whether the module is active.
///
/// If `true`, playing a new demo will start a new video recording.
static IS_ACTIVE: MainThreadCell<bool> = MainThreadCell::new(false);
/// Target directory to save recorded videos.
///
/// If `None`, videos are saved in the same folders as the demos.
static TARGET_DIR: MainThreadRefCell<Option<PathBuf>> = MainThreadRefCell::new(None);

fn cap_separate_start(marker: MainThreadMarker) {
if !CaptureVideoPerDemo.is_enabled(marker) {
return;
}

*TARGET_DIR.borrow_mut(marker) = None;
IS_ACTIVE.set(marker, true);

engine::con_print(
marker,
"Demos will be recorded into videos. Use bxt_cap_stop to stop.\n",
);

// If we're already playing a demo, start capturing.
maybe_start_capture(marker);
}

fn cap_separate_start_with_dir(marker: MainThreadMarker, target_dir: PathBuf) {
if !CaptureVideoPerDemo.is_enabled(marker) {
return;
}

if let Err(err) = fs::create_dir_all(&target_dir) {
con_print(marker, &format!("Error creating output directory: {err}\n"));
return;
}

engine::con_print(
marker,
&format!(
"Demos will be recorded into videos in {}. Use bxt_cap_stop to stop.\n",
target_dir.to_string_lossy()
),
);

*TARGET_DIR.borrow_mut(marker) = Some(target_dir);
IS_ACTIVE.set(marker, true);

// If we're already playing a demo, start capturing.
maybe_start_capture(marker);
}

fn maybe_start_capture(marker: MainThreadMarker) {
let Some(current_demo) = &*CURRENT_DEMO.borrow(marker) else { return };
let current_demo = c_str_to_os_string(current_demo);
let current_demo = PathBuf::from(current_demo);

let mut output_path = PathBuf::new();
match &*TARGET_DIR.borrow(marker) {
Some(target_dir) => {
output_path.push(target_dir);
output_path.push(current_demo.file_name().unwrap_or(OsStr::new("output")));
}
None => {
if let Some(game_dir) = engine::com_gamedir.get_opt(marker) {
output_path =
PathBuf::from(unsafe { CStr::from_ptr(game_dir.cast()) }.to_str().unwrap());
}
output_path.push(current_demo);
}
}
output_path.set_extension("mp4");

let output_path = output_path.to_string_lossy().to_string();
engine::con_print(marker, &format!("Recording into {}.\n", &output_path));
capture::cap_start_with_filename(marker, output_path);
}

pub fn stop(marker: MainThreadMarker) {
IS_ACTIVE.set(marker, false);
}

pub unsafe fn on_before_cl_playdemo_f(marker: MainThreadMarker) {
if !CaptureVideoPerDemo.is_enabled(marker) {
return;
}

let Some(demo_name) = commands::Args::new(marker).nth(1) else { return };
*CURRENT_DEMO_PENDING.borrow_mut(marker) = Some(demo_name.to_owned());
}

pub unsafe fn on_after_cl_playdemo_f(marker: MainThreadMarker) {
if !CaptureVideoPerDemo.is_enabled(marker) {
return;
}

*CURRENT_DEMO.borrow_mut(marker) = mem::take(&mut *CURRENT_DEMO_PENDING.borrow_mut(marker));

{
// Safety: no engine functions are called while the reference is active.
let cls_demos = &*engine::cls_demos.get(marker);

// Has not started playing a demo.
if cls_demos.demoplayback == 0 {
return;
}
}

if IS_ACTIVE.get(marker) {
// Start recording the next demo.
maybe_start_capture(marker);
}
}

/// Returns `true` if capture::on_cl_disconnect() should be prevented.
pub unsafe fn on_cl_disconnect(marker: MainThreadMarker) -> bool {
if !CaptureVideoPerDemo.is_enabled(marker) {
return false;
}

*CURRENT_DEMO.borrow_mut(marker) = None;

if !IS_ACTIVE.get(marker) {
return false;
}

{
// Safety: no engine functions are called while the reference is active.
let cls_demos = &*engine::cls_demos.get(marker);

// Wasn't playing back a demo.
if cls_demos.demoplayback == 0 {
return true;
}
}

capture::cap_stop(marker);

// cap_stop will reset IS_ACTIVE, but we need to keep recording if there are more demos in the
// queue. Therefore check the demo queue and reset IS_ACTIVE back to true if there are more.

{
// Safety: no engine functions are called while the reference is active.
let cls_demos = &*engine::cls_demos.get(marker);

// Will play another demo right after.
if cls_demos.demonum != -1 && cls_demos.demos[0][0] != 0 {
IS_ACTIVE.set(marker, true);
}
}

true
}
2 changes: 2 additions & 0 deletions src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod cvars;
use cvars::CVar;

pub mod capture;
pub mod capture_video_per_demo;
pub mod comment_overflow_fix;
pub mod demo_playback;
pub mod disable_loading_text;
Expand Down Expand Up @@ -77,6 +78,7 @@ pub trait Module: Sync {
/// All modules.
pub static MODULES: &[&dyn Module] = &[
&capture::Capture,
&capture_video_per_demo::CaptureVideoPerDemo,
&commands::Commands,
&comment_overflow_fix::CommentOverflowFix,
&cvars::CVars,
Expand Down

0 comments on commit 4b4a31f

Please sign in to comment.