summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 23:46:58 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 23:46:58 +0100
commita224cc451233a487a3d931954b125f6e1736e710 (patch)
tree5cd1ed10848b6510ff92dccbe6f7b3cd5f7348f4
parentCharger juice: resolve per-class offsets (diff)
downloadhl2h-a224cc451233a487a3d931954b125f6e1736e710.tar.gz
hl2h-a224cc451233a487a3d931954b125f6e1736e710.zip
Grenade trajectory: actual grenade state, full-length path, rounder rings
- Prefer the local player's thrown frag over the pre-throw estimate: resolve CBaseGrenade::m_hThrower from the server datamap (client owner handles as fallback), then seed the prediction from the live origin/velocity. The real frag is a spinning VPhysics box with a random Y angular impulse, so bounce points remain approximate; the doc now says so. - Keep 512 path points sampled every other 20 ms step (up to 24 s) instead of 127 points / ~2.5 s, so the line reaches the landing instead of stopping in mid-air. - Radius rings use 36 segments; a segment whose near-plane-clipped endpoint the engine overlay refuses falls back to the local projection, and that projection now uses the configured camera FOV instead of a hardcoded 75 degrees (the wide-FOV ring no longer kinks/drops pieces).
-rw-r--r--CONTEXT.md17
-rw-r--r--src/esp.cpp95
-rw-r--r--src/hl2h.h1
-rw-r--r--src/server.cpp22
4 files changed, 101 insertions, 34 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index d0f89cb..54ca0b4 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -744,9 +744,12 @@ vocabulary / decisions.
744 `m_DmgRadius` (`FIELD_FLOAT`) as fallback, then 300/200/150/300 for 744 `m_DmgRadius` (`FIELD_FLOAT`) as fallback, then 300/200/150/300 for
745 frag/RPG/AR2/bomb. 745 frag/RPG/AR2/bomb.
746- Draws a small marker and a `FRAG 1.2s`-style label; the optional radius ring 746- Draws a small marker and a `FRAG 1.2s`-style label; the optional radius ring
747 is a 24-segment horizontal circle projected at the projectile height, with 747 is a 36-segment horizontal circle projected at the projectile height, with
748 every segment near-plane/screen clipped independently, so it still draws 748 every segment near-plane/screen clipped independently, so it still draws
749 when the grenade itself is off-screen or behind the camera. 749 when the grenade itself is off-screen or behind the camera. Segments whose
750 clipped endpoint the engine overlay refuses fall back to the local
751 projection, which now uses the configured camera FOV (not a hardcoded 75°),
752 so the ring does not kink or drop pieces at wide FOVs.
750 Toggles: Fuse timer, Blast radius, Grenade trajectory 753 Toggles: Fuse timer, Blast radius, Grenade trajectory
751 (`[projectiles] trajectory=1`). 754 (`[projectiles] trajectory=1`).
752- Grenade trajectory: while the frag weapon is active, simulate the throw 755- Grenade trajectory: while the frag weapon is active, simulate the throw
@@ -757,7 +760,15 @@ vocabulary / decisions.
757 normal, and an upward normal with a slow vertical speed marks the landing. 760 normal, and an upward normal with a slow vertical speed marks the landing.
758 The polyline, landing cross and a 300-unit blast-radius ring at the landing 761 The polyline, landing cross and a 300-unit blast-radius ring at the landing
759 are drawn with the same near-plane-clipped projector as the radius ring. 762 are drawn with the same near-plane-clipped projector as the radius ring.
760 Verified on `:99` (the line arcs, bounces off a tunnel wall and lands). 763 The path keeps 512 points (one every other 20 ms step, up to 24 s), so it no
764 longer stops after ~2.5 s. Once a local frag has been thrown it is preferred
765 over the pre-throw estimate: `CBaseGrenade::m_hThrower` (server
766 `FIELD_EHANDLE`, with the client owner handles as fallback) identifies it, and
767 its live position/velocity seed the prediction. Bounces are still
768 approximate: the real frag is a VPhysics box with a random Y angular impulse
769 (`AngularImpulse(600, random(-1200,1200), 0)`), so contact points and
770 post-bounce paths differ between throws from the same spot. Verified on
771 `:99` (pre-throw arc + landing ring; the line now reaches the landing).
761 772
762## Roadmap / TODO 773## Roadmap / TODO
763 774
diff --git a/src/esp.cpp b/src/esp.cpp
index dbe0051..c358b2f 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -70,7 +70,8 @@ static bool project_manual(const Vector &w, float &sx, float &sy) {
70 return false; 70 return false;
71 float x = dx * right[0] + dy * right[1] + dz * right[2]; 71 float x = dx * right[0] + dy * right[1] + dz * right[2];
72 float y = dx * up[0] + dy * up[1] + dz * up[2]; 72 float y = dx * up[0] + dy * up[1] + dz * up[2];
73 float tanfov = tanf(75.0f * 0.5f * PI_F / 180.0f); 73 float fov = g_fov_camera > 10 ? (float)g_fov_camera : 75.0f;
74 float tanfov = tanf(fov * 0.5f * PI_F / 180.0f);
74 sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov); 75 sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov);
75 sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov); 76 sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov);
76 return std::isfinite(sx) && std::isfinite(sy) && fabsf(sx) < 1e7f && 77 return std::isfinite(sx) && std::isfinite(sy) && fabsf(sx) < 1e7f &&
@@ -1986,7 +1987,7 @@ static bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0,
1986 Vector fwd{cp * cy, cp * sy, -sp}; 1987 Vector fwd{cp * cy, cp * sy, -sp};
1987 Vector da{a.x - g_cam_eye.x, a.y - g_cam_eye.y, a.z - g_cam_eye.z}; 1988 Vector da{a.x - g_cam_eye.x, a.y - g_cam_eye.y, a.z - g_cam_eye.z};
1988 Vector db{b.x - g_cam_eye.x, b.y - g_cam_eye.y, b.z - g_cam_eye.z}; 1989 Vector db{b.x - g_cam_eye.x, b.y - g_cam_eye.y, b.z - g_cam_eye.z};
1989 const float nearz = 4.0f; 1990 const float nearz = 5.0f;
1990 float za = da.x * fwd.x + da.y * fwd.y + da.z * fwd.z; 1991 float za = da.x * fwd.x + da.y * fwd.y + da.z * fwd.z;
1991 float zb = db.x * fwd.x + db.y * fwd.y + db.z * fwd.z; 1992 float zb = db.x * fwd.x + db.y * fwd.y + db.z * fwd.z;
1992 if (za <= nearz && zb <= nearz) 1993 if (za <= nearz && zb <= nearz)
@@ -2002,7 +2003,9 @@ static bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0,
2002 b.z + (a.z - b.z) * t}; 2003 b.z + (a.z - b.z) * t};
2003 } 2004 }
2004 float ax, ay, bx, by; 2005 float ax, ay, bx, by;
2005 if (!world_to_screen(ca, ax, ay) || !world_to_screen(cb, bx, by)) 2006 if (!world_to_screen(ca, ax, ay) && !project_manual(ca, ax, ay))
2007 return false;
2008 if (!world_to_screen(cb, bx, by) && !project_manual(cb, bx, by))
2006 return false; 2009 return false;
2007 if (!clip_line2d(ax, ay, bx, by, -8.0f, -8.0f, (float)g_scr_w + 8.0f, 2010 if (!clip_line2d(ax, ay, bx, by, -8.0f, -8.0f, (float)g_scr_w + 8.0f,
2008 (float)g_scr_h + 8.0f)) 2011 (float)g_scr_h + 8.0f))
@@ -2973,6 +2976,7 @@ struct ProjTarget {
2973 Vector origin; 2976 Vector origin;
2974 float fuse; // seconds; < 0 = impact / no fuse 2977 float fuse; // seconds; < 0 = impact / no fuse
2975 float radius; 2978 float radius;
2979 bool owned;
2976}; 2980};
2977 2981
2978static ProjTarget g_projs[32]; 2982static ProjTarget g_projs[32];
@@ -3041,6 +3045,9 @@ static void projs_collect(int local_idx) {
3041 if (!model) 3045 if (!model)
3042 continue; 3046 continue;
3043 bool owned = item_weapon_is_owned(ent); 3047 bool owned = item_weapon_is_owned(ent);
3048 if (!owned && local_idx > 0 &&
3049 server_grenade_thrower(idx) == local_idx)
3050 owned = true;
3044 bool frag_cls = cls && strcasestr(cls, "grenade") && 3051 bool frag_cls = cls && strcasestr(cls, "grenade") &&
3045 !strcasestr(cls, "weapon"); 3052 !strcasestr(cls, "weapon");
3046 bool item_cls = cls && (strcasestr(cls, "item") || 3053 bool item_cls = cls && (strcasestr(cls, "item") ||
@@ -3066,6 +3073,7 @@ static void projs_collect(int local_idx) {
3066 ProjTarget &p = g_projs[g_nprojs++]; 3073 ProjTarget &p = g_projs[g_nprojs++];
3067 p.idx = idx; 3074 p.idx = idx;
3068 p.kind = kind; 3075 p.kind = kind;
3076 p.owned = owned;
3069 p.origin = Vector{*(float *)(e + g_net.origin), 3077 p.origin = Vector{*(float *)(e + g_net.origin),
3070 *(float *)(e + g_net.origin + 4), 3078 *(float *)(e + g_net.origin + 4),
3071 *(float *)(e + g_net.origin + 8)}; 3079 *(float *)(e + g_net.origin + 8)};
@@ -3103,41 +3111,67 @@ static void draw_grenade_trajectory(int local_idx) {
3103 void *le = entitylist_get(local_idx); 3111 void *le = entitylist_get(local_idx);
3104 if (!le) 3112 if (!le)
3105 return; 3113 return;
3106 if (!active_weapon_contains(le, "frag") &&
3107 !active_weapon_contains(le, "w_grenade"))
3108 return;
3109 3114
3110 QAngle va = engine_view_angles(); 3115 Vector src{}, vel{};
3111 float pitch = va.x * PI_F / 180.0f; 3116 bool have = false;
3112 float yaw = va.y * PI_F / 180.0f; 3117 bool actual = false;
3113 float cp = cosf(pitch), sp = sinf(pitch); 3118 // Prefer the local player's thrown frag: the real position and velocity are
3114 float cy = cosf(yaw), sy = sinf(yaw); 3119 // networked, so the preview starts from what is actually flying instead of
3115 Vector fwd{cp * cy, cp * sy, -sp}; 3120 // the pre-throw estimate. The bounce contact points still depend on the
3116 Vector right{sy, -cy, 0.0f}; 3121 // grenade's spin/orientation, so this remains an approximation.
3117 3122 for (int i = 0; i < g_nprojs; i++) {
3118 Vector src{g_cam_eye.x + fwd.x * 18.0f + right.x * 8.0f, 3123 if (g_projs[i].kind != PK_FRAG || !g_projs[i].owned)
3119 g_cam_eye.y + fwd.y * 18.0f + right.y * 8.0f, 3124 continue;
3120 g_cam_eye.z + fwd.z * 18.0f}; 3125 void *gent = entitylist_get(g_projs[i].idx);
3121 Vector vel{}; 3126 if (!gent)
3122 if (g_net.velocity >= 0) { 3127 continue;
3123 const float *pv = (const float *)((uintptr_t)le + g_net.velocity); 3128 src = g_projs[i].origin;
3124 vel = Vector{pv[0], pv[1], pv[2]}; 3129 if (g_net.velocity >= 0) {
3130 const float *pv = (const float *)((uintptr_t)gent + g_net.velocity);
3131 vel = Vector{pv[0], pv[1], pv[2]};
3132 }
3133 have = true;
3134 actual = true;
3135 break;
3136 }
3137 if (!have) {
3138 if (!active_weapon_contains(le, "frag") &&
3139 !active_weapon_contains(le, "w_grenade"))
3140 return;
3141 QAngle va = engine_view_angles();
3142 float pitch = va.x * PI_F / 180.0f;
3143 float yaw = va.y * PI_F / 180.0f;
3144 float cp = cosf(pitch), sp = sinf(pitch);
3145 float cy = cosf(yaw), sy = sinf(yaw);
3146 Vector fwd{cp * cy, cp * sy, -sp};
3147 Vector right{sy, -cy, 0.0f};
3148
3149 src = Vector{g_cam_eye.x + fwd.x * 18.0f + right.x * 8.0f,
3150 g_cam_eye.y + fwd.y * 18.0f + right.y * 8.0f,
3151 g_cam_eye.z + fwd.z * 18.0f};
3152 if (g_net.velocity >= 0) {
3153 const float *pv = (const float *)((uintptr_t)le + g_net.velocity);
3154 vel = Vector{pv[0], pv[1], pv[2]};
3155 }
3156 vel.x += fwd.x * 1200.0f;
3157 vel.y += fwd.y * 1200.0f;
3158 vel.z += fwd.z * 1200.0f + 120.0f; // vForward[2] += 0.1
3159 have = true;
3125 } 3160 }
3126 vel.x += fwd.x * 1200.0f; 3161 if (actual && vel.x * vel.x + vel.y * vel.y + vel.z * vel.z < 100.0f)
3127 vel.y += fwd.y * 1200.0f; 3162 return; // resting grenade: no useful continuation
3128 vel.z += fwd.z * 1200.0f + 120.0f; // vForward[2] += 0.1
3129 3163
3130 const float gravity = 600.0f; 3164 const float gravity = 600.0f;
3131 const float dt = 0.02f; 3165 const float dt = 0.02f;
3132 const float restitution = 0.35f; 3166 const float restitution = 0.35f;
3133 const float friction = 0.35f; 3167 const float friction = 0.35f;
3134 3168
3135 Vector pts[128]; 3169 Vector pts[512];
3136 int n = 0; 3170 int n = 0;
3137 Vector pos = src; 3171 Vector pos = src;
3138 pts[n++] = pos; 3172 pts[n++] = pos;
3139 bool landed = false; 3173 bool landed = false;
3140 for (int step = 0; step < 240 && n < 127; step++) { 3174 for (int step = 0; step < 1200 && n < 511; step++) {
3141 Vector nv = vel; 3175 Vector nv = vel;
3142 nv.z -= gravity * dt; 3176 nv.z -= gravity * dt;
3143 Vector next{pos.x + nv.x * dt, pos.y + nv.y * dt, pos.z + nv.z * dt}; 3177 Vector next{pos.x + nv.x * dt, pos.y + nv.y * dt, pos.z + nv.z * dt};
@@ -3162,7 +3196,10 @@ static void draw_grenade_trajectory(int local_idx) {
3162 } else { 3196 } else {
3163 pos = next; 3197 pos = next;
3164 vel = nv; 3198 vel = nv;
3165 pts[n++] = pos; 3199 // One point every other step covers the whole flight without the old
3200 // 127-point / 2.5-second cut-off.
3201 if ((step & 1) == 0 && n < 511)
3202 pts[n++] = pos;
3166 } 3203 }
3167 } 3204 }
3168 3205
@@ -3185,7 +3222,7 @@ static void draw_grenade_trajectory(int local_idx) {
3185 surf_line(x, y + 4, x, y + 9); 3222 surf_line(x, y + 4, x, y + 9);
3186 } 3223 }
3187 if (landed) { 3224 if (landed) {
3188 const int SEG = 24; 3225 const int SEG = 36;
3189 const float radius = 300.0f; // CBaseGrenade m_DmgRadius 3226 const float radius = 300.0f; // CBaseGrenade m_DmgRadius
3190 surf_color(255, 120, 40, 120); 3227 surf_color(255, 120, 40, 120);
3191 for (int k = 0; k < SEG; k++) { 3228 for (int k = 0; k < SEG; k++) {
@@ -3254,7 +3291,7 @@ void esp_draw_projectiles() {
3254 // off-screen, so it is not gated on center_ok and each segment is clipped 3291 // off-screen, so it is not gated on center_ok and each segment is clipped
3255 // at the near plane independently. 3292 // at the near plane independently.
3256 if (g_proj_radius && p.radius > 1.0f) { 3293 if (g_proj_radius && p.radius > 1.0f) {
3257 const int SEG = 24; 3294 const int SEG = 36;
3258 Vector ring[SEG + 1]; 3295 Vector ring[SEG + 1];
3259 for (int k = 0; k <= SEG; k++) { 3296 for (int k = 0; k <= SEG; k++) {
3260 float a = (float)k * 2.0f * PI_F / (float)SEG; 3297 float a = (float)k * 2.0f * PI_F / (float)SEG;
diff --git a/src/hl2h.h b/src/hl2h.h
index 32cee8f..86573d8 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -319,6 +319,7 @@ int server_player_ammo(int idx, int type);
319uint32_t server_player_weapon(int idx, int slot); 319uint32_t server_player_weapon(int idx, int slot);
320float server_grenade_detonate(int idx); 320float server_grenade_detonate(int idx);
321float server_grenade_radius(int idx); 321float server_grenade_radius(int idx);
322int server_grenade_thrower(int idx);
322int server_item_juice(int idx); 323int server_item_juice(int idx);
323int server_entity_count(); 324int server_entity_count();
324void *server_entity_base(int idx); 325void *server_entity_base(int idx);
diff --git a/src/server.cpp b/src/server.cpp
index f69ab30..cffc24f 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -58,6 +58,7 @@ static int g_srv_mapname = -1;
58static int g_srv_armor = -1; 58static int g_srv_armor = -1;
59static int g_srv_detonate = -1; 59static int g_srv_detonate = -1;
60static int g_srv_grenade_radius = -1; 60static int g_srv_grenade_radius = -1;
61static int g_srv_thrower = -1;
61static int g_srv_juice_suit = -1; 62static int g_srv_juice_suit = -1;
62static int g_srv_juice_health = -1; 63static int g_srv_juice_health = -1;
63static int g_srv_ammo = -1; 64static int g_srv_ammo = -1;
@@ -181,6 +182,7 @@ bool resolve_server_netvars() {
181 // CBaseGrenade fuse / blast radius (m_flDetonateTime is not networked). 182 // CBaseGrenade fuse / blast radius (m_flDetonateTime is not networked).
182 g_srv_detonate = datamap_offset("m_flDetonateTime", FIELD_TIME); 183 g_srv_detonate = datamap_offset("m_flDetonateTime", FIELD_TIME);
183 g_srv_grenade_radius = datamap_offset("m_DmgRadius", FIELD_FLOAT); 184 g_srv_grenade_radius = datamap_offset("m_DmgRadius", FIELD_FLOAT);
185 g_srv_thrower = datamap_offset("m_hThrower", FIELD_EHANDLE);
184 int juice_offs[8]; 186 int juice_offs[8];
185 int njuice = datamap_offsets("m_iJuice", FIELD_INTEGER, 0x40, 0x4000, 187 int njuice = datamap_offsets("m_iJuice", FIELD_INTEGER, 0x40, 0x4000,
186 juice_offs, 8); 188 juice_offs, 8);
@@ -510,8 +512,24 @@ float server_grenade_radius(int idx) {
510 return *(float *)((uintptr_t)base + g_srv_grenade_radius); 512 return *(float *)((uintptr_t)base + g_srv_grenade_radius);
511} 513}
512 514
513// CWallHealth / CNewRecharge share the same m_iJuice layout (the fields before 515// CBaseGrenade::m_hThrower, used to recognise the local player's own frag even
514// it match), so one datamap offset works for both health and suit chargers. 516// when the client owner handles are unresolved.
517int server_grenade_thrower(int idx) {
518 if (!g_srv_ready || g_srv_thrower < 0)
519 return -1;
520 void *base = nullptr;
521 server_entity_health(idx, nullptr, nullptr, &base);
522 if (!base)
523 return -1;
524 uint32_t h = *(uint32_t *)((uintptr_t)base + g_srv_thrower);
525 if (h == 0 || h == 0xFFFFFFFFu)
526 return -1;
527 return (int)(h & 0x7FF);
528}
529
530// Health chargers (CWallHealth, CBaseToggle) and suit chargers
531// (CNewRecharge, CBaseAnimating) have m_iJuice at different offsets; pick by
532// the server class name.
515int server_item_juice(int idx) { 533int server_item_juice(int idx) {
516 if (!g_srv_ready) 534 if (!g_srv_ready)
517 return -1; 535 return -1;