summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-27 13:19:00 +0100
committersalmonik <salmonik@tuta.io>2026-10-27 13:22:27 +0100
commitbb99c978c5f982e80a3007101fd61fe23635e242 (patch)
tree749acafe3d2c4fcffeb2060c7a4a92c49aa69407 /src
parentSpeed graph: show current speed at the top-right (diff)
downloadhl2h-bb99c978c5f982e80a3007101fd61fe23635e242.tar.gz
hl2h-bb99c978c5f982e80a3007101fd61fe23635e242.zip
Jumpbug: automatic duck+jump on damaging falls
- Predict the landing speed from the current vertical velocity and a swept standing-hull ground trace; engage only when it exceeds PLAYER_MAX_SAFE_FALL_SPEED. - Press duck one command before the unduck window so FinishDuck's 36-unit origin raise places the feet ~37 units above the floor. - On the next command release duck and press jump: PlayerMove runs Duck() before CheckJumpButton, so FinishUnDuck sets ground, the jump executes, and CheckFalling never sees ground for the fall-damage check. - Measure the per-command fall from consecutive airborne origins instead of a fixed tick interval; clear IN_JUMP while waiting so the pogo latch is clear. - New Movement -> Jumpbug -> Active toggle (default on), config [jumpbug].
Diffstat (limited to '')
-rw-r--r--src/hl2h.cpp186
-rw-r--r--src/hl2h.h8
-rw-r--r--src/menu.cpp28
-rw-r--r--src/vischeck.cpp45
4 files changed, 252 insertions, 15 deletions
diff --git a/src/hl2h.cpp b/src/hl2h.cpp
index 4947b18..125d94e 100644
--- a/src/hl2h.cpp
+++ b/src/hl2h.cpp
@@ -166,7 +166,184 @@ static void apply_autostrafe(CUserCmd *cmd, const float *vel) {
166 cmd->sidemove = (dx * rx + dy * ry) * 450.0f; 166 cmd->sidemove = (dx * rx + dy * ry) * 450.0f;
167} 167}
168 168
169static void do_move(int seq) { 169// ---------------------------------------------------------------------------
170// jumpbug
171// ---------------------------------------------------------------------------
172//
173// Source jumpbug: while falling, hold duck; on the frame where unducking would
174// leave the standing hull within CategorizePosition's 2-unit ground probe,
175// release duck and press jump. PlayerMove calls Duck() before FullWalkMove's
176// CheckJumpButton, so FinishUnDuck -> CategorizePosition sets the ground
177// entity, the jump executes in the same frame, and CheckFalling never sees
178// ground for the fall-damage test.
179//
180// In air, FinishDuck raises the feet by 36 units (the standing hull is 72
181// tall, the duck hull 36). We exploit that as a one-frame phase shift: press
182// duck when the standing distance x to the floor is about `drop` units, so
183// after this frame's fall the ducked feet sit ~37 units above the floor (the
184// middle of the [36, 38] unduck window) and the next frame can jump.
185
186static constexpr float JB_SAFE_FALL = 526.5f;
187static constexpr float JB_GRAVITY = 600.0f;
188static constexpr float JB_DUCK_RAISE = 36.0f;
189
190static int jb_state = 0; // 0 idle, 1 ducked/waiting for the unduck window
191static int jb_local = -1;
192static float jb_prev_z = 0.0f;
193static bool jb_prev_valid = false;
194static bool jb_prev_ducked = false;
195static float jb_drop = 0.0f; // last measured vertical fall per command
196static int jb_logs = 0;
197static bool jb_tracked = false;
198
199static bool game_running() {
200 if (!g_iface.engine)
201 return true;
202 using BoolFn = bool (*)(void *);
203 if (!vfunc<BoolFn>(g_iface.engine, 26)(g_iface.engine))
204 return false;
205 if (vfunc<BoolFn>(g_iface.engine, 84)(g_iface.engine))
206 return false;
207 return true;
208}
209
210static void jumpbug_reset() {
211 jb_state = 0;
212 jb_prev_valid = false;
213 jb_tracked = false;
214}
215
216static bool jumpbug_apply(CUserCmd *cmd, int local, void *ent, int flags,
217 const float *vel, float frametime) {
218 if (!feature_active(FEAT_JUMPBUG) || !g_iface.engine ||
219 g_net.origin < 0 || g_net.velocity < 0) {
220 jumpbug_reset();
221 return false;
222 }
223 if (!game_running()) {
224 jumpbug_reset();
225 return false;
226 }
227
228 bool onground = (flags & FL_ONGROUND) != 0;
229 bool inwater = (flags & FL_INWATER) != 0;
230 bool ducked = (flags & FL_DUCKING) != 0;
231 uintptr_t e = (uintptr_t)ent;
232 int movetype =
233 g_net.movetype >= 0 ? *(int *)(e + g_net.movetype) : MOVETYPE_WALK;
234
235 if (onground || inwater || movetype != MOVETYPE_WALK ||
236 (g_net.health >= 0 && *(int *)(e + g_net.health) <= 0)) {
237 jumpbug_reset();
238 return false;
239 }
240
241 Vector origin{*(float *)(e + g_net.origin),
242 *(float *)(e + g_net.origin + 4),
243 *(float *)(e + g_net.origin + 8)};
244 float vz = vel[2];
245
246 if (local != jb_local) {
247 jb_local = local;
248 jumpbug_reset();
249 jb_drop = 0.0f;
250 jb_logs = 0;
251 }
252
253 // Measure the previous command's fall. Both endpoints must be airborne and
254 // standing: a duck transition moves the origin by 36 units and would
255 // otherwise look like a huge fall.
256 if (jb_prev_valid && !jb_prev_ducked && !ducked) {
257 float d = jb_prev_z - origin.z;
258 if (d > 0.05f && d < 250.0f)
259 jb_drop = d;
260 }
261 jb_prev_valid = true;
262 jb_prev_z = origin.z;
263 jb_prev_ducked = ducked;
264
265 if (vz > -10.0f) { // not falling
266 jb_state = 0;
267 jb_tracked = false;
268 return false;
269 }
270
271 float x = ground_distance(origin, ent, 2048.0f);
272 if (x < 0.0f) { // no standable surface below
273 jb_state = 0;
274 jb_tracked = false;
275 return false;
276 }
277 float impact = sqrtf(vz * vz + 2.0f * JB_GRAVITY * x);
278 if (impact <= JB_SAFE_FALL + 0.5f) {
279 jb_tracked = false;
280 jb_state = 0;
281 return false;
282 }
283 if (!jb_tracked) {
284 jb_tracked = true;
285 logf("jumpbug: tracking x=%.1f vz=%.1f impact=%.1f", x, vz, impact);
286 }
287
288 if (jb_drop <= 0.05f) {
289 // No measured fall yet. Bias toward the default 15 ms Source tick when
290 // the input frametime is smaller: setting the duck up early leaves us
291 // ducked above the window, where we can wait; setting it up late would
292 // put us below the unduck window and lose the jump.
293 float dt = frametime;
294 if (!(dt > 0.0f && dt < 0.05f) || dt < 0.015f)
295 dt = 0.015f;
296 jb_drop = -vz * dt + 0.5f * JB_GRAVITY * dt * dt;
297 if (jb_drop < 0.05f)
298 jb_drop = 0.05f;
299 }
300
301 int before = cmd->buttons;
302 bool trigger = false;
303
304 if (ducked) {
305 if (x >= JB_DUCK_RAISE && x <= JB_DUCK_RAISE + 2.0f) {
306 // In the window: unduck (CategorizePosition sets ground) and jump in
307 // the same command.
308 cmd->buttons &= ~IN_DUCK;
309 cmd->buttons |= IN_JUMP;
310 trigger = true;
311 jb_state = 0;
312 jb_tracked = false;
313 if (jb_logs < 20) {
314 jb_logs++;
315 logf("jumpbug: trigger x=%.2f drop=%.2f vz=%.1f", x, jb_drop, vz);
316 }
317 } else {
318 // Hold the crouch and wait for the window. Staying ducked is required
319 // for the unduck trick; releasing early would just add a phase shift
320 // and fight a user who is holding crouch.
321 cmd->buttons |= IN_DUCK;
322 jb_state = 1;
323 }
324 } else {
325 if (jb_state == 1)
326 jb_state = 0; // expected to be ducked but is not
327 if (x <= jb_drop + 2.0f) {
328 // Setup frame: raise the feet 36 units here so the next frame starts
329 // near the middle of the unduck window.
330 cmd->buttons |= IN_DUCK;
331 jb_state = 1;
332 if (jb_logs < 20) {
333 jb_logs++;
334 logf("jumpbug: setup x=%.2f drop=%.2f pred=%.2f vz=%.1f", x, jb_drop,
335 x + JB_DUCK_RAISE - jb_drop, vz);
336 }
337 }
338 }
339
340 if (!trigger && (cmd->buttons & IN_JUMP))
341 cmd->buttons &= ~IN_JUMP; // keep the pogo latch clear for the trigger
342
343 return cmd->buttons != before;
344}
345
346static void do_move(int seq, float frametime) {
170 static bool g_echo_sent = false; 347 static bool g_echo_sent = false;
171 if (!g_input || !g_net.ok()) 348 if (!g_input || !g_net.ok())
172 return; 349 return;
@@ -206,6 +383,7 @@ static void do_move(int seq) {
206 int flags = *(int *)(e + g_net.flags); 383 int flags = *(int *)(e + g_net.flags);
207 bool onground = (flags & FL_ONGROUND) != 0; 384 bool onground = (flags & FL_ONGROUND) != 0;
208 bool inwater = (flags & FL_INWATER) != 0; 385 bool inwater = (flags & FL_INWATER) != 0;
386 const float *vel = (const float *)(e + g_net.velocity);
209 387
210 bool bhop_on = feature_active(FEAT_BHOP); 388 bool bhop_on = feature_active(FEAT_BHOP);
211 bool abh_on = feature_active(FEAT_ABH); 389 bool abh_on = feature_active(FEAT_ABH);
@@ -217,7 +395,6 @@ static void do_move(int seq) {
217 lastground = onground; 395 lastground = onground;
218 return; 396 return;
219 } 397 }
220 const float *vel = (const float *)(e + g_net.velocity);
221 float speed = sqrtf(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]); 398 float speed = sqrtf(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]);
222 bool moving = speed > 0.1f; 399 bool moving = speed > 0.1f;
223 bool backmove = false; 400 bool backmove = false;
@@ -260,6 +437,9 @@ static void do_move(int seq) {
260 } 437 }
261 } 438 }
262 439
440 if (jumpbug_apply(cmd, localIdx, ent, flags, vel, frametime))
441 note_modified(cmd, flags, vel);
442
263 if (!feature_active(FEAT_AIMBOT) || !feature_has_bind(FEAT_AIMBOT)) 443 if (!feature_active(FEAT_AIMBOT) || !feature_has_bind(FEAT_AIMBOT))
264 aimbot_reset_lock(); 444 aimbot_reset_lock();
265 if (!feature_active(FEAT_SILENTAIM) || !feature_has_bind(FEAT_SILENTAIM)) 445 if (!feature_active(FEAT_SILENTAIM) || !feature_has_bind(FEAT_SILENTAIM))
@@ -361,7 +541,7 @@ static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) {
361 541
362static bool hkdCreateMove(void *self, int seq, float frametime, bool active) { 542static bool hkdCreateMove(void *self, int seq, float frametime, bool active) {
363 bool ret = oCreateMove(self, seq, frametime, active); 543 bool ret = oCreateMove(self, seq, frametime, active);
364 do_move(seq); 544 do_move(seq, frametime);
365 545
366 // Hook ISurface once: PaintTraverseEx (slot 114) fires every frame; keep 546 // Hook ISurface once: PaintTraverseEx (slot 114) fires every frame; keep
367 // DrawPrintText (slot 22) hooked as a fallback text-draw trigger. 547 // DrawPrintText (slot 22) hooked as a fallback text-draw trigger.
diff --git a/src/hl2h.h b/src/hl2h.h
index fdc88c1..92ae3f1 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -128,8 +128,8 @@ struct CUserCmd {
128// --------------------------------------------------------------------------- 128// ---------------------------------------------------------------------------
129 129
130enum { IN_ATTACK = 1 << 0, IN_JUMP = 1 << 1, IN_DUCK = 1 << 2, IN_USE = 1 << 5 }; 130enum { IN_ATTACK = 1 << 0, IN_JUMP = 1 << 1, IN_DUCK = 1 << 2, IN_USE = 1 << 5 };
131enum { FL_ONGROUND = 1 << 0, FL_INWATER = 1 << 9 }; 131enum { FL_ONGROUND = 1 << 0, FL_DUCKING = 1 << 1, FL_INWATER = 1 << 9 };
132enum { MOVETYPE_NOCLIP = 8, MOVETYPE_LADDER = 9 }; 132enum { MOVETYPE_WALK = 2, MOVETYPE_NOCLIP = 8, MOVETYPE_LADDER = 9 };
133 133
134// --------------------------------------------------------------------------- 134// ---------------------------------------------------------------------------
135// vector math 135// vector math
@@ -152,7 +152,8 @@ enum FeatureId {
152 FEAT_ESP = 2, 152 FEAT_ESP = 2,
153 FEAT_AIMBOT = 3, 153 FEAT_AIMBOT = 3,
154 FEAT_TRIGGERBOT = 4, 154 FEAT_TRIGGERBOT = 4,
155 FEAT_SILENTAIM = 5 155 FEAT_SILENTAIM = 5,
156 FEAT_JUMPBUG = 6
156}; 157};
157enum EspOption { 158enum EspOption {
158 ESP_OPT_SHOW_NEUTRAL = 0, 159 ESP_OPT_SHOW_NEUTRAL = 0,
@@ -200,6 +201,7 @@ bool world_to_screen(const Vector &w, float &sx, float &sy);
200void set_silent_restore(const QAngle &angles); 201void set_silent_restore(const QAngle &angles);
201bool is_visible(const Vector &eye, const Vector &point); 202bool is_visible(const Vector &eye, const Vector &point);
202void *trace_entity(const Vector &start, const Vector &end, void *ignore); 203void *trace_entity(const Vector &start, const Vector &end, void *ignore);
204float ground_distance(const Vector &origin, void *ignore, float max_dist);
203 205
204// bones.cpp — client-side animated skeleton / hitbox access (see there) 206// bones.cpp — client-side animated skeleton / hitbox access (see there)
205bool bones_update(int idx, void *ent); 207bool bones_update(int idx, void *ent);
diff --git a/src/menu.cpp b/src/menu.cpp
index 3e20cb3..0ed89d4 100644
--- a/src/menu.cpp
+++ b/src/menu.cpp
@@ -62,6 +62,7 @@ static FeatureState g_bhop{true, 0};
62static bool g_bhop_autostrafe = true; 62static bool g_bhop_autostrafe = true;
63 63
64static FeatureState g_abh{true, 0}; 64static FeatureState g_abh{true, 0};
65static FeatureState g_jumpbug{true, 0};
65static FeatureState g_esp{true, 0}; 66static FeatureState g_esp{true, 0};
66static FeatureState g_aimbot{true, 604}; // default Mouse5, rebindable in menu 67static FeatureState g_aimbot{true, 604}; // default Mouse5, rebindable in menu
67static FeatureState g_triggerbot{true, 603}; // default Mouse4 68static FeatureState g_triggerbot{true, 603}; // default Mouse4
@@ -96,6 +97,8 @@ static MenuNode g_bhop_active{NODE_TOGGLE, "Active", false, 0, {}, &g_bhop.activ
96static MenuNode g_bhop_autostrafe_node{NODE_TOGGLE, "Uni-directional auto-strafe", false, 0, {}, &g_bhop_autostrafe, nullptr, nullptr}; 97static MenuNode g_bhop_autostrafe_node{NODE_TOGGLE, "Uni-directional auto-strafe", false, 0, {}, &g_bhop_autostrafe, nullptr, nullptr};
97static MenuNode g_abh_feat{NODE_FEATURE, "ABH", false, 1, {}, nullptr, nullptr, nullptr}; 98static MenuNode g_abh_feat{NODE_FEATURE, "ABH", false, 1, {}, nullptr, nullptr, nullptr};
98static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active, nullptr, nullptr}; 99static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active, nullptr, nullptr};
100static MenuNode g_jumpbug_feat{NODE_FEATURE, "Jumpbug", false, 1, {}, nullptr, nullptr, nullptr};
101static MenuNode g_jumpbug_active{NODE_TOGGLE, "Active", false, 0, {}, &g_jumpbug.active, nullptr, nullptr};
99static MenuNode g_esp_feat{NODE_FEATURE, "ESP", false, 1, {}, nullptr, nullptr, nullptr}; 102static MenuNode g_esp_feat{NODE_FEATURE, "ESP", false, 1, {}, nullptr, nullptr, nullptr};
100static MenuNode g_esp_active{NODE_TOGGLE, "Active", false, 0, {}, &g_esp.active, nullptr, nullptr}; 103static MenuNode g_esp_active{NODE_TOGGLE, "Active", false, 0, {}, &g_esp.active, nullptr, nullptr};
101static MenuNode g_esp_opt_nodes[ESP_OPT_COUNT] = { 104static MenuNode g_esp_opt_nodes[ESP_OPT_COUNT] = {
@@ -145,6 +148,9 @@ static void build_tree() {
145 g_bhop_feat.children[1] = &g_bhop_autostrafe_node; 148 g_bhop_feat.children[1] = &g_bhop_autostrafe_node;
146 g_bhop_feat.child_count = 2; 149 g_bhop_feat.child_count = 2;
147 g_abh_feat.children[0] = &g_abh_active; 150 g_abh_feat.children[0] = &g_abh_active;
151 g_abh_feat.child_count = 1;
152 g_jumpbug_feat.children[0] = &g_jumpbug_active;
153 g_jumpbug_feat.child_count = 1;
148 g_esp_feat.children[0] = &g_esp_active; 154 g_esp_feat.children[0] = &g_esp_active;
149 g_aimbot_feat.children[0] = &g_aimbot_active; 155 g_aimbot_feat.children[0] = &g_aimbot_active;
150 g_aimbot_feat.children[1] = &g_aimbot_bind; 156 g_aimbot_feat.children[1] = &g_aimbot_bind;
@@ -169,11 +175,12 @@ static void build_tree() {
169 g_esp_feat.child_count = ESP_OPT_COUNT + 1; 175 g_esp_feat.child_count = ESP_OPT_COUNT + 1;
170 g_root.children[0] = &g_bhop_feat; 176 g_root.children[0] = &g_bhop_feat;
171 g_root.children[1] = &g_abh_feat; 177 g_root.children[1] = &g_abh_feat;
172 g_root.children[2] = &g_esp_feat; 178 g_root.children[2] = &g_jumpbug_feat;
173 g_root.children[3] = &g_aimbot_feat; 179 g_root.children[3] = &g_esp_feat;
174 g_root.children[4] = &g_triggerbot_feat; 180 g_root.children[4] = &g_aimbot_feat;
175 g_root.children[5] = &g_silentaim_feat; 181 g_root.children[5] = &g_triggerbot_feat;
176 g_root.child_count = 6; 182 g_root.children[6] = &g_silentaim_feat;
183 g_root.child_count = 7;
177} 184}
178 185
179static int g_depth[48]; // depth of each visible row (for indentation) 186static int g_depth[48]; // depth of each visible row (for indentation)
@@ -295,6 +302,7 @@ static void config_save() {
295 fprintf(f, "[bhop]\nactive=%d\nbind=%d\nautostrafe=%d\n", g_bhop.active, 302 fprintf(f, "[bhop]\nactive=%d\nbind=%d\nautostrafe=%d\n", g_bhop.active,
296 g_bhop.bind, g_bhop_autostrafe); 303 g_bhop.bind, g_bhop_autostrafe);
297 fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind); 304 fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind);
305 fprintf(f, "[jumpbug]\nactive=%d\n", g_jumpbug.active);
298 fprintf(f, "[aimbot]\nactive=%d\nbind=%d\nsmooth=%d\nvischeck=%d\n" 306 fprintf(f, "[aimbot]\nactive=%d\nbind=%d\nsmooth=%d\nvischeck=%d\n"
299 "fov=%d\nfov_circle=%d\n" 307 "fov=%d\nfov_circle=%d\n"
300 "bone_head=%d\nbone_torso=%d\nbone_larm=%d\nbone_rarm=%d\n" 308 "bone_head=%d\nbone_torso=%d\nbone_larm=%d\nbone_rarm=%d\n"
@@ -332,6 +340,8 @@ static void config_load() {
332 cur = &g_bhop; 340 cur = &g_bhop;
333 else if (strstr(line, "[abh]")) 341 else if (strstr(line, "[abh]"))
334 cur = &g_abh; 342 cur = &g_abh;
343 else if (strstr(line, "[jumpbug]"))
344 cur = &g_jumpbug;
335 else if (strstr(line, "[esp]")) 345 else if (strstr(line, "[esp]"))
336 cur = &g_esp; 346 cur = &g_esp;
337 else if (strstr(line, "[aimbot]")) 347 else if (strstr(line, "[aimbot]"))
@@ -516,10 +526,10 @@ static bool key_pressed(int sc) {
516// feature gate queried from hl2h.cpp do_move(): feature must be Active AND 526// feature gate queried from hl2h.cpp do_move(): feature must be Active AND
517// its bind key held (always-on when unbound). 527// its bind key held (always-on when unbound).
518static FeatureState *feature_state(int feature) { 528static FeatureState *feature_state(int feature) {
519 static FeatureState *feats[6] = {&g_bhop, &g_abh, &g_esp, 529 static FeatureState *feats[7] = {&g_bhop, &g_abh, &g_esp,
520 &g_aimbot, &g_triggerbot, 530 &g_aimbot, &g_triggerbot, &g_silentaim,
521 &g_silentaim}; 531 &g_jumpbug};
522 if (feature < 0 || feature > 5) 532 if (feature < 0 || feature > 6)
523 return nullptr; 533 return nullptr;
524 return feats[feature]; 534 return feats[feature];
525} 535}
diff --git a/src/vischeck.cpp b/src/vischeck.cpp
index 428f54f..6d453c5 100644
--- a/src/vischeck.cpp
+++ b/src/vischeck.cpp
@@ -95,6 +95,9 @@ constexpr unsigned MASK_VISIBLE = 0x1u | 0x4000u | 0x80u | 0x2000u;
95// MASK_SHOT = SOLID|MOVEABLE|MONSTER|WINDOW|DEBRIS|HITBOX 95// MASK_SHOT = SOLID|MOVEABLE|MONSTER|WINDOW|DEBRIS|HITBOX
96constexpr unsigned MASK_SHOT = 0x1u | 0x4000u | 0x2000000u | 0x4u | 96constexpr unsigned MASK_SHOT = 0x1u | 0x4000u | 0x2000000u | 0x4u |
97 0x4000000u | 0x40000000u; 97 0x4000000u | 0x40000000u;
98// MASK_PLAYERSOLID = SOLID|MOVEABLE|PLAYERCLIP|WINDOW|MONSTER|GRATE
99constexpr unsigned MASK_PLAYERSOLID =
100 0x1u | 0x4000u | 0x10000u | 0x4u | 0x2000000u | 0x8u;
98 101
99static void make_ray(RayCompat &ray, const Vector &start, const Vector &end) { 102static void make_ray(RayCompat &ray, const Vector &start, const Vector &end) {
100 float dx = end.x - start.x, dy = end.y - start.y, dz = end.z - start.z; 103 float dx = end.x - start.x, dy = end.y - start.y, dz = end.z - start.z;
@@ -107,6 +110,21 @@ static void make_ray(RayCompat &ray, const Vector &start, const Vector &end) {
107 ray.isSwept = (dx * dx + dy * dy + dz * dz) > 0.0f; 110 ray.isSwept = (dx * dx + dy * dy + dz * dz) > 0.0f;
108} 111}
109 112
113// Swept AABB matching the standing player hull used by CGameMovement.
114// Ray_t::Init shifts m_Start to the box center and stores the negated center
115// offset in m_StartOffset. With mins=(-16,-16,0), maxs=(16,16,72), the
116// extents are (16,16,36) and m_StartOffset is (0,0,-36).
117static void make_standing_hull_ray(RayCompat &ray, const Vector &origin,
118 float distance) {
119 ray = RayCompat{};
120 ray.start = {origin.x, origin.y, origin.z + 36.0f, 0.0f};
121 ray.delta = {0.0f, 0.0f, -distance, 0.0f};
122 ray.startOffset = {0.0f, 0.0f, -36.0f, 0.0f};
123 ray.extents = {16.0f, 16.0f, 36.0f, 0.0f};
124 ray.isRay = false;
125 ray.isSwept = true;
126}
127
110} // namespace 128} // namespace
111 129
112bool is_visible(const Vector &eye, const Vector &point) { 130bool is_visible(const Vector &eye, const Vector &point) {
@@ -147,3 +165,30 @@ void *trace_entity(const Vector &start, const Vector &end, void *ignore) {
147 return nullptr; 165 return nullptr;
148 return tr.ent; 166 return tr.ent;
149} 167}
168
169// Vertical distance from the standing player hull's feet to the first
170// standable surface below. Matches CategorizePosition's 2-unit probe and its
171// plane.normal.z >= 0.7 rule, but sweeps much farther so the caller can
172// predict the landing. Returns -1 when there is no usable ground.
173float ground_distance(const Vector &origin, void *ignore, float max_dist) {
174 if (!g_iface.enginetrace || max_dist <= 0.0f)
175 return -1.0f;
176 RayCompat ray;
177 make_standing_hull_ray(ray, origin, max_dist);
178 GameTraceCompat tr{};
179 EntityFilter filter(ignore);
180 using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *,
181 GameTraceCompat *);
182 vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray,
183 MASK_PLAYERSOLID, &filter, &tr);
184 if (tr.startsolid || tr.fraction >= 1.0f || tr.normal.z < 0.7f) {
185 static int solid_logs = 0;
186 if (tr.startsolid && solid_logs < 4) {
187 solid_logs++;
188 logf("ground trace: startsolid at (%.1f %.1f %.1f)", origin.x,
189 origin.y, origin.z);
190 }
191 return -1.0f;
192 }
193 return tr.fraction * max_dist;
194}