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
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Version 1.5.0 (TBD): Not yet released
[@is-this-c](https://github.com/is-this-c)
- Let `Caps Lock` capitalize

[@nickalreadyinuse](https://github.com/nickalreadyinuse)
- Fix first person camera height on miner1 and merc based skeletons, which caused other players to appear shorter than you in multiplayer (stock bug since 1.2)
- Servers keep firing origins consistent for clients on older Alpine Faction versions

Version 1.4.0 (Lupin): Released Aug-25-2026
--------------------------------
### Major features
Expand Down
41 changes: 41 additions & 0 deletions game_patch/debug/obj_debug.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
#include "../rf/collide.h"
#include "../rf/player/camera.h"
#include "debug_internal.h"
#include "../rf/physics.h"
#include "../rf/vmesh.h"
#include <common/utils/list-utils.h>
#include <xlog/xlog.h>
#include <algorithm>
#include <format>

std::optional<float> g_target_rotate_speed;

Expand Down Expand Up @@ -165,6 +171,40 @@ ConsoleCommand2 dbg_ai_pause_cmd{
},
};

// Debug: per-entity origin/eye/eye-tag/csphere heights above the floor
ConsoleCommand2 dbg_eye_heights_cmd{
"d_eye_heights",
[]() {
for (auto& ep : DoublyLinkedList{rf::entity_list}) {
rf::Vector3 p0 = ep.pos;
rf::Vector3 p1 = ep.pos - rf::Vector3{0.0f, 5.0f, 0.0f};
rf::PCollisionOut col{};
col.obj_handle = -1;
float floor = rf::collide_linesegment_world(p0, p1, rf::CF_ANY_HIT, &col) ? col.hit_point.y : ep.pos.y - 5.0f;
float tag_y = 0.0f;
if (ep.vmesh && ep.info2->eye_tag >= 0) {
rf::Matrix3 o;
rf::Vector3 t;
rf::vmesh_get_prop_point_transform(ep.vmesh, ep.info2->eye_tag, &ep.orient, &ep.pos, &o, &t);
tag_y = t.y - floor;
}
float bottom = 0.0f;
for (int i = 0; i < ep.info2->collision_spheres.size(); ++i) {
const auto& cs = ep.info2->collision_spheres[i];
bottom = std::min(bottom, cs.center.y - cs.radius);
}
std::string line = std::format(
"{} [{}] local={} origin={:.3f} eye_pos={:.3f} eye_tag_now={:.3f} sphere_bottom={:.3f} anim={}",
ep.name, ep.info2->name, &ep == rf::local_player_entity, ep.pos.y - floor, ep.eye_pos.y - floor,
tag_y, ep.pos.y + bottom - floor, ep.current_state_anim);
rf::console::print("{}", line);
xlog::info("d_eye_heights: {}", line);
}
},
"Print entity origin/eye/csphere heights above the floor under them",
"d_eye_heights"
};

const char* get_obj_type_name(rf::Object& obj)
{
switch (obj.type) {
Expand Down Expand Up @@ -246,6 +286,7 @@ void register_obj_debug_commands()
dbg_entity_action_cmd.register_cmd();
dbg_spin_cmd.register_cmd();
dbg_ai_pause_cmd.register_cmd();
dbg_eye_heights_cmd.register_cmd();
}

void render_obj_debug_ui()
Expand Down
72 changes: 72 additions & 0 deletions game_patch/object/entity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
#include <patch_common/AsmWriter.h>
#include <cstring>
#include <string>
#include <unordered_map>
#include <xlog/xlog.h>
#include "../misc/achievements.h"
#include "../misc/alpine_settings.h"
#include "../misc/player.h"
#include "../os/console.h"
#include "../rf/gr/gr_light.h"
#include "../rf/entity.h"
Expand Down Expand Up @@ -681,6 +683,72 @@ CodeInjection clear_stale_movement_input_injection{
},
};

// Stock samples EntityInfo::local_eye_offset from the "stand" pose, but armed entities idle in "attack_stand",
// which sits ~9cm lower on the miner1/merc skeletons. Resample after stock init so hitboxes stay stock.
static auto& entity_can_crouch = addr_as_ref<bool(rf::Entity*)>(0x00428010);
static auto& entity_is_humanoid = addr_as_ref<bool(rf::Entity*)>(0x0040A150);
constexpr int EIF_MESH_DATA_INITIALIZED = 0x40000000;
// stock eye Y minus corrected eye Y, per entity type (for pre-1.5 clients on the server rewind path)
static std::unordered_map<rf::EntityInfo*, float> g_legacy_eye_offset_delta_y;

FunHook<void(rf::Entity*)> entity_info_setup_bone_damage_modifiers_hook{
0x00423B90,
[](rf::Entity* ep) {
rf::EntityInfo* info = ep->info2;
bool was_initialized = info->flags & EIF_MESH_DATA_INITIALIZED;
entity_info_setup_bone_damage_modifiers_hook.call_target(ep);
if (was_initialized || !ep->vmesh || info->eye_tag < 0 || !entity_can_crouch(ep)) {
return;
}
int idx = rf::ENTITY_STATE_ATTACK_STAND;
if (ep->state_anims[idx].vmesh_anim_index < 0) {
idx = rf::ENTITY_STATE_STAND;
}
int anim = ep->state_anims[idx].vmesh_anim_index;
if (anim < 0) {
return;
}
rf::vmesh_reset_actions(ep->vmesh);
rf::vmesh_set_action_weight(ep->vmesh, anim, 1.0f);
rf::vmesh_process(ep->vmesh, 1.0f, 0, nullptr, nullptr, 1);
rf::Matrix3 identity;
identity.make_identity();
rf::Vector3 zero{};
rf::Matrix3 out_orient;
rf::Vector3 eye;
rf::vmesh_get_prop_point_transform(ep->vmesh, info->eye_tag, &identity, &zero, &out_orient, &eye);
// restore stock pose
rf::vmesh_reset_actions(ep->vmesh);
if (ep->state_anims[0].vmesh_anim_index >= 0) {
rf::vmesh_set_action_weight(ep->vmesh, ep->state_anims[0].vmesh_anim_index, 1.0f);
}
rf::vmesh_process(ep->vmesh, 0.2f, 0, nullptr, nullptr, 1);
if (entity_is_humanoid(ep)) {
eye.x = 0.0f;
eye.z = 0.0f;
}
g_legacy_eye_offset_delta_y[info] = info->local_eye_offset.y - eye.y;
xlog::info("eye offset resample: type {} state={} stock y={} resampled y={}", info->name, idx,
info->local_eye_offset.y, eye.y);
info->local_eye_offset = eye;
},
};

// Server: burst follow-up rounds have no packet pos, so the shooter's eye_pos is rebuilt from local_eye_offset.
// Pre-1.5 clients still fire from the stock eye height; reproduce it for them. Orient is yaw-only, so y-only.
CallHook<void(rf::Entity*)> lag_comp_shooter_eye_pos_legacy_client_hook{
0x004264C3,
[](rf::Entity* ep) {
lag_comp_shooter_eye_pos_legacy_client_hook.call_target(ep);
if (ep->local_player && !is_player_minimum_af_client_version(ep->local_player, 1, 5, 0)) {
auto it = g_legacy_eye_offset_delta_y.find(ep->info2);
if (it != g_legacy_eye_offset_delta_y.end()) {
ep->eye_pos.y += it->second;
}
}
},
};

void entity_do_patch()
{
//player_create_entity_patch.install(); // force team skin experiment
Expand Down Expand Up @@ -762,6 +830,10 @@ void entity_do_patch()
// Cancel movement input when escape menu is open in multiplayer
clear_stale_movement_input_injection.install();

// Fix MP camera height on miner1/merc skeletons (eye offset sampled from wrong idle pose)
entity_info_setup_bone_damage_modifiers_hook.install();
lag_comp_shooter_eye_pos_legacy_client_hook.install();

// Commands
sp_exposuredamage_cmd.register_cmd();
cl_gorelevel_cmd.register_cmd();
Expand Down
Loading