From 184cecbccdc023c25f68d7de802d0a3c6fefd4cf Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 23 Feb 2026 12:52:22 -0500 Subject: [PATCH 1/2] fix fp guns in spectator, add af_spectate_event --- game_patch/hud/multi_spectate.cpp | 187 +++++++++++++++++++++++++++- game_patch/hud/multi_spectate.h | 3 + game_patch/misc/player_fpgun.cpp | 44 ++++--- game_patch/multi/alpine_packets.cpp | 73 +++++++++++ game_patch/multi/alpine_packets.h | 16 +++ game_patch/multi/multi.cpp | 7 ++ game_patch/multi/network.cpp | 9 +- game_patch/rf/player/player_fpgun.h | 1 + 8 files changed, 315 insertions(+), 25 deletions(-) diff --git a/game_patch/hud/multi_spectate.cpp b/game_patch/hud/multi_spectate.cpp index 090527a2..03739f84 100644 --- a/game_patch/hud/multi_spectate.cpp +++ b/game_patch/hud/multi_spectate.cpp @@ -13,6 +13,7 @@ #include "../rf/gr/gr_font.h" #include "../rf/hud.h" #include "../rf/player/camera.h" +#include "../rf/player/player_fpgun.h" #include "../main/main.h" #include "../misc/player.h" #include "../misc/alpine_settings.h" @@ -30,6 +31,12 @@ static rf::Camera* g_old_target_camera = nullptr; static bool g_spectate_mode_enabled = false; static bool g_spectate_mode_follow_killer = false; +// Edge-detection state for spectated player action animations +static bool g_prev_weapon_is_on = false; +static bool g_prev_is_reloading = false; +static bool g_prev_alt_fire_is_on = false; +static int g_prev_weapon_type = -1; + void player_fpgun_set_player(rf::Player* pp); static void set_camera_target(rf::Player* player) @@ -155,6 +162,11 @@ void multi_spectate_set_target_player(rf::Player* player) if (g_spectate_mode_target != player) { af_send_spectate_start_packet(player); g_spectate_mode_target = player; + // Reset action animation edge-detection state for new target + g_prev_weapon_is_on = false; + g_prev_is_reloading = false; + g_prev_alt_fire_is_on = false; + g_prev_weapon_type = -1; } rf::multi_kill_local_player(); @@ -415,6 +427,29 @@ static ConsoleCommand2 spectate_mode_follow_killer_cmd{ #if SPECTATE_MODE_SHOW_WEAPON +// Hook player_fpgun_play_anim to send weapon fire events to spectators via the server. +// Only WA_FIRE and WA_ALT_FIRE are sent - other animations (custom mode, reload, draw) +// are handled by entity state detection in player_render_new. +FunHook player_fpgun_play_anim_hook{ + 0x004A9380, + [](rf::Player* pp, rf::WeaponAction action) { + player_fpgun_play_anim_hook.call_target(pp, action); + + if (pp != rf::local_player || !rf::is_multi || rf::is_server) + return; + + if (action != rf::WA_FIRE && action != rf::WA_ALT_FIRE) + return; + + rf::Entity* entity = rf::entity_from_handle(pp->entity_handle); + if (!entity) + return; + + int weapon_type = entity->ai.current_primary_weapon; + af_send_spectate_weapon_fire_event(weapon_type, action == rf::WA_ALT_FIRE); + }, +}; + static void player_render_new(rf::Player* player) { if (g_spectate_mode_enabled) { @@ -425,12 +460,69 @@ static void player_render_new(rf::Player* player) (entity && entity->ai.current_primary_weapon == rf::remote_charge_weapon_type); if (g_spectate_mode_target != rf::local_player && entity) { - static rf::Vector3 old_vel; - rf::Vector3 vel_diff = entity->p_data.vel - old_vel; - old_vel = entity->p_data.vel; - - if (vel_diff.y > 0.1f) - entity->entity_flags |= rf::EF_JUMP_START_ANIM; // jump + // Clear jump/land flags so player_fpgun_process doesn't play WA_JUMP + // which cancels action anims (reload, fire, draw, etc.) + entity->entity_flags &= ~rf::EF_JUMP_START_ANIM; + g_spectate_mode_target->just_landed = false; + + int weapon_type = entity->ai.current_primary_weapon; + + // Detect weapon switch and play draw animation for the new weapon + if (weapon_type != g_prev_weapon_type && g_prev_weapon_type != -1) { + if (rf::player_fpgun_action_anim_exists(weapon_type, rf::WA_DRAW)) { + rf::player_fpgun_play_anim(g_spectate_mode_target, rf::WA_DRAW); + } + } + g_prev_weapon_type = weapon_type; + + // Detect weapon fire rising/falling edge. + // AIF_ALT_FIRE distinguishes primary vs alt fire on the same weapon_is_on state. + // For continuous alt fire weapons (baton taser): skip WA_CUSTOM_START intro, go + // straight to WS_LOOP_FIRE on rising edge, play WA_CUSTOM_LEAVE on falling edge. + bool weapon_is_on = rf::entity_weapon_is_on(entity->handle, weapon_type); + bool is_alt_fire = (entity->ai.ai_flags & rf::AIF_ALT_FIRE) != 0; + bool is_continuous_alt_fire_weapon = + rf::weapon_is_on_off_weapon(weapon_type, true); + + if (weapon_is_on && !g_prev_weapon_is_on) { + // Rising edge - weapon just started firing + if (is_alt_fire && is_continuous_alt_fire_weapon) { + // Continuous alt fire (baton taser): skip intro, go straight to looping fire + rf::player_fpgun_set_next_state_anim(g_spectate_mode_target, rf::WS_LOOP_FIRE); + } + else if (is_alt_fire && + rf::player_fpgun_action_anim_exists(weapon_type, rf::WA_ALT_FIRE)) { + rf::player_fpgun_play_anim(g_spectate_mode_target, rf::WA_ALT_FIRE); + } + else if (!is_alt_fire && + rf::player_fpgun_action_anim_exists(weapon_type, rf::WA_FIRE)) { + rf::player_fpgun_play_anim(g_spectate_mode_target, rf::WA_FIRE); + } + // Reset firing timer so muzzle flash renders (used by rail gun glow, + // shoulder cannon boom, and other time-based effects in player_fpgun_render) + g_spectate_mode_target->fpgun_data.time_elapsed_since_firing = 0.0f; + } + else if (!weapon_is_on && g_prev_weapon_is_on) { + // Falling edge - weapon stopped firing + if (g_prev_alt_fire_is_on && is_continuous_alt_fire_weapon && + rf::player_fpgun_action_anim_exists(weapon_type, rf::WA_CUSTOM_LEAVE)) { + rf::player_fpgun_play_anim(g_spectate_mode_target, rf::WA_CUSTOM_LEAVE); + } + } + g_prev_weapon_is_on = weapon_is_on; + g_prev_alt_fire_is_on = is_alt_fire; + + // Detect reload rising edge and play fpgun reload action animation + // Skip if the player has no reserve ammo (empty weapon causes continuous reload flag) + bool is_reloading = rf::entity_is_reloading(entity); + if (is_reloading && !g_prev_is_reloading) { + int ammo_type = rf::weapon_types[weapon_type].ammo_type; + bool has_reserve_ammo = ammo_type >= 0 && entity->ai.ammo[ammo_type] > 0; + if (has_reserve_ammo && rf::player_fpgun_action_anim_exists(weapon_type, rf::WA_RELOAD)) { + rf::player_fpgun_play_anim(g_spectate_mode_target, rf::WA_RELOAD); + } + } + g_prev_is_reloading = is_reloading; } if (g_spectate_mode_target->fpgun_data.zooming_in) @@ -439,6 +531,15 @@ static void player_render_new(rf::Player* player) rf::local_player->fpgun_data.zoom_factor = g_spectate_mode_target->fpgun_data.zoom_factor; rf::player_fpgun_process(g_spectate_mode_target); + + // Force WS_LOOP_FIRE state after process so the render function sees it for muzzle flash. + // The state anim hook inside process should already set this, but the animation transition + // system may not complete in time for the render check. Directly writing the state fields + // guarantees player_fpgun_render's is_in_state_anim(WS_LOOP_FIRE) check passes. + if (entity && rf::entity_weapon_is_on(entity->handle, entity->ai.current_primary_weapon)) { + g_spectate_mode_target->fpgun_current_state_anim = rf::WS_LOOP_FIRE; + } + rf::player_render(g_spectate_mode_target); } else @@ -457,6 +558,79 @@ CallHook gameplay_render_frame_player_fpgun_get_zoom_hook{ #endif // SPECTATE_MODE_SHOW_WEAPON +void multi_spectate_on_remote_weapon_fire(rf::Entity* entity, int weapon_type, bool alt_fire) +{ +#if SPECTATE_MODE_SHOW_WEAPON + if (!g_spectate_mode_enabled || !g_spectate_mode_target || !entity) + return; + + // Check if the firing entity belongs to the spectated player + rf::Entity* target_entity = rf::entity_from_handle(g_spectate_mode_target->entity_handle); + if (entity != target_entity) + return; + + // Don't handle local player + if (g_spectate_mode_target == rf::local_player) + return; + + // Continuous alt fire weapons (baton taser): skip intro, go straight to looping fire + if (alt_fire && rf::weapon_is_on_off_weapon(weapon_type, true)) { + rf::player_fpgun_set_next_state_anim(g_spectate_mode_target, rf::WS_LOOP_FIRE); + } + else { + rf::WeaponAction action = alt_fire ? rf::WA_ALT_FIRE : rf::WA_FIRE; + if (rf::player_fpgun_action_anim_exists(weapon_type, action)) { + // Don't restart the fire animation if it's already playing - this prevents + // weapons like the baton from having their swing animation constantly reset + // by rapid fire events. Semi-automatic weapons (pistol, precision rifle) are + // excluded because each click is a distinct shot that should restart the anim. + bool should_play = rf::weapon_is_semi_automatic(weapon_type) + || !rf::player_fpgun_action_anim_is_playing(g_spectate_mode_target, action); + if (should_play) { + rf::player_fpgun_play_anim(g_spectate_mode_target, action); + } + } + } + + // Reset firing timer for muzzle flash effects + g_spectate_mode_target->fpgun_data.time_elapsed_since_firing = 0.0f; +#endif // SPECTATE_MODE_SHOW_WEAPON +} + +void multi_spectate_on_obj_update_fire(rf::Entity* entity, bool alt_fire) +{ +#if SPECTATE_MODE_SHOW_WEAPON + if (!g_spectate_mode_enabled || !g_spectate_mode_target || !entity) + return; + + rf::Entity* target_entity = rf::entity_from_handle(g_spectate_mode_target->entity_handle); + if (entity != target_entity) + return; + + if (g_spectate_mode_target == rf::local_player) + return; + + int weapon_type = entity->ai.current_primary_weapon; + + // Continuous alt fire weapons (baton taser): skip intro, go straight to looping fire + if (alt_fire && rf::weapon_is_on_off_weapon(weapon_type, true)) { + rf::player_fpgun_set_next_state_anim(g_spectate_mode_target, rf::WS_LOOP_FIRE); + } + else { + rf::WeaponAction action = alt_fire ? rf::WA_ALT_FIRE : rf::WA_FIRE; + if (rf::player_fpgun_action_anim_exists(weapon_type, action)) { + bool should_play = rf::weapon_is_semi_automatic(weapon_type) + || !rf::player_fpgun_action_anim_is_playing(g_spectate_mode_target, action); + if (should_play) { + rf::player_fpgun_play_anim(g_spectate_mode_target, action); + } + } + } + + g_spectate_mode_target->fpgun_data.time_elapsed_since_firing = 0.0f; +#endif +} + void multi_spectate_appy_patch() { render_reticle_hook.install(); @@ -472,6 +646,7 @@ void multi_spectate_appy_patch() AsmWriter(0x0043285D).call(player_render_new); gameplay_render_frame_player_fpgun_get_zoom_hook.install(); + player_fpgun_play_anim_hook.install(); write_mem_ptr(0x0048857E + 2, &g_spectate_mode_target); // obj_mark_all_for_room write_mem_ptr(0x00488598 + 1, &g_spectate_mode_target); // obj_mark_all_for_room diff --git a/game_patch/hud/multi_spectate.h b/game_patch/hud/multi_spectate.h index e9fa108b..a535a1c3 100644 --- a/game_patch/hud/multi_spectate.h +++ b/game_patch/hud/multi_spectate.h @@ -1,6 +1,7 @@ #pragma once #include "../rf/player/control_config.h" +#include "../rf/player/player_fpgun.h" namespace rf { @@ -20,3 +21,5 @@ void multi_spectate_player_create_entity_post(rf::Player* player, rf::Entity* en bool multi_spectate_is_spectating(); bool multi_spectate_execute_action(rf::ControlConfigAction action, bool was_pressed); void multi_spectate_sync_crouch_anim(); +void multi_spectate_on_remote_weapon_fire(rf::Entity* entity, int weapon_type, bool alt_fire); +void multi_spectate_on_obj_update_fire(rf::Entity* entity, bool alt_fire); diff --git a/game_patch/misc/player_fpgun.cpp b/game_patch/misc/player_fpgun.cpp index 7ec6e2b1..678981a3 100644 --- a/game_patch/misc/player_fpgun.cpp +++ b/game_patch/misc/player_fpgun.cpp @@ -23,23 +23,29 @@ static rf::Player* g_fpgun_main_player = nullptr; static FunHook player_fpgun_update_state_anim_hook{ 0x004AA3A0, [](rf::Player* player) { - player_fpgun_update_state_anim_hook.call_target(player); - if (player != rf::local_player) { - rf::Entity* entity = rf::entity_from_handle(player->entity_handle); - if (entity) { - float horz_speed_pow2 = entity->p_data.vel.x * entity->p_data.vel.x + - entity->p_data.vel.z * entity->p_data.vel.z; - int state = rf::WS_IDLE; - if (rf::entity_weapon_is_on(entity->handle, entity->ai.current_primary_weapon)) - state = rf::WS_LOOP_FIRE; - else if (rf::entity_is_swimming(entity) || rf::entity_is_falling(entity)) - state = rf::WS_IDLE; - else if (horz_speed_pow2 > 0.2f) - state = rf::WS_RUN; - if (!rf::player_fpgun_is_in_state_anim(player, state)) - rf::player_fpgun_set_next_state_anim(player, state); - } + if (player == rf::local_player) { + // Only run the original state anim logic for the local player + player_fpgun_update_state_anim_hook.call_target(player); + return; } + rf::Entity* entity = rf::entity_from_handle(player->entity_handle); + if (!entity) + return; + // When falling, keep the current fpgun state anim to avoid + // cancelling action anims (reload, fire, draw, etc.) during spectate + if (rf::entity_is_falling(entity)) + return; + int state = rf::WS_IDLE; + if (rf::entity_weapon_is_on(entity->handle, entity->ai.current_primary_weapon)) + state = rf::WS_LOOP_FIRE; + else if (!rf::entity_is_swimming(entity)) { + float horz_speed_pow2 = entity->p_data.vel.x * entity->p_data.vel.x + + entity->p_data.vel.z * entity->p_data.vel.z; + if (horz_speed_pow2 > 0.2f) + state = rf::WS_RUN; + } + if (!rf::player_fpgun_is_in_state_anim(player, state)) + rf::player_fpgun_set_next_state_anim(player, state); }, }; @@ -310,6 +316,12 @@ void player_fpgun_do_patch() AsmWriter(0x004AA6E7).nop(6); // player_fpgun_process AsmWriter(0x004AE384).nop(6); // player_fpgun_page_in write_mem(0x004ACE2C, asm_opcodes::jmp_rel_short); // player_fpgun_get_zoom + write_mem(0x004AD6E0, asm_opcodes::jmp_rel_short); // player_fpgun_get_muzzle_tag_pos + AsmWriter(0x004ACC8E).nop(6); // player_fpgun_get_non_bullet_muzzle_flash_info + AsmWriter(0x004AB03D).nop(2); // player_fpgun_is_firing_or_reloading: run checks for all players + write_mem(0x004AB0CC, asm_opcodes::jmp_rel_short); // player_fpgun_is_holstering_or_drawing: run checks for all players + write_mem(0x004ADB6C, asm_opcodes::jmp_rel_short); // player_fpgun_is_in_custom_anim: run checks for all players + AsmWriter(0x004AD8CE).nop(6); // player_fpgun_action_anim_is_playing: run checks for all players write_mem_ptr(0x004AE569 + 2, &g_fpgun_main_player); // player_fpgun_load_meshes write_mem_ptr(0x004AE5E3 + 2, &g_fpgun_main_player); // player_fpgun_load_meshes diff --git a/game_patch/multi/alpine_packets.cpp b/game_patch/multi/alpine_packets.cpp index 7112fc42..529562fe 100644 --- a/game_patch/multi/alpine_packets.cpp +++ b/game_patch/multi/alpine_packets.cpp @@ -21,6 +21,7 @@ #include "../sound/sound.h" #include "../misc/alpine_settings.h" #include "../object/object.h" +#include "../hud/multi_spectate.h" void af_send_packet(rf::Player* player, const void* data, int len, bool is_reliable) { @@ -125,6 +126,10 @@ bool af_process_packet(const void* data, int len, const rf::NetAddr& addr, rf::P af_process_server_req_packet(data, static_cast(len), addr); return true; } + case af_packet_type::af_spectate_event: { + af_process_spectate_event_packet(data, static_cast(len), addr); + return true; + } default: return false; // ignore if unrecognized } @@ -1787,3 +1792,71 @@ void af_process_server_msg_packet( rf::console::print("{}", msg); } } + +void af_send_spectate_weapon_fire_event(int weapon_type, bool alt_fire) +{ + // Send: client -> server + if (!rf::is_multi || rf::is_server) + return; + + if (!rf::local_player || !rf::local_player->net_data) + return; + + af_spectate_event_packet pkt{}; + pkt.header.type = static_cast(af_packet_type::af_spectate_event); + pkt.header.size = static_cast(sizeof(pkt) - sizeof(pkt.header)); + pkt.player_id = rf::local_player->net_data->player_id; + pkt.event_type = static_cast(af_spectate_event_type::weapon_fire); + pkt.data[0] = static_cast(weapon_type); + pkt.data[1] = static_cast(alt_fire ? 1 : 0); + + af_send_packet(rf::local_player, &pkt, sizeof(pkt), false); +} + +static void af_process_spectate_event_on_client(const af_spectate_event_packet& pkt) +{ + rf::Player* player = rf::multi_find_player_by_id(pkt.player_id); + if (!player) + return; + + rf::Entity* entity = rf::entity_from_handle(player->entity_handle); + if (!entity) + return; + + switch (static_cast(pkt.event_type)) { + case af_spectate_event_type::weapon_fire: + multi_spectate_on_remote_weapon_fire(entity, pkt.data[0], + pkt.data[1] != 0); + break; + } +} + +void af_process_spectate_event_packet(const void* data, size_t len, const rf::NetAddr& addr) +{ + if (len < sizeof(af_spectate_event_packet)) + return; + + af_spectate_event_packet pkt{}; + std::memcpy(&pkt, data, sizeof(pkt)); + + if (rf::is_server) { + // Server: relay to all other clients + rf::Player* sender = rf::multi_find_player_by_addr(addr); + if (!sender) + return; + + // Overwrite player_id with the server's authoritative value + pkt.player_id = sender->net_data->player_id; + + for (rf::Player& player : SinglyLinkedList{rf::player_list}) { + if (&player == sender || &player == rf::local_player) + continue; + if (!player.net_data) + continue; + af_send_packet(&player, &pkt, sizeof(pkt), false); + } + } + else { + af_process_spectate_event_on_client(pkt); + } +} diff --git a/game_patch/multi/alpine_packets.h b/game_patch/multi/alpine_packets.h index a57b2952..c94f3dcd 100644 --- a/game_patch/multi/alpine_packets.h +++ b/game_patch/multi/alpine_packets.h @@ -7,6 +7,7 @@ #include "gametype.h" #include "../multi/server_internal.h" #include "../rf/multi.h" +#include "../rf/player/player_fpgun.h" #pragma pack(push, 1) @@ -36,6 +37,7 @@ enum class af_packet_type : uint8_t af_spectate_notify = 0x5C, // Alpine 1.2 af_server_msg = 0x5D, // Alpine 1.2 af_server_req = 0x5E, // Alpine 1.2.1 + af_spectate_event = 0x5F, // Alpine 1.3 }; struct af_ping_location_req_packet @@ -229,6 +231,17 @@ struct af_server_msg_packet { char data[]; }; +enum class af_spectate_event_type : uint8_t { + weapon_fire = 0, +}; + +struct af_spectate_event_packet { + RF_GamePacketHeader header; + uint8_t player_id; + uint8_t event_type; + uint8_t data[2]; // subtype-specific payload +}; + #pragma pack(pop) bool af_process_packet(const void* data, int len, const rf::NetAddr& addr, rf::Player* player); @@ -270,6 +283,9 @@ void af_broadcast_automated_chat_msg(std::string_view msg); void af_send_automated_chat_msg(std::string_view msg, rf::Player* player, bool tell_server = false); void af_send_server_console_msg(std::string_view msg, rf::Player* player, bool tell_server = false); +void af_send_spectate_weapon_fire_event(int weapon_type, bool alt_fire); +void af_process_spectate_event_packet(const void* data, size_t len, const rf::NetAddr& addr); + // client requests void af_send_handicap_request(uint8_t amount); void af_send_server_cfg_request(); diff --git a/game_patch/multi/multi.cpp b/game_patch/multi/multi.cpp index a578550b..cb533339 100644 --- a/game_patch/multi/multi.cpp +++ b/game_patch/multi/multi.cpp @@ -15,6 +15,7 @@ #include "server_internal.h" #include "gametype.h" #include "../hud/hud.h" +#include "../hud/multi_spectate.h" #include "../rf/file/file.h" #include "../rf/level.h" #include "../os/console.h" @@ -521,6 +522,12 @@ FunHook multi_process_ } } multi_process_remote_weapon_fire_hook.call_target(ep, weapon_type, pos, orient, alt_fire); + + // Notify spectate system of discrete weapon fire (pistol, precision rifle, etc.) + // so the fpgun fire animation is triggered for the spectated player + if (!rf::is_server) { + multi_spectate_on_obj_update_fire(ep, alt_fire); + } }, }; diff --git a/game_patch/multi/network.cpp b/game_patch/multi/network.cpp index 9b1237d4..64e8d267 100644 --- a/game_patch/multi/network.cpp +++ b/game_patch/multi/network.cpp @@ -221,7 +221,8 @@ enum packet_type : uint8_t { af_spectate_start = 0x5B, af_spectate_notify = 0x5C, af_server_msg = 0x5D, - af_server_req = 0x5E + af_server_req = 0x5E, + af_spectate_event = 0x5F }; // client -> server @@ -248,7 +249,8 @@ std::array g_server_side_packet_whitelist{ rcon, af_ping_location_req, af_client_req, - af_spectate_start + af_spectate_start, + af_spectate_event }; // server -> client @@ -305,7 +307,8 @@ std::array g_client_side_packet_whitelist{ af_server_info, af_spectate_notify, af_server_msg, - af_server_req + af_server_req, + af_spectate_event }; // clang-format on diff --git a/game_patch/rf/player/player_fpgun.h b/game_patch/rf/player/player_fpgun.h index 44c7d8b5..a794beb2 100644 --- a/game_patch/rf/player/player_fpgun.h +++ b/game_patch/rf/player/player_fpgun.h @@ -80,6 +80,7 @@ namespace rf static auto& player_fpgun_render_ir = addr_as_ref(0x004AEEF0); static auto& player_fpgun_set_next_state_anim = addr_as_ref(0x004AA560); static auto& player_fpgun_is_in_state_anim = addr_as_ref(0x004A9520); + static auto& player_fpgun_is_in_custom_anim = addr_as_ref(0x004ADB60); static auto& player_fpgun_clear_all_action_anim_sounds = addr_as_ref(0x004A9490); static auto& player_fpgun_load_meshes = addr_as_ref(0x004AE530); static auto& player_fpgun_delete_meshes = addr_as_ref(0x004AEB40); From 3cdd3db70344fb14dd3577a6e0a90a52c1ca4d54 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 24 Feb 2026 10:13:33 -0500 Subject: [PATCH 2/2] fix thrown projectile timing, remove af_spectate_event --- game_patch/hud/multi_spectate.cpp | 73 ++++++----------------------- game_patch/hud/multi_spectate.h | 1 - game_patch/multi/alpine_packets.cpp | 71 ---------------------------- game_patch/multi/alpine_packets.h | 16 ------- game_patch/multi/multi.cpp | 12 +++-- game_patch/multi/network.cpp | 9 ++-- 6 files changed, 27 insertions(+), 155 deletions(-) diff --git a/game_patch/hud/multi_spectate.cpp b/game_patch/hud/multi_spectate.cpp index 03739f84..e998b004 100644 --- a/game_patch/hud/multi_spectate.cpp +++ b/game_patch/hud/multi_spectate.cpp @@ -427,26 +427,22 @@ static ConsoleCommand2 spectate_mode_follow_killer_cmd{ #if SPECTATE_MODE_SHOW_WEAPON -// Hook player_fpgun_play_anim to send weapon fire events to spectators via the server. -// Only WA_FIRE and WA_ALT_FIRE are sent - other animations (custom mode, reload, draw) -// are handled by entity state detection in player_render_new. -FunHook player_fpgun_play_anim_hook{ - 0x004A9380, - [](rf::Player* pp, rf::WeaponAction action) { - player_fpgun_play_anim_hook.call_target(pp, action); - - if (pp != rf::local_player || !rf::is_multi || rf::is_server) +// Hook entity_play_attack_anim (0x0042C3C0) — called from the obj_update processing path +// when a remote entity's attack animation bits change. This fires at the correct time for +// thrown projectile weapons (grenade, C4, flamethrower canister), before the projectile +// itself arrives. For non-thrown weapons the existing multi_process_remote_weapon_fire_hook +// path also calls multi_spectate_on_obj_update_fire, but the !is_playing guard prevents +// double-triggering. +FunHook entity_play_attack_anim_spectate_hook{ + 0x0042C3C0, + [](rf::Entity* entity, bool alt_fire) { + entity_play_attack_anim_spectate_hook.call_target(entity, alt_fire); + if (!g_spectate_mode_enabled || !g_spectate_mode_target || !entity || rf::is_server) return; - - if (action != rf::WA_FIRE && action != rf::WA_ALT_FIRE) - return; - - rf::Entity* entity = rf::entity_from_handle(pp->entity_handle); - if (!entity) + rf::Entity* target = rf::entity_from_handle(g_spectate_mode_target->entity_handle); + if (entity != target || g_spectate_mode_target == rf::local_player) return; - - int weapon_type = entity->ai.current_primary_weapon; - af_send_spectate_weapon_fire_event(weapon_type, action == rf::WA_ALT_FIRE); + multi_spectate_on_obj_update_fire(entity, alt_fire); }, }; @@ -558,45 +554,6 @@ CallHook gameplay_render_frame_player_fpgun_get_zoom_hook{ #endif // SPECTATE_MODE_SHOW_WEAPON -void multi_spectate_on_remote_weapon_fire(rf::Entity* entity, int weapon_type, bool alt_fire) -{ -#if SPECTATE_MODE_SHOW_WEAPON - if (!g_spectate_mode_enabled || !g_spectate_mode_target || !entity) - return; - - // Check if the firing entity belongs to the spectated player - rf::Entity* target_entity = rf::entity_from_handle(g_spectate_mode_target->entity_handle); - if (entity != target_entity) - return; - - // Don't handle local player - if (g_spectate_mode_target == rf::local_player) - return; - - // Continuous alt fire weapons (baton taser): skip intro, go straight to looping fire - if (alt_fire && rf::weapon_is_on_off_weapon(weapon_type, true)) { - rf::player_fpgun_set_next_state_anim(g_spectate_mode_target, rf::WS_LOOP_FIRE); - } - else { - rf::WeaponAction action = alt_fire ? rf::WA_ALT_FIRE : rf::WA_FIRE; - if (rf::player_fpgun_action_anim_exists(weapon_type, action)) { - // Don't restart the fire animation if it's already playing - this prevents - // weapons like the baton from having their swing animation constantly reset - // by rapid fire events. Semi-automatic weapons (pistol, precision rifle) are - // excluded because each click is a distinct shot that should restart the anim. - bool should_play = rf::weapon_is_semi_automatic(weapon_type) - || !rf::player_fpgun_action_anim_is_playing(g_spectate_mode_target, action); - if (should_play) { - rf::player_fpgun_play_anim(g_spectate_mode_target, action); - } - } - } - - // Reset firing timer for muzzle flash effects - g_spectate_mode_target->fpgun_data.time_elapsed_since_firing = 0.0f; -#endif // SPECTATE_MODE_SHOW_WEAPON -} - void multi_spectate_on_obj_update_fire(rf::Entity* entity, bool alt_fire) { #if SPECTATE_MODE_SHOW_WEAPON @@ -646,7 +603,7 @@ void multi_spectate_appy_patch() AsmWriter(0x0043285D).call(player_render_new); gameplay_render_frame_player_fpgun_get_zoom_hook.install(); - player_fpgun_play_anim_hook.install(); + entity_play_attack_anim_spectate_hook.install(); write_mem_ptr(0x0048857E + 2, &g_spectate_mode_target); // obj_mark_all_for_room write_mem_ptr(0x00488598 + 1, &g_spectate_mode_target); // obj_mark_all_for_room diff --git a/game_patch/hud/multi_spectate.h b/game_patch/hud/multi_spectate.h index a535a1c3..cfef16f6 100644 --- a/game_patch/hud/multi_spectate.h +++ b/game_patch/hud/multi_spectate.h @@ -21,5 +21,4 @@ void multi_spectate_player_create_entity_post(rf::Player* player, rf::Entity* en bool multi_spectate_is_spectating(); bool multi_spectate_execute_action(rf::ControlConfigAction action, bool was_pressed); void multi_spectate_sync_crouch_anim(); -void multi_spectate_on_remote_weapon_fire(rf::Entity* entity, int weapon_type, bool alt_fire); void multi_spectate_on_obj_update_fire(rf::Entity* entity, bool alt_fire); diff --git a/game_patch/multi/alpine_packets.cpp b/game_patch/multi/alpine_packets.cpp index 529562fe..52d2a2cf 100644 --- a/game_patch/multi/alpine_packets.cpp +++ b/game_patch/multi/alpine_packets.cpp @@ -126,10 +126,6 @@ bool af_process_packet(const void* data, int len, const rf::NetAddr& addr, rf::P af_process_server_req_packet(data, static_cast(len), addr); return true; } - case af_packet_type::af_spectate_event: { - af_process_spectate_event_packet(data, static_cast(len), addr); - return true; - } default: return false; // ignore if unrecognized } @@ -1793,70 +1789,3 @@ void af_process_server_msg_packet( } } -void af_send_spectate_weapon_fire_event(int weapon_type, bool alt_fire) -{ - // Send: client -> server - if (!rf::is_multi || rf::is_server) - return; - - if (!rf::local_player || !rf::local_player->net_data) - return; - - af_spectate_event_packet pkt{}; - pkt.header.type = static_cast(af_packet_type::af_spectate_event); - pkt.header.size = static_cast(sizeof(pkt) - sizeof(pkt.header)); - pkt.player_id = rf::local_player->net_data->player_id; - pkt.event_type = static_cast(af_spectate_event_type::weapon_fire); - pkt.data[0] = static_cast(weapon_type); - pkt.data[1] = static_cast(alt_fire ? 1 : 0); - - af_send_packet(rf::local_player, &pkt, sizeof(pkt), false); -} - -static void af_process_spectate_event_on_client(const af_spectate_event_packet& pkt) -{ - rf::Player* player = rf::multi_find_player_by_id(pkt.player_id); - if (!player) - return; - - rf::Entity* entity = rf::entity_from_handle(player->entity_handle); - if (!entity) - return; - - switch (static_cast(pkt.event_type)) { - case af_spectate_event_type::weapon_fire: - multi_spectate_on_remote_weapon_fire(entity, pkt.data[0], - pkt.data[1] != 0); - break; - } -} - -void af_process_spectate_event_packet(const void* data, size_t len, const rf::NetAddr& addr) -{ - if (len < sizeof(af_spectate_event_packet)) - return; - - af_spectate_event_packet pkt{}; - std::memcpy(&pkt, data, sizeof(pkt)); - - if (rf::is_server) { - // Server: relay to all other clients - rf::Player* sender = rf::multi_find_player_by_addr(addr); - if (!sender) - return; - - // Overwrite player_id with the server's authoritative value - pkt.player_id = sender->net_data->player_id; - - for (rf::Player& player : SinglyLinkedList{rf::player_list}) { - if (&player == sender || &player == rf::local_player) - continue; - if (!player.net_data) - continue; - af_send_packet(&player, &pkt, sizeof(pkt), false); - } - } - else { - af_process_spectate_event_on_client(pkt); - } -} diff --git a/game_patch/multi/alpine_packets.h b/game_patch/multi/alpine_packets.h index c94f3dcd..a57b2952 100644 --- a/game_patch/multi/alpine_packets.h +++ b/game_patch/multi/alpine_packets.h @@ -7,7 +7,6 @@ #include "gametype.h" #include "../multi/server_internal.h" #include "../rf/multi.h" -#include "../rf/player/player_fpgun.h" #pragma pack(push, 1) @@ -37,7 +36,6 @@ enum class af_packet_type : uint8_t af_spectate_notify = 0x5C, // Alpine 1.2 af_server_msg = 0x5D, // Alpine 1.2 af_server_req = 0x5E, // Alpine 1.2.1 - af_spectate_event = 0x5F, // Alpine 1.3 }; struct af_ping_location_req_packet @@ -231,17 +229,6 @@ struct af_server_msg_packet { char data[]; }; -enum class af_spectate_event_type : uint8_t { - weapon_fire = 0, -}; - -struct af_spectate_event_packet { - RF_GamePacketHeader header; - uint8_t player_id; - uint8_t event_type; - uint8_t data[2]; // subtype-specific payload -}; - #pragma pack(pop) bool af_process_packet(const void* data, int len, const rf::NetAddr& addr, rf::Player* player); @@ -283,9 +270,6 @@ void af_broadcast_automated_chat_msg(std::string_view msg); void af_send_automated_chat_msg(std::string_view msg, rf::Player* player, bool tell_server = false); void af_send_server_console_msg(std::string_view msg, rf::Player* player, bool tell_server = false); -void af_send_spectate_weapon_fire_event(int weapon_type, bool alt_fire); -void af_process_spectate_event_packet(const void* data, size_t len, const rf::NetAddr& addr); - // client requests void af_send_handicap_request(uint8_t amount); void af_send_server_cfg_request(); diff --git a/game_patch/multi/multi.cpp b/game_patch/multi/multi.cpp index cb533339..293b3eb5 100644 --- a/game_patch/multi/multi.cpp +++ b/game_patch/multi/multi.cpp @@ -523,10 +523,16 @@ FunHook multi_process_ } multi_process_remote_weapon_fire_hook.call_target(ep, weapon_type, pos, orient, alt_fire); - // Notify spectate system of discrete weapon fire (pistol, precision rifle, etc.) - // so the fpgun fire animation is triggered for the spectated player + // Notify spectate system of weapon fire so the fpgun fire animation is triggered. + // Skip thrown projectile weapons (grenade, C4, flamethrower canister alt-fire) because + // their animation is driven earlier and at the correct time by entity_play_attack_anim_spectate_hook. if (!rf::is_server) { - multi_spectate_on_obj_update_fire(ep, alt_fire); + bool is_thrown = (weapon_type == rf::grenade_weapon_type) + || (weapon_type == rf::remote_charge_weapon_type) + || (rf::weapon_is_flamethrower(weapon_type) && alt_fire); + if (!is_thrown) { + multi_spectate_on_obj_update_fire(ep, alt_fire); + } } }, }; diff --git a/game_patch/multi/network.cpp b/game_patch/multi/network.cpp index 64e8d267..9b1237d4 100644 --- a/game_patch/multi/network.cpp +++ b/game_patch/multi/network.cpp @@ -221,8 +221,7 @@ enum packet_type : uint8_t { af_spectate_start = 0x5B, af_spectate_notify = 0x5C, af_server_msg = 0x5D, - af_server_req = 0x5E, - af_spectate_event = 0x5F + af_server_req = 0x5E }; // client -> server @@ -249,8 +248,7 @@ std::array g_server_side_packet_whitelist{ rcon, af_ping_location_req, af_client_req, - af_spectate_start, - af_spectate_event + af_spectate_start }; // server -> client @@ -307,8 +305,7 @@ std::array g_client_side_packet_whitelist{ af_server_info, af_spectate_notify, af_server_msg, - af_server_req, - af_spectate_event + af_server_req }; // clang-format on