summaryrefslogtreecommitdiffstats
path: root/src/vischeck.cpp
blob: 6b531b985b9566c84d22b3058395cd609877ad25 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
// vischeck.cpp — line-of-sight test via EngineTraceClient003.
//
// IEngineTrace::TraceRay (vtable slot 4) is given a ray from the eye to the
// aim point with TRACE_EVERYTHING/MASK_SHOT, ignoring the local player and the
// target entity. This catches world geometry, static props and temporary
// objects such as crates/NPCs. Glass is skipped only when it is non-solid or
// its material name marks it breakable; solid non-break glass (bulletproof)
// blocks the check.

#include "hl2h.h"
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <strings.h>

namespace {

struct alignas(16) VectorAlignedCompat {
  float x, y, z, w;
};

// Ray_t: four VectorAligned (16 bytes each) followed by two bools.
struct alignas(16) RayCompat {
  VectorAlignedCompat start;
  VectorAlignedCompat delta;
  VectorAlignedCompat startOffset;
  VectorAlignedCompat extents;
  bool isRay;
  bool isSwept;
};

static_assert(sizeof(VectorAlignedCompat) == 16, "VectorAligned layout");
static_assert(offsetof(RayCompat, delta) == 16, "Ray_t delta offset");
static_assert(offsetof(RayCompat, isRay) == 64, "Ray_t isRay offset");

// trace_t / CGameTrace layout (Source 2013, 32-bit):
//   CBaseTrace: startpos 0, endpos 12, cplane_t plane 24 (20 bytes),
//               fraction 44, contents 48, dispFlags 52, allsolid 54,
//               startsolid 55
//   CGameTrace: fractionleftsolid 56, csurface_t surface 60 (8 bytes),
//               hitgroup 68, physicsbone 72, m_pEnt 76, hitbox 80
struct SurfaceCompat {
  const char *name;
  short surfaceProps;
  unsigned short flags;
};

struct GameTraceCompat {
  Vector startpos;
  Vector endpos;
  Vector normal;
  float dist;
  uint8_t type;
  uint8_t signbits;
  uint8_t pad[2];
  float fraction;
  int contents;
  uint16_t dispFlags;
  bool allsolid;
  bool startsolid;
  float fractionleftsolid;
  SurfaceCompat surface;
  int hitgroup;
  short physicsbone;
  void *ent;
  int hitbox;
  uint8_t rest[64]; // future-proof / avoid short writes
};

static_assert(offsetof(GameTraceCompat, fraction) == 44, "trace_t fraction");
static_assert(offsetof(GameTraceCompat, startsolid) == 55, "trace_t startsolid");
static_assert(offsetof(GameTraceCompat, ent) == 76, "trace_t m_pEnt");

// ITraceFilter has no virtual destructor: vtable[0] = ShouldHitEntity,
// vtable[1] = GetTraceType.
struct ITraceFilterCompat {
  virtual bool ShouldHitEntity(void *pEntity, int contentsMask) = 0;
  virtual int GetTraceType() const = 0;
};

struct EntityFilter : ITraceFilterCompat {
  void *ignore;
  explicit EntityFilter(void *ig) : ignore(ig) {}
  bool ShouldHitEntity(void *pEntity, int) override {
    return pEntity != ignore;
  }
  int GetTraceType() const override { return 0; } // TRACE_EVERYTHING
};

struct VisibilityFilter : ITraceFilterCompat {
  void *self;
  void *target;
  VisibilityFilter(void *s, void *t) : self(s), target(t) {}
  bool ShouldHitEntity(void *pEntity, int) override {
    return pEntity != self && pEntity != target;
  }
  int GetTraceType() const override { return 0; } // TRACE_EVERYTHING
};

constexpr unsigned CONTENTS_SOLID = 0x1u;
constexpr unsigned CONTENTS_WINDOW = 0x2u;
// MASK_SHOT = SOLID|MOVEABLE|MONSTER|WINDOW|DEBRIS|HITBOX
constexpr unsigned MASK_SHOT = 0x1u | 0x4000u | 0x2000000u | 0x2u |
                               0x4000000u | 0x40000000u;
// MASK_PLAYERSOLID = SOLID|MOVEABLE|PLAYERCLIP|WINDOW|MONSTER|GRATE
constexpr unsigned MASK_PLAYERSOLID =
    0x1u | 0x4000u | 0x10000u | 0x2u | 0x2000000u | 0x8u;

static void make_ray(RayCompat &ray, const Vector &start, const Vector &end) {
  float dx = end.x - start.x, dy = end.y - start.y, dz = end.z - start.z;
  ray = RayCompat{};
  ray.start = {start.x, start.y, start.z, 0.0f};
  ray.delta = {dx, dy, dz, 0.0f};
  ray.startOffset = {0.0f, 0.0f, 0.0f, 0.0f};
  ray.extents = {0.0f, 0.0f, 0.0f, 0.0f};
  ray.isRay = true;
  ray.isSwept = (dx * dx + dy * dy + dz * dz) > 0.0f;
}

// Swept AABB matching the standing player hull used by CGameMovement.
// Ray_t::Init shifts m_Start to the box center and stores the negated center
// offset in m_StartOffset. With mins=(-16,-16,0), maxs=(16,16,72), the
// extents are (16,16,36) and m_StartOffset is (0,0,-36).
static void make_standing_hull_ray(RayCompat &ray, const Vector &origin,
                                   float distance) {
  ray = RayCompat{};
  ray.start = {origin.x, origin.y, origin.z + 36.0f, 0.0f};
  ray.delta = {0.0f, 0.0f, -distance, 0.0f};
  ray.startOffset = {0.0f, 0.0f, -36.0f, 0.0f};
  ray.extents = {16.0f, 16.0f, 36.0f, 0.0f};
  ray.isRay = false;
  ray.isSwept = true;
}

} // namespace

static bool surface_contains(const SurfaceCompat &surf, const char *needle) {
  if (!surf.name || !needle)
    return false;
  size_t n = strlen(needle);
  for (const char *p = surf.name; *p; p++)
    if (strncasecmp(p, needle, n) == 0)
      return true;
  return false;
}

static bool surface_is_breakable_glass(const SurfaceCompat &surf) {
  return surface_contains(surf, "break");
}

static bool surface_is_glass(const SurfaceCompat &surf) {
  return surface_contains(surf, "glass");
}

bool is_visible(const Vector &eye, const Vector &point, void *self,
                void *target) {
  static bool warned = false;
  if (!g_iface.enginetrace) {
    if (!warned) {
      warned = true;
      logf("vischeck: EngineTraceClient003 unavailable; failing open");
    }
    return true;
  }

  float dx = point.x - eye.x;
  float dy = point.y - eye.y;
  float dz = point.z - eye.z;
  float len2 = dx * dx + dy * dy + dz * dz;
  if (len2 < 0.0001f)
    return true;
  float inv_len = 1.0f / sqrtf(len2);
  Vector dir{dx * inv_len, dy * inv_len, dz * inv_len};

  // Trace everything that bullets care about: world, static props, crates and
  // NPCs. Non-solid glass and glass whose material name marks it breakable is
  // skipped; solid non-break glass (bulletproof) blocks.
  Vector start = eye;
  for (int pass = 0; pass < 5; pass++) {
    RayCompat ray;
    make_ray(ray, start, point);
    GameTraceCompat tr{};
    VisibilityFilter filter(self, target);
    using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *,
                                GameTraceCompat *);
    vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray,
                                              MASK_SHOT, &filter, &tr);
    if (tr.startsolid || tr.allsolid)
      return false;
    if (tr.fraction >= 1.0f)
      return true;

    bool glass = (tr.contents & CONTENTS_WINDOW) != 0 ||
                 surface_is_glass(tr.surface);
    bool solid = (tr.contents & CONTENTS_SOLID) != 0;
    bool breakable_glass = glass && surface_is_breakable_glass(tr.surface);
    if (glass && (!solid || breakable_glass)) {
      // Step just past the pane and look for the next obstruction.
      start = tr.endpos;
      start.x += dir.x * 0.5f;
      start.y += dir.y * 0.5f;
      start.z += dir.z * 0.5f;
      logd("vischeck: skipped glass surface=%s contents=0x%x solid=%d break=%d",
           tr.surface.name ? tr.surface.name : "?", tr.contents, (int)solid,
           (int)breakable_glass);
      continue;
    }
    if (glass)
      logd("vischeck: blocked by glass surface=%s contents=0x%x",
           tr.surface.name ? tr.surface.name : "?", tr.contents);
    return false;
  }
  return false;
}

// Trace the crosshair ray and return the first entity hit (nullptr for world
// or no hit). `ignore` is the local player entity pointer.
void *trace_entity(const Vector &start, const Vector &end, void *ignore) {
  if (!g_iface.enginetrace)
    return nullptr;
  RayCompat ray;
  make_ray(ray, start, end);
  GameTraceCompat tr{};
  EntityFilter filter(ignore);
  using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *,
                              GameTraceCompat *);
  vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray,
                                            MASK_SHOT, &filter, &tr);
  if (tr.fraction >= 1.0f || tr.startsolid)
    return nullptr;
  return tr.ent;
}

// First solid/world hit along a segment, for grenade bounce prediction.
// Returns false on a clear path; otherwise fills the impact point and normal.
bool trace_solid_segment(const Vector &start, const Vector &end, void *ignore,
                         Vector &out_end, Vector &out_normal) {
  if (!g_iface.enginetrace)
    return false;
  RayCompat ray;
  make_ray(ray, start, end);
  GameTraceCompat tr{};
  EntityFilter filter(ignore);
  using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *,
                              GameTraceCompat *);
  vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray,
                                            MASK_PLAYERSOLID, &filter, &tr);
  if (tr.startsolid || tr.fraction >= 1.0f)
    return false;
  out_end = tr.endpos;
  out_normal = tr.normal;
  return true;
}

// True when a downward point trace from just above `point` finds a standable
// surface. Used by edge jump: a ledge has no floor within the probe, slopes and
// stairs do.
bool point_ground(const Vector &point, void *ignore) {
  if (!g_iface.enginetrace)
    return false;
  RayCompat ray;
  Vector start{point.x, point.y, point.z + 4.0f};
  Vector end{point.x, point.y, point.z - 96.0f};
  make_ray(ray, start, end);
  GameTraceCompat tr{};
  EntityFilter filter(ignore);
  using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *,
                              GameTraceCompat *);
  vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray,
                                            MASK_PLAYERSOLID, &filter, &tr);
  // A start inside geometry means a wall/step there, not a ledge.
  return tr.startsolid || tr.fraction < 1.0f;
}

// Vertical distance from the standing player hull's feet to the first
// standable surface below. Matches CategorizePosition's 2-unit probe and its
// plane.normal.z >= 0.7 rule, but sweeps much farther so the caller can
// predict the landing. Returns -1 when there is no usable ground.
float ground_distance(const Vector &origin, void *ignore, float max_dist) {
  if (!g_iface.enginetrace || max_dist <= 0.0f)
    return -1.0f;
  RayCompat ray;
  make_standing_hull_ray(ray, origin, max_dist);
  GameTraceCompat tr{};
  EntityFilter filter(ignore);
  using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *,
                              GameTraceCompat *);
  vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray,
                                            MASK_PLAYERSOLID, &filter, &tr);
  if (tr.startsolid || tr.fraction >= 1.0f || tr.normal.z < 0.7f) {
    static int solid_logs = 0;
    if (tr.startsolid && solid_logs < 4) {
      solid_logs++;
      logd("ground trace: startsolid at (%.1f %.1f %.1f)", origin.x,
           origin.y, origin.z);
    }
    return -1.0f;
  }
  return tr.fraction * max_dist;
}