diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 23:46:58 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 23:46:58 +0100 |
| commit | a224cc451233a487a3d931954b125f6e1736e710 (patch) | |
| tree | 5cd1ed10848b6510ff92dccbe6f7b3cd5f7348f4 | |
| parent | Charger juice: resolve per-class offsets (diff) | |
| download | hl2h-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.md | 17 | ||||
| -rw-r--r-- | src/esp.cpp | 95 | ||||
| -rw-r--r-- | src/hl2h.h | 1 | ||||
| -rw-r--r-- | src/server.cpp | 22 |
4 files changed, 101 insertions, 34 deletions
| @@ -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 | ||
| 2978 | static ProjTarget g_projs[32]; | 2982 | static 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; |
| @@ -319,6 +319,7 @@ int server_player_ammo(int idx, int type); | |||
| 319 | uint32_t server_player_weapon(int idx, int slot); | 319 | uint32_t server_player_weapon(int idx, int slot); |
| 320 | float server_grenade_detonate(int idx); | 320 | float server_grenade_detonate(int idx); |
| 321 | float server_grenade_radius(int idx); | 321 | float server_grenade_radius(int idx); |
| 322 | int server_grenade_thrower(int idx); | ||
| 322 | int server_item_juice(int idx); | 323 | int server_item_juice(int idx); |
| 323 | int server_entity_count(); | 324 | int server_entity_count(); |
| 324 | void *server_entity_base(int idx); | 325 | void *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; | |||
| 58 | static int g_srv_armor = -1; | 58 | static int g_srv_armor = -1; |
| 59 | static int g_srv_detonate = -1; | 59 | static int g_srv_detonate = -1; |
| 60 | static int g_srv_grenade_radius = -1; | 60 | static int g_srv_grenade_radius = -1; |
| 61 | static int g_srv_thrower = -1; | ||
| 61 | static int g_srv_juice_suit = -1; | 62 | static int g_srv_juice_suit = -1; |
| 62 | static int g_srv_juice_health = -1; | 63 | static int g_srv_juice_health = -1; |
| 63 | static int g_srv_ammo = -1; | 64 | static 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. |
| 517 | int 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. | ||
| 515 | int server_item_juice(int idx) { | 533 | int server_item_juice(int idx) { |
| 516 | if (!g_srv_ready) | 534 | if (!g_srv_ready) |
| 517 | return -1; | 535 | return -1; |
