Skip to content
Draft
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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Version 1.3.0 (Bakeapple): Not yet released
[@nickalreadyinuse](https://github.com/nickalreadyinuse)
- Optimize network performance for `af_obj_update` packets and bot decommission logic
- Sync animation state for crouched players in first person spectate view
- Implement enemy and teammate footstep audio for weapons other than pistol in multiplayer. Enabled by default with server permission, `cl_footsteps` to toggle.

Version 1.2.2 (Willow): Released Jan-04-2026
--------------------------------
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 @@ -148,6 +148,7 @@ struct AlpineGameSettings
bool always_clamp_official_lightmaps = false;
bool static_bomb_code = false;
bool entity_pain_sounds = true;
bool footsteps = true;
static constexpr int min_gib_chunk_count = 7;
static constexpr int max_gib_chunk_count = 100;
int gib_chunk_count = 14;
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 @@ -21,6 +21,7 @@
#include "../sound/sound.h"
#include "../misc/alpine_settings.h"
#include "../object/object.h"
#include "../object/object_private.h"

void af_send_packet(rf::Player* player, const void* data, int len, bool is_reliable)
{
Expand Down Expand Up @@ -1253,6 +1254,8 @@ static void build_af_server_info_packet(af_server_info_packet& pkt)
af |= af_server_info_flags::SIF_LOCATION_PINGING;
if (g_alpine_server_config_active_rules.spawn_delay.enabled)
af |= af_server_info_flags::SIF_DELAYED_SPAWNS;
if (g_alpine_server_config.allow_footsteps)
af |= af_server_info_flags::SIF_ALLOW_FOOTSTEPS;
if (g_alpine_server_config.signal_cfg_changed) {
af |= af_server_info_flags::SIF_SERVER_CFG_CHANGED;
for (rf::Player& player : SinglyLinkedList{rf::player_list}) {
Expand Down Expand Up @@ -1423,13 +1426,17 @@ static void af_process_server_info_packet(const void* data, size_t len, const rf
server_info.gaussian_spread = (pkt.af_flags & af_server_info_flags::SIF_GAUSSIAN_SPREAD) != 0;
server_info.location_pinging = (pkt.af_flags & af_server_info_flags::SIF_LOCATION_PINGING) != 0;
server_info.delayed_spawns = (pkt.af_flags & af_server_info_flags::SIF_DELAYED_SPAWNS) != 0;
server_info.allow_footsteps = (pkt.af_flags & af_server_info_flags::SIF_ALLOW_FOOTSTEPS) != 0;

if ((pkt.af_flags & af_server_info_flags::SIF_SERVER_CFG_CHANGED) != 0) {
g_remote_server_cfg_popup.set_cfg_changed();
}

server_info.semi_auto_cooldown = static_cast<int>(pkt.semi_auto_cooldown);

// Update footstep activation based on new server permissions
evaluate_footsteps();

//xlog::warn("af_server_info processed - gt {}, cooldown {}", pkt.game_type, server_info.semi_auto_cooldown.value());
}

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 @@ -183,6 +183,7 @@ enum af_server_info_flags : uint32_t {
SIF_LOCATION_PINGING = 1u << 10,
SIF_DELAYED_SPAWNS = 1u << 11,
SIF_SERVER_CFG_CHANGED = 1u << 12,
SIF_ALLOW_FOOTSTEPS = 1u << 13,
};

// Subset of `rf::NetGameFlags`.
Expand Down
5 changes: 5 additions & 0 deletions game_patch/multi/dedi_cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,10 @@ static void apply_known_key_in_order(AlpineServerConfig& cfg, const std::string&
if (auto v = node.value<bool>())
cfg.allow_unlimited_fps = *v;
}
else if (key == "allow_footsteps") {
if (auto v = node.value<bool>())
cfg.allow_footsteps = *v;
}
else if (key == "use_sp_damage_calculation") {
if (auto v = node.value<bool>())
cfg.use_sp_damage_calculation = *v;
Expand Down Expand Up @@ -1858,6 +1862,7 @@ void print_alpine_dedicated_server_config_info(std::string& output, bool verbose
std::format_to(iter, " Allow lightmap only mode: {}\n", cfg.allow_lightmaps_only);
std::format_to(iter, " Allow disable muzzle flash: {}\n", cfg.allow_disable_muzzle_flash);
std::format_to(iter, " Allow disable 240 FPS cap: {}\n", cfg.allow_unlimited_fps);
std::format_to(iter, " Allow footsteps: {}\n", cfg.allow_footsteps);
std::format_to(iter, " SP-style damage calculation: {}\n", cfg.use_sp_damage_calculation);
std::format_to(iter, " Exclude bots from player count: {}\n", cfg.exclude_bots_from_player_count);

Expand Down
4 changes: 4 additions & 0 deletions game_patch/multi/kill.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "server_internal.h"
#include "alpine_packets.h"
#include "../misc/player.h"
#include "../object/object_private.h"

bool kill_messages = true;

Expand Down Expand Up @@ -353,6 +354,9 @@ FunHook<void()> multi_level_init_hook{
reset_gungame_notifications();
weapon_manager.initialize_score_to_weapon_map();
}

// Re-evaluate footstep state when level loads
evaluate_footsteps();
},
};

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 @@ -100,6 +100,7 @@ struct AlpineFactionServerInfo
bool delayed_spawns = false;
int koth_score_limit = 0;
int dc_score_limit = 0;
bool allow_footsteps = false;
};

enum class AlpineRestrictVerdict : uint8_t
Expand Down
11 changes: 11 additions & 0 deletions game_patch/multi/network.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
#include "../misc/player.h"
#include "../misc/alpine_settings.h"
#include "../object/object.h"
#include "../object/object_private.h"
#include "../os/console.h"
#include "../purefaction/pf.h"
#include "../sound/sound.h"
Expand Down Expand Up @@ -1318,6 +1319,9 @@ CallHook<int(const rf::NetAddr*, std::byte*, size_t)> send_join_accept_packet_ho
if (server_delayed_spawns()) {
ext_data.flags |= AlpineFactionJoinAcceptPacketExt::Flags::delayed_spawns;
}
if (server_allow_footsteps()) {
ext_data.flags |= AlpineFactionJoinAcceptPacketExt::Flags::allow_footsteps;
}
auto [buf, new_len] = extend_packet_bytes(data, len, &ext_data, sizeof(ext_data));
//auto [new_data, new_len] = extend_packet_fixed(data, len, ext_data);
return send_join_accept_packet_hook.call_target(addr, buf.get(), new_len);
Expand Down Expand Up @@ -1350,6 +1354,7 @@ CodeInjection process_join_accept_injection{
server_info.gaussian_spread = !!(ext_data.flags & AlpineFactionJoinAcceptPacketExt::Flags::gaussian_spread);
server_info.location_pinging = !!(ext_data.flags & AlpineFactionJoinAcceptPacketExt::Flags::location_pinging);
server_info.delayed_spawns = !!(ext_data.flags & AlpineFactionJoinAcceptPacketExt::Flags::delayed_spawns);
server_info.allow_footsteps = !!(ext_data.flags & AlpineFactionJoinAcceptPacketExt::Flags::allow_footsteps);

constexpr float default_fov = 90.0f;
if (!!(ext_data.flags & AlpineFactionJoinAcceptPacketExt::Flags::max_fov) && ext_data.max_fov >= default_fov) {
Expand All @@ -1359,6 +1364,9 @@ CodeInjection process_join_accept_injection{
server_info.semi_auto_cooldown = ext_data.semi_auto_cooldown;
}
g_af_server_info = std::optional{server_info};

// Update footstep activation based on server permissions
evaluate_footsteps();
}
else {
g_af_server_info.reset();
Expand Down Expand Up @@ -1949,6 +1957,9 @@ FunHook<void()> multi_stop_hook{
static_cast<PlayerAdditionalData*>(rf::local_player);
*player_add_data = PlayerAdditionalData{};
}

// Re-evaluate footstep state when leaving multiplayer
evaluate_footsteps();
multi_stop_hook.call_target();
},
};
Expand Down
1 change: 1 addition & 0 deletions game_patch/multi/network.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ struct AlpineFactionJoinAcceptPacketExt
gaussian_spread = 1u << 9,
location_pinging = 1u << 10,
delayed_spawns = 1u << 11,
allow_footsteps = 1u << 12,
} flags = Flags::none;

float max_fov;
Expand Down
5 changes: 5 additions & 0 deletions game_patch/multi/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3223,6 +3223,11 @@ bool server_gaussian_spread()
return g_alpine_server_config.gaussian_spread;
}

bool server_allow_footsteps()
{
return g_alpine_server_config.allow_footsteps;
}

std::tuple<bool, int, bool, bool> server_features_require_alpine_client()
{
bool requires_alpine = false; // alpine required to spawn
Expand Down
1 change: 1 addition & 0 deletions game_patch/multi/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ bool server_allow_disable_muzzle_flash();
bool server_apply_click_limiter();
bool server_allow_unlimited_fps();
bool server_gaussian_spread();
bool server_allow_footsteps();
std::tuple<bool, int, bool, bool> server_features_require_alpine_client();
void server_reliable_socket_ready(rf::Player* player);
bool server_weapon_items_give_full_ammo();
Expand Down
1 change: 1 addition & 0 deletions game_patch/multi/server_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ struct AlpineServerConfig
bool allow_disable_screenshake = true;
bool allow_disable_muzzle_flash = true;
bool allow_unlimited_fps = false;
bool allow_footsteps = true;
bool use_sp_damage_calculation = false;
bool exclude_bots_from_player_count = false;
AlpineRestrictConfig alpine_restricted_config;
Expand Down
123 changes: 123 additions & 0 deletions game_patch/object/entity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#include <patch_common/FunHook.h>
#include <patch_common/CallHook.h>
#include <patch_common/AsmWriter.h>
#include <cstring>
#include <unordered_set>
#include <xlog/xlog.h>
#include "../misc/achievements.h"
#include "../misc/alpine_settings.h"
Expand All @@ -17,6 +19,11 @@
#include "../rf/os/frametime.h"
#include "../rf/os/os.h"
#include "../rf/sound/sound.h"
#include "../rf/object.h"
#include "../rf/vmesh.h"
#include "../rf/character.h"
#include "../rf/math/vector.h"
#include "../multi/multi.h"

rf::Timestamp g_player_jump_timestamp;

Expand Down Expand Up @@ -437,6 +444,117 @@ void apply_entity_sim_distance() {
rf::entity_sim_distance = g_alpine_game_config.entity_sim_distance;
}

// Fix for footstep audio bug in multiplayer where remote players' footsteps
// only play when they have a pistol equipped. Only pistol walk/run animations
// have footstep triggers defined in entity.tbl; other weapons lack them.
// We hook the animation update function (0x0051bce3) to inject trigger values
// into skeletons that have empty trigger names, using VMVF time range data.

bool g_footsteps_active = false;

// Footstep trigger positions as percentages of animation duration
static constexpr float footstep_walk_left_pct = 0.15f;
static constexpr float footstep_walk_right_pct = 0.55f;
static constexpr float footstep_run_left_pct = 0.12f;
static constexpr float footstep_run_right_pct = 0.52f;

CodeInjection footstep_trigger_fixup_injection{
0x0051bce3,
[](auto& regs) {
if (!g_footsteps_active) return;

uint32_t* anims_array = reinterpret_cast<uint32_t*>(static_cast<int32_t>(regs.edx));
int anim_index = static_cast<int32_t>(regs.ebp);
rf::Skeleton* skeleton = reinterpret_cast<rf::Skeleton*>(anims_array[anim_index]);
if (!skeleton) return;

// Only inject if trigger names are empty (no existing footstep data)
if (skeleton->triggers[0].name[0] != '\0') return;

const char* name = skeleton->mvf_filename;
if (!name || name[0] == '\0') return;

bool is_walk = std::strstr(name, "walk") != nullptr;
bool is_run = !is_walk && std::strstr(name, "run") != nullptr;
if (!is_walk && !is_run) return;

if (!skeleton->animation_data) return;

uint8_t* data = static_cast<uint8_t*>(skeleton->animation_data);
if (data[0] != 'V' || data[1] != 'M' || data[2] != 'V' || data[3] != 'F') return;

int start_time = *reinterpret_cast<int*>(data + 0x10);
int end_time = *reinterpret_cast<int*>(data + 0x14);
int duration = end_time - start_time;
if (duration <= 0) return;

float left_pct = is_walk ? footstep_walk_left_pct : footstep_run_left_pct;
float right_pct = is_walk ? footstep_walk_right_pct : footstep_run_right_pct;

int left_trigger = start_time + static_cast<int>(duration * left_pct);
int right_trigger = start_time + static_cast<int>(duration * right_pct);

std::strncpy(skeleton->triggers[0].name, "footstep_left", 15);
skeleton->triggers[0].name[15] = '\0';
skeleton->triggers[0].value = left_trigger;

std::strncpy(skeleton->triggers[1].name, "footstep_right", 15);
skeleton->triggers[1].name[15] = '\0';
skeleton->triggers[1].value = right_trigger;

static std::unordered_set<std::string> logged_injected;
if (logged_injected.insert(name).second) {
xlog::info("Footstep fix: {} injected left={} right={} (start={} end={} dur={} type={})",
name, left_trigger, right_trigger, start_time, end_time, duration,
is_walk ? "walk" : "run");
}
}
};

void evaluate_footsteps()
{
// Check both client preference and server permission
bool client_wants_fix = g_alpine_game_config.footsteps;
bool server_allows_fix = false;

// Determine server permission:
// - Single-player: always allowed
// - Server (hosting): always allowed
// - Client on Alpine Faction server: check server permission
// - Client on legacy server: NOT allowed (compatibility)
if (!rf::is_multi) {
server_allows_fix = true;
}
else if (rf::is_server) {
server_allows_fix = true;
}
else {
// Client: only allow if Alpine Faction server permits it
const auto& server_info = get_af_server_info();
if (server_info.has_value()) {
server_allows_fix = server_info->allow_footsteps;
}
}

g_footsteps_active = client_wants_fix && server_allows_fix;

xlog::info("Footstep fix: client_wants={}, server_allows={}, active={}",
client_wants_fix, server_allows_fix, g_footsteps_active);
}

ConsoleCommand2 cl_footsteps_cmd{
"cl_footsteps",
[]() {
g_alpine_game_config.footsteps = !g_alpine_game_config.footsteps;
evaluate_footsteps();
rf::console::print("Footsteps: {} (active: {})",
g_alpine_game_config.footsteps ? "enabled" : "disabled",
g_footsteps_active ? "yes" : "no");
},
"Toggle footstep trigger injection for all weapons",
"cl_footsteps",
};

FunHook<void(rf::Entity*, float)> entity_maybe_play_pain_sound_hook{
0x004196F0, [](rf::Entity* ep, float percent_damage) {
if (g_alpine_game_config.entity_pain_sounds) {
Expand Down Expand Up @@ -512,11 +630,16 @@ void entity_do_patch()
// Restore cut stock game feature for entities and corpses exploding into chunks
entity_blood_throw_gibs_hook.install();

// Footstep fix: inject trigger frames on-the-fly when animations have empty triggers
evaluate_footsteps();
footstep_trigger_fixup_injection.install();

// Commands
sp_exposuredamage_cmd.register_cmd();
cl_gorelevel_cmd.register_cmd();
cl_gibchunks_cmd.register_cmd();
cl_gibvelocityscale_cmd.register_cmd();
cl_giblifetimems_cmd.register_cmd();
cl_painsounds_cmd.register_cmd();
cl_footsteps_cmd.register_cmd();
}
4 changes: 4 additions & 0 deletions game_patch/object/object_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ void entity_do_patch();
void item_do_patch();
void particle_do_patch();
void obj_light_apply_patch();

// Footstep fix state (controlled by client config AND server permission)
extern bool g_footsteps_active;
void evaluate_footsteps();
Loading