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
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ Version 1.4.0 (Lupin): Not yet released
- Add detection of manually loaded levels
- Highlight an active level in a server's rotation via background color instead of text color

[@nickalreadyinuse](https://github.com/nickalreadyinuse)
- Add hybrid (sphere, capsule, cylinder) hitbox system with bone tracking for torso and head

[@AL2009man](https://github.com/AL2009man)
- Add support for binding controls to additional mouse buttons and `Alt` keys

Expand Down
285 changes: 285 additions & 0 deletions game_patch/debug/debug.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
#include <cmath>
#include <algorithm>
#include "debug_internal.h"
#include <patch_common/FunHook.h>
#include <patch_common/MemUtils.h>
#include <xlog/xlog.h>
#include "../os/console.h"
#include "../rf/entity.h"
#include "../rf/vmesh.h"
#include "../rf/player/player.h"
#include "../multi/multi.h"

#define DEBUG_PERF 1

Expand Down Expand Up @@ -201,9 +208,287 @@ void debug_init()
#endif
}

static void draw_3d_capsule_general(const rf::Vector3& cap_a, const rf::Vector3& cap_b, float radius)
{
auto mode = rf::gr::Mode{
rf::gr::TEXTURE_SOURCE_NONE,
rf::gr::COLOR_SOURCE_VERTEX,
rf::gr::ALPHA_SOURCE_VERTEX,
rf::gr::ALPHA_BLEND_NONE,
rf::gr::ZBUFFER_TYPE_FULL,
rf::gr::FOG_NOT_ALLOWED,
};

rf::Vector3 axis = cap_b - cap_a;
float axis_len = axis.len();

constexpr int num_segments = 24;
constexpr int half_arc = num_segments / 4;
constexpr float two_pi = 6.2831853f;
constexpr float half_pi = 1.5707963f;

rf::Vector3 v_axis;
if (axis_len > 1e-6f) {
v_axis = axis * (1.0f / axis_len);
}
else {
v_axis = {0.0f, 1.0f, 0.0f};
}

rf::Vector3 helper = (std::abs(v_axis.y) < 0.9f)
? rf::Vector3{0.0f, 1.0f, 0.0f}
: rf::Vector3{1.0f, 0.0f, 0.0f};
rf::Vector3 u_axis = v_axis.cross(helper);
u_axis.normalize();
rf::Vector3 w_axis = v_axis.cross(u_axis);

rf::Vector3 a_pts[num_segments];
rf::Vector3 b_pts[num_segments];

for (int i = 0; i < num_segments; i++) {
float angle = two_pi * static_cast<float>(i) / static_cast<float>(num_segments);
rf::Vector3 offset = u_axis * (radius * std::cos(angle)) + w_axis * (radius * std::sin(angle));
a_pts[i] = cap_a + offset;
b_pts[i] = cap_b + offset;
}

for (int i = 0; i < num_segments; i++) {
int next = (i + 1) % num_segments;
rf::gr::line_vec(a_pts[i], a_pts[next], mode);
rf::gr::line_vec(b_pts[i], b_pts[next], mode);

if (i % (num_segments / 4) == 0) {
rf::gr::line_vec(a_pts[i], b_pts[i], mode);
}
}

auto draw_hemisphere_arc = [&](const rf::Vector3& center, float sign, const rf::Vector3& arc_dir) {
rf::Vector3 prev;
for (int i = 0; i <= half_arc; i++) {
float phi = half_pi * static_cast<float>(i) / static_cast<float>(half_arc);
float r_eq = radius * std::cos(phi);
float r_ax = radius * std::sin(phi) * sign;
rf::Vector3 pt = center + arc_dir * r_eq + v_axis * r_ax;
if (i > 0)
rf::gr::line_vec(prev, pt, mode);
prev = pt;
}
};

draw_hemisphere_arc(cap_a, -1.0f, u_axis);
draw_hemisphere_arc(cap_a, -1.0f, w_axis);
draw_hemisphere_arc(cap_a, -1.0f, -u_axis);
draw_hemisphere_arc(cap_a, -1.0f, -w_axis);

draw_hemisphere_arc(cap_b, 1.0f, u_axis);
draw_hemisphere_arc(cap_b, 1.0f, w_axis);
draw_hemisphere_arc(cap_b, 1.0f, -u_axis);
draw_hemisphere_arc(cap_b, 1.0f, -w_axis);
}

static void draw_3d_cylinder_general(const rf::Vector3& cyl_a, const rf::Vector3& cyl_b, float radius)
{
auto mode = rf::gr::Mode{
rf::gr::TEXTURE_SOURCE_NONE,
rf::gr::COLOR_SOURCE_VERTEX,
rf::gr::ALPHA_SOURCE_VERTEX,
rf::gr::ALPHA_BLEND_NONE,
rf::gr::ZBUFFER_TYPE_FULL,
rf::gr::FOG_NOT_ALLOWED,
};

rf::Vector3 axis = cyl_b - cyl_a;
float axis_len = axis.len();

constexpr int num_segments = 24;
constexpr float two_pi = 6.2831853f;

rf::Vector3 v_axis;
if (axis_len > 1e-6f) {
v_axis = axis * (1.0f / axis_len);
}
else {
v_axis = {0.0f, 1.0f, 0.0f};
}

rf::Vector3 helper = (std::abs(v_axis.y) < 0.9f)
? rf::Vector3{0.0f, 1.0f, 0.0f}
: rf::Vector3{1.0f, 0.0f, 0.0f};
rf::Vector3 u_axis = v_axis.cross(helper);
u_axis.normalize();
rf::Vector3 w_axis = v_axis.cross(u_axis);

rf::Vector3 a_pts[num_segments];
rf::Vector3 b_pts[num_segments];

for (int i = 0; i < num_segments; i++) {
float angle = two_pi * static_cast<float>(i) / static_cast<float>(num_segments);
rf::Vector3 offset = u_axis * (radius * std::cos(angle)) + w_axis * (radius * std::sin(angle));
a_pts[i] = cyl_a + offset;
b_pts[i] = cyl_b + offset;
}

for (int i = 0; i < num_segments; i++) {
int next = (i + 1) % num_segments;
// Rings at each end
rf::gr::line_vec(a_pts[i], a_pts[next], mode);
rf::gr::line_vec(b_pts[i], b_pts[next], mode);

// Connecting lines at cardinal points
if (i % (num_segments / 4) == 0) {
rf::gr::line_vec(a_pts[i], b_pts[i], mode);
}
}

// Cross-lines on disc caps
rf::gr::line_vec(a_pts[0], a_pts[num_segments / 2], mode);
rf::gr::line_vec(a_pts[num_segments / 4], a_pts[3 * num_segments / 4], mode);
rf::gr::line_vec(b_pts[0], b_pts[num_segments / 2], mode);
rf::gr::line_vec(b_pts[num_segments / 4], b_pts[3 * num_segments / 4], mode);
}

static void draw_3d_aabb(const rf::Vector3& bbox_min, const rf::Vector3& bbox_max)
{
auto mode = rf::gr::Mode{
rf::gr::TEXTURE_SOURCE_NONE,
rf::gr::COLOR_SOURCE_VERTEX,
rf::gr::ALPHA_SOURCE_VERTEX,
rf::gr::ALPHA_BLEND_NONE,
rf::gr::ZBUFFER_TYPE_FULL,
rf::gr::FOG_NOT_ALLOWED,
};

// 8 corners of the box
rf::Vector3 c[8] = {
{bbox_min.x, bbox_min.y, bbox_min.z},
{bbox_max.x, bbox_min.y, bbox_min.z},
{bbox_max.x, bbox_min.y, bbox_max.z},
{bbox_min.x, bbox_min.y, bbox_max.z},
{bbox_min.x, bbox_max.y, bbox_min.z},
{bbox_max.x, bbox_max.y, bbox_min.z},
{bbox_max.x, bbox_max.y, bbox_max.z},
{bbox_min.x, bbox_max.y, bbox_max.z},
};

// Bottom face
rf::gr::line_vec(c[0], c[1], mode);
rf::gr::line_vec(c[1], c[2], mode);
rf::gr::line_vec(c[2], c[3], mode);
rf::gr::line_vec(c[3], c[0], mode);
// Top face
rf::gr::line_vec(c[4], c[5], mode);
rf::gr::line_vec(c[5], c[6], mode);
rf::gr::line_vec(c[6], c[7], mode);
rf::gr::line_vec(c[7], c[4], mode);
// Vertical edges
rf::gr::line_vec(c[0], c[4], mode);
rf::gr::line_vec(c[1], c[5], mode);
rf::gr::line_vec(c[2], c[6], mode);
rf::gr::line_vec(c[3], c[7], mode);
}

// Fraction of the bbox height by which the engine lowers a crouched entity's bbox_max.y (matches
// the FMUL constant at 0x005893C0 in the stock collision path). Used to reconstruct the same bbox
// the server hit-tests against before handing it to compute_hitbox_geometry().
static constexpr float k_crouch_bbox_top_fraction = 0.5f;

static void render_hitboxes()
{
if (!g_dbg_hitboxes && !g_dbg_cspheres)
return;

auto& multi_entity_bbox_size = addr_as_ref<rf::Vector3>(0x007C6A70);
auto& server_info = get_af_server_info();
bool legacy = server_info ? server_info->legacy_hitboxes : g_alpine_server_config.legacy_hitboxes;

rf::Entity* ep = rf::entity_list.next;
while (ep != &rf::entity_list) {
rf::Entity* entity = ep;
ep = ep->next;

if (!entity->vmesh)
continue;

if (rf::local_player && entity->handle == rf::local_player->entity_handle)
continue;

// Hybrid (or legacy) hitbox volume — independent toggle: `debug hitbox`
if (g_dbg_hitboxes) {
// Match engine bbox computation: entity->pos ± half_size,
// then for crouching only bbox_max.y is lowered (feet stay on floor)
rf::Vector3 half_size = multi_entity_bbox_size;
rf::Vector3 bbox_min = entity->pos - half_size;
rf::Vector3 bbox_max = entity->pos + half_size;
bool crouching = rf::entity_is_crouching(entity);
if (crouching) {
bbox_max.y -= multi_entity_bbox_size.y * k_crouch_bbox_top_fraction;
}

if (legacy) {
// Legacy mode: draw the AABB that the engine actually uses for collision
rf::gr::set_color(255, 128, 0, 255);
draw_3d_aabb(bbox_min, bbox_max);
}
else {
// Compute the exact same volume the server hit-tests (shared with the collision path).
HitboxGeometry geo = compute_hitbox_geometry(entity, bbox_min, bbox_max);

// Lower capsule (green)
rf::gr::set_color(0, 255, 0, 255);
draw_3d_capsule_general(geo.lower_bot, geo.lower_top, geo.radius);

if (geo.split) {
// Torso cylinder (cyan)
rf::gr::set_color(0, 255, 255, 255);
draw_3d_cylinder_general(geo.upper_a, geo.upper_b, geo.radius);

// Head sphere (magenta)
if (geo.has_head) {
rf::gr::set_color(255, 0, 255, 255);
auto mode = rf::gr::Mode{
rf::gr::TEXTURE_SOURCE_NONE,
rf::gr::COLOR_SOURCE_VERTEX,
rf::gr::ALPHA_SOURCE_VERTEX,
rf::gr::ALPHA_BLEND_NONE,
rf::gr::ZBUFFER_TYPE_FULL,
rf::gr::FOG_NOT_ALLOWED,
};
rf::gr::sphere(geo.head_pos, geo.head_radius, mode);
}
}
}
}

// Engine collision spheres (yellow) — independent toggle: `debug cspheres`
if (g_dbg_cspheres) {
int num_cspheres = rf::vmesh_get_num_cspheres(entity->vmesh);
for (int i = 0; i < num_cspheres; i++) {
rf::Vector3 sphere_pos;
if (rf::vmesh_get_csphere_pos(entity->vmesh, i, &sphere_pos, &entity->pos, &entity->orient)) {
rf::Vector3 local_pos;
float sphere_radius;
if (rf::vmesh_get_csphere(entity->vmesh, i, &local_pos, &sphere_radius)) {
rf::gr::set_color(255, 255, 0, 180);
auto mode = rf::gr::Mode{
rf::gr::TEXTURE_SOURCE_NONE,
rf::gr::COLOR_SOURCE_VERTEX,
rf::gr::ALPHA_SOURCE_VERTEX,
rf::gr::ALPHA_BLEND_ALPHA,
rf::gr::ZBUFFER_TYPE_FULL,
rf::gr::FOG_NOT_ALLOWED,
};
rf::gr::sphere(sphere_pos, sphere_radius, mode);
}
}
}
}
}
}

void debug_render()
{
debug_cmd_render();
render_hitboxes();
}

void debug_render_ui()
Expand Down
6 changes: 5 additions & 1 deletion game_patch/debug/debug_cmd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ struct DebugFlagDesc
bool g_dbg_geometry_rendering_stats = false;
bool g_dbg_static_lights = false;
bool g_dbg_particle_emitters = false;
bool g_dbg_hitboxes = false;
bool g_dbg_cspheres = false;

DebugFlagDesc g_debug_flags[] = {
{addr_as_ref<bool>(0x0062F3AA), "thruster"},
Expand Down Expand Up @@ -51,6 +53,8 @@ DebugFlagDesc g_debug_flags[] = {
{addr_as_ref<bool>(0x009BB5A4), "lightmap", true}, // show_lightmaps
{addr_as_ref<bool>(0x009BB5A8), "nolightmap", true}, // fullbright
{addr_as_ref<bool>(0x009BB5B0), "show_invisible_faces", true},
{g_dbg_hitboxes, "hitbox", false, true},
{g_dbg_cspheres, "cspheres", false, true},
};

ConsoleCommand2 debug_cmd{
Expand Down Expand Up @@ -78,7 +82,7 @@ ConsoleCommand2 debug_cmd{
nullptr,
"debug [thruster | light | light2 | push_climb_reg | geo_reg | glass | mover | ignite | movemode | perf |\n"
"perfbar | waypoint | network | particlestats | weapon | event | trigger | objrender | roomstats | trans |\n"
"room | portal | lightmap | nolightmap | show_invisible_faces]",
"room | portal | lightmap | nolightmap | show_invisible_faces | hitbox | cspheres]",
};
Comment thread
GooberRF marked this conversation as resolved.

void debug_cmd_multi_init()
Expand Down
2 changes: 2 additions & 0 deletions game_patch/debug/debug_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ class DebugNameValueBoxTwoColumns
StringBuffer<2048> value_buffer_;
};

extern bool g_dbg_hitboxes;
extern bool g_dbg_cspheres;
void debug_cmd_init();
void debug_cmd_render();
void debug_cmd_render_ui();
Expand Down
1 change: 1 addition & 0 deletions game_patch/main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ FunHook<void(bool)> level_init_post_hook{
populate_world_hud_sprite_events();
populate_fullscreen_overlay_events();
reset_achievement_state_info();
hitbox_reset_head_csphere_cache(); // drop per-model head-csphere cache (mesh pointers change per level)
multi_level_init_post_gametypes();
apply_geoable_flags();
apply_breakable_materials();
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 @@ -1265,6 +1265,10 @@ static void apply_known_key_in_order(AlpineServerConfig& cfg, const std::string&
if (auto v = node.value<bool>())
cfg.allow_outlines_xray = *v;
}
else if (key == "legacy_hitboxes") {
if (auto v = node.value<bool>())
cfg.legacy_hitboxes = *v;
}
}

// apply base config toml tables
Expand Down Expand Up @@ -2160,6 +2164,7 @@ void print_alpine_dedicated_server_config_info(std::string& output, bool verbose
std::format_to(iter, " SP-style damage calculation: {}\n", cfg.use_sp_damage_calculation);
std::format_to(iter, " Allow outlines: {}\n", cfg.allow_outlines);
std::format_to(iter, " Allow outlines xray: {}\n", cfg.allow_outlines_xray);
std::format_to(iter, " Legacy hitboxes: {}\n", cfg.legacy_hitboxes);

// inactivity
std::format_to(iter, " Identify inactive players: {}\n", cfg.inactivity_config.enabled);
Expand Down
Loading
Loading