diff --git a/bxt-strafe/src/lib.rs b/bxt-strafe/src/lib.rs index b814457..2fca200 100644 --- a/bxt-strafe/src/lib.rs +++ b/bxt-strafe/src/lib.rs @@ -133,6 +133,8 @@ pub struct State { // Number of frames for [`StrafeDir::LeftRight`] or [`StrafeDir::RightLeft`] which goes from // `0` to `count - 1`. pub strafe_cycle_frame_count: u32, + // In case of yaw and pitch override, this might be useful. + pub rendered_viewangles: Vec3, } impl State { @@ -145,6 +147,7 @@ impl State { jumped: false, move_traces: ArrayVec::new(), strafe_cycle_frame_count: 0, + rendered_viewangles: Vec3::ZERO, }; rv.update_place(tracer); diff --git a/src/hooks/bxt.rs b/src/hooks/bxt.rs index 9d62fe4..ec4ddf6 100644 --- a/src/hooks/bxt.rs +++ b/src/hooks/bxt.rs @@ -141,6 +141,7 @@ pub struct OnTasPlaybackFrameData { pub strafe_cycle_frame_count: u32, pub prev_predicted_trace_fractions: [f32; 4], pub prev_predicted_trace_normal_zs: [f32; 4], + pub rendered_viewangles: [f32; 3], } unsafe extern "C" fn on_tas_playback_frame(data: OnTasPlaybackFrameData) -> c_int { diff --git a/src/hooks/engine.rs b/src/hooks/engine.rs index 55f13d1..ab89929 100644 --- a/src/hooks/engine.rs +++ b/src/hooks/engine.rs @@ -723,6 +723,14 @@ pub static SCR_DrawLoading: Pointer = Pointer::empty_pat ]), my_SCR_DrawLoading as _, ); +pub static SCR_DrawPause: Pointer = Pointer::empty_patterns( + b"SCR_DrawPause\0", + // To find, search for "cz_worldmap". You are in SCR_DrawPause(). + Patterns(&[ + pattern!(D9 05 ?? ?? ?? ?? D8 1D ?? ?? ?? ?? DF E0 F6 C4 44 7B ?? A1 ?? ?? ?? ?? 85 C0 74 ?? E8 ?? ?? ?? ?? 85), + ]), + my_SCR_DrawPause as _, +); pub static scr_fov_value: Pointer<*mut c_float> = Pointer::empty(b"scr_fov_value\0"); pub static shm: Pointer<*mut *mut dma_t> = Pointer::empty(b"shm\0"); pub static sv: Pointer<*mut c_void> = Pointer::empty(b"sv\0"); @@ -987,6 +995,7 @@ static POINTERS: &[&dyn PointerTrait] = &[ &S_PaintChannels, &S_TransferStereo16, &SCR_DrawLoading, + &SCR_DrawPause, &scr_fov_value, &shm, &sv, @@ -2191,6 +2200,19 @@ pub mod exported { }) } + #[export_name = "SCR_DrawPause"] + pub unsafe extern "C" fn my_SCR_DrawPause() { + abort_on_panic(move || { + let marker = MainThreadMarker::new(); + + if !tas_studio::should_draw_pause(marker) { + return; + } + + SCR_DrawPause.get(marker)(); + }) + } + #[export_name = "SV_Frame"] pub unsafe extern "C" fn my_SV_Frame() { abort_on_panic(move || { @@ -2376,6 +2398,7 @@ pub mod exported { let marker = MainThreadMarker::new(); campath::override_view(marker); + tas_studio::tas_playback_rendered_views(marker); R_RenderView.get(marker)(); }) diff --git a/src/modules/tas_studio/editor/mod.rs b/src/modules/tas_studio/editor/mod.rs index 411de74..1a0ac76 100644 --- a/src/modules/tas_studio/editor/mod.rs +++ b/src/modules/tas_studio/editor/mod.rs @@ -478,6 +478,18 @@ impl Editor { self.hovered_frame_idx.map(|idx| &self.branch().frames[idx]) } + pub fn has_all_accurate_frames(&self) -> bool { + let frame_count = self + .branch() + .branch + .script + .frame_bulks() + .map(|bulk| bulk.frame_count.get() as usize) + .sum::(); + + self.branch().first_predicted_frame == frame_count + 1 + } + pub fn undo_log_len(&self) -> usize { self.undo_log.len() } @@ -2814,16 +2826,8 @@ impl Editor { return Err(ManualOpError::CannotDoDuringAdjustment); } - let frame_count = self - .branch() - .branch - .script - .frame_bulks() - .map(|bulk| bulk.frame_count.get() as usize) - .sum::(); - // Only smooth when we have all accurate frames. - if self.branch().first_predicted_frame != frame_count + 1 { + if !self.has_all_accurate_frames() { return Err(ManualOpError::UserError( "all frames must be accurate (simulated by the \ second game) to apply global smoothing" diff --git a/src/modules/tas_studio/mod.rs b/src/modules/tas_studio/mod.rs index cf2c498..d0735cc 100644 --- a/src/modules/tas_studio/mod.rs +++ b/src/modules/tas_studio/mod.rs @@ -71,6 +71,7 @@ impl Module for TasStudio { &BXT_TAS_STUDIO_NEW, &BXT_TAS_STUDIO_LOAD, &BXT_TAS_STUDIO_CONVERT_HLTAS, + &BXT_TAS_STUDIO_REPLAY_VIEWS, &BXT_TAS_STUDIO_REPLAY, &BXT_TAS_STUDIO_SET_STOP_FRAME, &BXT_TAS_STUDIO_SET_YAWSPEED, @@ -391,11 +392,13 @@ fn norefresh_until_stop_frame_frame_idx(marker: MainThreadMarker, editor: &Edito let norefresh_last_frames = unsafe { bxt::BXT_TAS_NOREFRESH_UNTIL_LAST_FRAMES.get(marker)() } as usize; - if editor.stop_frame() == 0 { + let first_frame_idx = if editor.stop_frame() == 0 { (editor.branch().frames.len() - 1).saturating_sub(norefresh_last_frames) } else { (editor.stop_frame() as usize).saturating_sub(norefresh_last_frames) - } + }; + + first_frame_idx.clamp(0, editor.branch().frames.len() - 1) } fn set_effective_norefresh_until_stop_frame(marker: MainThreadMarker, editor: &Editor) { @@ -433,6 +436,89 @@ fn replay(marker: MainThreadMarker) { }; } +static BXT_TAS_STUDIO_REPLAY_VIEWS: Command = Command::new( + b"bxt_tas_studio_replay_views\0", + handler!( + "bxt_tas_studio_replay_views + +Replays the currently loaded camera views of TAS up to the stop frame.", + replay_views as fn(_) + ), +); + +fn replay_views(marker: MainThreadMarker) { + let mut state = STATE.borrow_mut(marker); + *state = match mem::take(&mut *state) { + State::Editing { + mut editor, + last_generation, + last_branch_idx, + simulate_at, + bridge, + } => { + if !editor.has_all_accurate_frames() { + con_print( + marker, + "You need to have second game done simulating to playback views.\n", + ); + + State::Editing { + editor, + last_generation, + last_branch_idx, + simulate_at, + bridge, + } + } else { + editor.cancel_ongoing_adjustments(); + + sdl::set_relative_mouse_mode(marker, true); + client::activate_mouse(marker, true); + + // bxt_tas_norefresh_until_last_frames = 0 means 1 frame being played. Heh. + let start_frame = norefresh_until_stop_frame_frame_idx(marker, &editor); + + engine::prepend_command(marker, "bxt_hud 0; hud_draw 0\n"); + + State::PlayingViews { + editor, + last_generation, + last_branch_idx, + simulate_at, + bridge, + current_frame: start_frame, + } + } + } + // Press play view again to stop. + State::PlayingViews { + editor, + last_generation, + last_branch_idx, + simulate_at, + bridge, + .. + } => { + sdl::set_relative_mouse_mode(marker, false); + client::activate_mouse(marker, false); + engine::prepend_command(marker, "bxt_hud 1; hud_draw 1\n"); + ENABLE_FREECAM_ON_CALCREFDEF.set(marker, true); + + State::Editing { + editor, + last_generation, + last_branch_idx, + simulate_at, + bridge, + } + } + other => { + con_print(marker, "You need to be in the editor to playback views.\n"); + other + } + }; +} + static BXT_TAS_STUDIO_SET_STOP_FRAME: Command = Command::new( b"bxt_tas_studio_set_stop_frame\0", handler!( @@ -1451,6 +1537,16 @@ enum State { simulate_at: Option, bridge: Bridge, }, + /// Playing camera views, will open the editor afterwards. + /// Playback is inside Editing mode so original data will be restored. + PlayingViews { + editor: Editor, + last_generation: u16, + last_branch_idx: usize, + simulate_at: Option, + bridge: Bridge, + current_frame: usize, + }, } impl Default for State { @@ -1560,9 +1656,87 @@ pub unsafe fn maybe_receive_messages_from_remote_server(marker: MainThreadMarker editor.recompute_extra_camera_frame_data_if_needed(); } State::PreparingToPlayToEditor(_, _, _) => unreachable!(), + State::PlayingViews { .. } => { + // Do nothing + // While PlayingViews is happening, nothing else happens or will happen. + // Because PlayingViews only starts when all accurate frames are received. + // Meaning there is nothing second game could do anything to affect the state. + } } } +pub fn should_draw_pause(marker: MainThreadMarker) -> bool { + !matches!(*STATE.borrow_mut(marker), State::PlayingViews { .. }) +} + +pub fn tas_playback_rendered_views(marker: MainThreadMarker) { + if !TasStudio.is_enabled(marker) { + return; + } + + let mut state = STATE.borrow_mut(marker); + *state = match mem::take(&mut *state) { + State::PlayingViews { + editor, + last_generation, + last_branch_idx, + simulate_at, + bridge, + current_frame, + } => { + if current_frame == editor.stop_frame() as usize + || current_frame >= editor.branch().frames.len() + { + sdl::set_relative_mouse_mode(marker, false); + client::activate_mouse(marker, false); + engine::prepend_command(marker, "bxt_hud 1; hud_draw 1\n"); + ENABLE_FREECAM_ON_CALCREFDEF.set(marker, true); + + State::Editing { + editor, + last_generation, + last_branch_idx, + simulate_at, + bridge, + } + } else { + let r_refdef_vieworg = unsafe { &mut *engine::r_refdef_vieworg.get(marker) }; + let r_refdef_viewangles = unsafe { &mut *engine::r_refdef_viewangles.get(marker) }; + + let state = &editor.branch().frames[current_frame].state; + + r_refdef_vieworg[0] = state.player.pos[0]; + r_refdef_vieworg[1] = state.player.pos[1]; + r_refdef_vieworg[2] = state.player.pos[2]; + + // We don't keep track of vieworg so just deduce it from here instead. + // It is not easy to keep track of vieworg because it isn't there to track. + // vieworg is calculated. + // It is also funny how vieworg is calculated here, + // but viewangles is passed from BXT. + r_refdef_vieworg[2] += 28.; + if state.player.ducking { + r_refdef_vieworg[2] -= 16.; + } + + r_refdef_viewangles[0] = state.rendered_viewangles[0]; + + r_refdef_viewangles[1] = state.rendered_viewangles[1]; + + State::PlayingViews { + editor, + last_generation, + last_branch_idx, + simulate_at, + bridge, + current_frame: current_frame + 1, + } + } + } + other => other, + }; +} + pub unsafe fn on_tas_playback_frame( marker: MainThreadMarker, data: OnTasPlaybackFrameData, @@ -1602,6 +1776,9 @@ pub unsafe fn on_tas_playback_frame( strafe_state.prev_frame_input.yaw = view_angles[1].to_radians(); } + // Store rendered viewangles. + strafe_state.rendered_viewangles = data.rendered_viewangles.into(); + // We don't have a good way to extract real trace results from the movement code, so let's // make up trace results based on previous frame's predicted fractions and normal Zs from // BXT.