Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions game_patch/misc/alpine_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,10 @@ bool alpine_player_settings_load(rf::Player* player)
g_alpine_game_config.play_hit_sounds = std::stoi(settings["PlayHitsounds"]);
processed_keys.insert("PlayHitsounds");
}
if (settings.count("ConfirmedHitFX")) {
g_alpine_game_config.confirmed_hit_fx = std::stoi(settings["ConfirmedHitFX"]);
processed_keys.insert("ConfirmedHitFX");
}
if (settings.count("ShowAwards")) {
g_alpine_game_config.show_awards = std::stoi(settings["ShowAwards"]);
processed_keys.insert("ShowAwards");
Expand Down Expand Up @@ -1611,6 +1615,7 @@ void alpine_player_settings_save(rf::Player* player)
file << "WorldHUDTeamLabels=" << g_alpine_game_config.world_hud_team_player_labels << "\n";
file << "ShowLocationPings=" << g_alpine_game_config.show_location_pings << "\n";
file << "PlayHitsounds=" << g_alpine_game_config.play_hit_sounds << "\n";
file << "ConfirmedHitFX=" << g_alpine_game_config.confirmed_hit_fx << "\n";
file << "ShowAwards=" << g_alpine_game_config.show_awards << "\n";
file << "SprayDisplay=" << g_alpine_game_config.spray_display << "\n";
file << "SpraySelection=" << g_alpine_game_config.selected_spray_index << "\n";
Expand Down
1 change: 1 addition & 0 deletions game_patch/misc/alpine_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ struct AlpineGameSettings
bool world_hud_team_player_labels = false;
bool show_location_pings = true;
bool play_hit_sounds = true;
bool confirmed_hit_fx = true;
bool show_awards = true;

bool spray_display = true;
Expand Down
10 changes: 10 additions & 0 deletions game_patch/misc/ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ static rf::ui::Checkbox ao_staticscope_cbox;
static rf::ui::Label ao_staticscope_label;
static rf::ui::Checkbox ao_hitsounds_cbox;
static rf::ui::Label ao_hitsounds_label;
static rf::ui::Checkbox ao_confirmedhits_cbox;
static rf::ui::Label ao_confirmedhits_label;
static rf::ui::Checkbox ao_taunts_cbox;
static rf::ui::Label ao_taunts_label;
static rf::ui::Checkbox ao_teamrad_cbox;
Expand Down Expand Up @@ -759,6 +761,12 @@ void ao_hitsounds_cbox_on_click(int x, int y) {
ao_play_button_snd(g_alpine_game_config.play_hit_sounds);
}

void ao_confirmedhits_cbox_on_click(int x, int y) {
g_alpine_game_config.confirmed_hit_fx = !g_alpine_game_config.confirmed_hit_fx;
ao_confirmedhits_cbox.checked = g_alpine_game_config.confirmed_hit_fx;
ao_play_button_snd(g_alpine_game_config.confirmed_hit_fx);
}

void ao_taunts_cbox_on_click(int x, int y) {
g_alpine_game_config.play_taunt_sounds = !g_alpine_game_config.play_taunt_sounds;
ao_taunts_cbox.checked = g_alpine_game_config.play_taunt_sounds;
Expand Down Expand Up @@ -1362,6 +1370,8 @@ void alpine_options_panel_init() {
&ao_exposuredamage_cbox, &ao_exposuredamage_label, &alpine_options_panel3, ao_exposuredamage_cbox_on_click, g_alpine_game_config.apply_exposure_damage, 280, 174, "Exposure damage");
alpine_options_panel_checkbox_init(
&ao_gthelp_cbox, &ao_gthelp_label, &alpine_options_panel3, ao_gthelp_cbox_on_click, g_alpine_game_config.show_gametype_help, 280, 204, "Gametype Help");
alpine_options_panel_checkbox_init(
&ao_confirmedhits_cbox, &ao_confirmedhits_label, &alpine_options_panel3, ao_confirmedhits_cbox_on_click, g_alpine_game_config.confirmed_hit_fx, 280, 234, "Confirmed hits");

// fflink text (panel3)
std::string fflink_username = g_game_config.fflink_username.value();
Expand Down
7 changes: 7 additions & 0 deletions game_patch/multi/alpine_packets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,10 @@ static void af_process_damage_notify_packet(const void* data, size_t len, const
add_damage_notify_world_hud_string(entity->pos, damage_notify_packet.player_id, damage_notify_packet.damage,
died, crit);
play_local_hit_sound(died);
// Spectators and demo playback never park local impacts - their instant FX already played
if (!multi_spectate_is_spectating() && !demo_playback_active()) {
confirmed_hit_fx_on_damage_notify(entity);
}
}

void af_send_crit_shot_packet(uint8_t shooter_player_id, uint8_t weapon_type, rf::Player* player)
Expand Down Expand Up @@ -3836,6 +3840,8 @@ uint32_t af_compute_server_info_flags()
af |= af_server_info_flags::SIF_DODGING;
if (g_alpine_server_config_active_rules.mutators.pogo_enabled)
af |= af_server_info_flags::SIF_POGO;
if (g_alpine_server_config.damage_notification_config.enabled)
af |= af_server_info_flags::SIF_DAMAGE_NOTIFICATIONS;
return af;
}

Expand Down Expand Up @@ -3971,6 +3977,7 @@ static void decode_af_server_info_flags(const af_server_info_packet& pkt, Alpine
server_info.skiing = (pkt.af_flags & af_server_info_flags::SIF_SKIING) != 0;
server_info.dodging = (pkt.af_flags & af_server_info_flags::SIF_DODGING) != 0;
server_info.pogo = (pkt.af_flags & af_server_info_flags::SIF_POGO) != 0;
server_info.damage_notifications = (pkt.af_flags & af_server_info_flags::SIF_DAMAGE_NOTIFICATIONS) != 0;
}

// Apply af_server_info_packet flags to the local server info (for listen server host)
Expand Down
1 change: 1 addition & 0 deletions game_patch/multi/alpine_packets.h
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ enum af_server_info_flags : uint32_t {
SIF_SKIING = 1u << 25,
SIF_POGO = 1u << 26,
SIF_DODGING = 1u << 27,
SIF_DAMAGE_NOTIFICATIONS = 1u << 28,
};

// Subset of `rf::NetGameFlags`.
Expand Down
1 change: 1 addition & 0 deletions game_patch/multi/multi.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ struct AlpineFactionServerInfo
bool skiing = false;
bool dodging = false;
bool pogo = false;
bool damage_notifications = false;
};

enum class AlpineRestrictVerdict : uint8_t
Expand Down
6 changes: 6 additions & 0 deletions game_patch/object/object_private.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#pragma once

namespace rf
{
struct Entity;
}

void cutscene_apply_patches();
void confirmed_hit_fx_on_damage_notify(rf::Entity* victim);
void apply_event_patches();
void apply_alpine_events();
void glare_patches_patches();
Expand Down
154 changes: 154 additions & 0 deletions game_patch/object/weapon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
#include <patch_common/CallHook.h>
#include <patch_common/CodeInjection.h>
#include <patch_common/ShortTypes.h>
#include <algorithm>
#include <array>
#include <deque>
#include <xlog/xlog.h>
#include "../multi/server.h"
#include "../rf/player/player.h"
Expand All @@ -18,6 +20,7 @@
#include "../multi/mutators.h"
#include "../misc/misc.h"
#include "../misc/alpine_settings.h"
#include "../os/os.h"

static std::array<uint8_t, 64U> weapon_reticle_custom_mask{}; // bit 0 = _0, bit 1 = _1
static std::pair<bool, bool> rocket_locked_custom_reticle = {false, false};
Expand Down Expand Up @@ -214,6 +217,153 @@ CallHook<void(rf::Vector3&, float, float, int, int)> weapon_hit_wall_obj_apply_r
},
};

// Server-confirmed hit FX: when the local player's projectile hits another player, park the
// impact FX (blood, vclip, foley sound) instead of playing them immediately, then replay them
// when the server confirms the hit via af_damage_notify. Markers for hits the server rejects
// (lag compensation disagreement) expire silently.
struct ConfirmedHitMarker
{
int victim_handle;
rf::Vector3 hit_point;
rf::Vector3 hit_normal;
rf::Vector3 dir;
int weapon_type;
int parent_handle;
int impact_sound; // sound handle resolved at park time, -1 = none
bool impact_vclip;
int64_t expiry;
};
static std::deque<ConfirmedHitMarker> g_confirmed_hit_markers;
static constexpr size_t confirmed_hit_max_markers = 32;
static constexpr int64_t confirmed_hit_marker_ms = 500; // povcomp_max_ms (450) + margin

static bool confirmed_hit_fx_should_park(rf::Weapon* wp, rf::Object* victim)
{
if (!rf::is_multi || rf::is_server || !g_alpine_game_config.confirmed_hit_fx) {
return false;
}
// without server damage notifications no confirmation ever arrives - keep stock instant FX
const auto& server_info = get_af_server_info();
if (!server_info || !server_info->damage_notifications) {
return false;
}
if (!rf::local_player || wp->parent_handle != rf::local_player->entity_handle) {
return false;
}
// only the flesh FX branch of weapon_hit_obj is deferred, and only for living player victims
if (victim->type != rf::OT_ENTITY || victim->material != 3) {
return false;
}
rf::Entity* ep = rf::entity_from_handle(victim->handle);
if (!ep || rf::entity_is_dying(ep) || !rf::player_from_entity_handle(victim->handle)) {
return false;
}
// melee/sticky/remote charge/flamethrower and the riot stick special sound path keep stock FX
if (wp->info->flags & (rf::WTF_MELEE | rf::WTF_STICKY | rf::WTF_REMOTE_CHARGE)) {
return false;
}
if (wp->info_index == rf::flamethrower_weapon_type || (wp->weapon_flags & 0x8)) {
return false;
}
return true;
}

// FX block of weapon_hit_obj (0x004C59F0). At this address ESI = weapon and EBX = hit object on
// both the client path (jump from 0x004C5CE3) and the server path (EBX reload at 0x004C62FA).
CodeInjection weapon_hit_obj_confirmed_hit_fx_injection{
0x004C6301,
[](auto& regs) {
rf::Weapon* wp = regs.esi;
rf::Object* victim = regs.ebx;
if (!confirmed_hit_fx_should_park(wp, victim)) {
return;
}
// resolve the impact foley sound like the stock selection at 0x004C6463
int material = std::clamp(wp->p_data.collide_out.material, 0, 9);
int sound = rf::foley_get_sound_handle(wp->info->impact_foley_id[material]);
if (sound == -1) {
sound = rf::foley_get_sound_handle(wp->info->impact_foley_id[0]);
}
if (g_confirmed_hit_markers.size() >= confirmed_hit_max_markers) {
g_confirmed_hit_markers.pop_front();
}
g_confirmed_hit_markers.push_back({
victim->handle,
wp->p_data.collide_out.hit_point,
wp->p_data.collide_out.hit_normal,
wp->orient.fvec,
wp->info_index,
wp->parent_handle,
sound,
wp->info->crater_radius > addr_as_ref<float>(0x005893F8), // stock flesh vclip gate
timer::get_i64(1000) + confirmed_hit_marker_ms,
});
regs.eip = 0x004C655D; // skip the FX block to the epilogue; FX replayed on confirmation
},
};

void confirmed_hit_fx_on_damage_notify(rf::Entity* victim)
{
if (!g_alpine_game_config.confirmed_hit_fx || rf::entity_is_local_player(victim)) {
return;
}
const auto& server_info = get_af_server_info();
if (!server_info || !server_info->damage_notifications) {
return;
}

const int64_t now = timer::get_i64(1000);
std::erase_if(g_confirmed_hit_markers, [now](const ConfirmedHitMarker& m) { return now > m.expiry; });

for (auto it = g_confirmed_hit_markers.begin(); it != g_confirmed_hit_markers.end(); ++it) {
if (it->victim_handle != victim->handle) {
continue;
}
ConfirmedHitMarker m = *it;
g_confirmed_hit_markers.erase(it);
// mirror the stock flesh FX block of weapon_hit_obj (0x004C6321); the multi client path
// always passes damage 0.0 there, so replaying with 0.0 matches stock visuals
rf::entity_blood_maybe_splatter(0.0f, &m.hit_point, &m.dir);
rf::entity_blood_do_hit_effect(&m.hit_point, victim->room, &victim->pos, 0.0f);
if (m.impact_vclip) {
rf::weapon_create_impact_vclip(m.weapon_type, reinterpret_cast<int>(victim->room), nullptr,
&m.hit_point, &m.hit_normal, m.parent_handle);
}
if (m.impact_sound != -1) {
rf::snd_play_3d(m.impact_sound, m.hit_point, 1.0f, rf::Vector3{}, 0);
}
return;
}

// No parked impact (very high ping, or a hit this client never predicted): approximate at the
// victim's current position so the confirmed hit still shows blood
rf::Entity* local_entity = rf::local_player ? rf::entity_from_handle(rf::local_player->entity_handle) : nullptr;
if (!local_entity) {
return;
}
rf::Vector3 dir = victim->pos - local_entity->pos;
dir.normalize_safe();
rf::entity_blood_maybe_splatter(0.0f, &victim->pos, &dir);
rf::entity_blood_do_hit_effect(&victim->pos, victim->room, &victim->pos, 0.0f);
int weapon_type = local_entity->ai.current_primary_weapon;
if (weapon_type >= 0 && weapon_type < rf::num_weapon_types) {
int sound = rf::foley_get_sound_handle(rf::weapon_types[weapon_type].impact_foley_id[3]); // flesh
if (sound != -1) {
rf::snd_play_3d(sound, victim->pos, 1.0f, rf::Vector3{}, 0);
}
}
}

ConsoleCommand2 confirmed_hits_cmd{
"cl_confirmedhits",
[]() {
g_alpine_game_config.confirmed_hit_fx = !g_alpine_game_config.confirmed_hit_fx;
rf::console::print("Server-confirmed hit effects are {}",
g_alpine_game_config.confirmed_hit_fx ? "enabled" : "disabled");
},
"Toggles whether blood and impact effects on other players are delayed until the server confirms the hit",
};

ConsoleCommand2 multi_ricochet_cmd{
"mp_ricochet",
[]() {
Expand Down Expand Up @@ -417,8 +567,12 @@ void apply_weapon_patches()
// Fix rockets not making damage after hitting a detail brush
weapon_hit_wall_obj_apply_radius_damage_hook.install();

// Delay blood/impact FX on other players until the server confirms the hit
weapon_hit_obj_confirmed_hit_fx_injection.install();

// commands
multi_ricochet_cmd.register_cmd();
confirmed_hits_cmd.register_cmd();
show_enemy_bullets_cmd.register_cmd();
gaussian_spread_cmd.register_cmd();
unlimited_semi_auto_cmd.register_cmd();
Expand Down
6 changes: 6 additions & 0 deletions game_patch/rf/entity.h
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,12 @@ namespace rf
};

static auto& entity_from_handle = addr_as_ref<Entity*(int handle)>(0x00426FC0);
// Blood splatter decal thrown from a flesh impact (weapon_hit_obj flesh branch)
static auto& entity_blood_maybe_splatter =
addr_as_ref<void(float damage, Vector3* hit_point, Vector3* dir)>(0x0042E1C0);
// Blood particle + explosion puff at a flesh impact point (weapon_hit_obj flesh branch)
static auto& entity_blood_do_hit_effect =
addr_as_ref<void(Vector3* hit_point, GRoom* room, Vector3* victim_pos, float damage)>(0x0042E3D0);
static auto& entity_lookup_type = addr_as_ref<int(const char* name)>(0x004251C0);
static auto& entity_create =
addr_as_ref<Entity*(int entity_type, const char* name, int parent_handle, const Vector3& pos,
Expand Down
Loading