/*
 * Source reconstruction for player state to entity state conversion.
 *
 * Binary SHA-256:
 * 4d2391c10e234559ace294fb69d96741cad522653c361f13f626f4dc1891acb7
 */

#include <stdint.h>
#include <stddef.h>
#include <math.h>
#include "coduo_x87emu.h" /* defines EMULATE_X87; x87 shim when it is 1 */
#include "recovered_game.h"
#include "game_types.h"
#include "g_public.h"
#include "game_globals.h"
#include "game_functions.h"
#include "coduo_libm.h"

/* ------------------------------------------------------------------ */
/*  0x1fe63  BG_EvaluateTrajectory                                    */
/* ------------------------------------------------------------------ */

#define TRAJECTORY_MSEC_TO_SEC 0.001f /* original float32 0x3a83126f */
#define TRAJECTORY_HALF_ACCEL 0.5f
#define TRAJECTORY_SINE_PI_DOUBLE 3.141592653589793 /* original double 0x400921fb54442d18 */
#define TRAJECTORY_GRAVITY 800.0f
#define TRAJECTORY_GRAVITY_LOW 240.00002f /* original float32 0x43700001 */
#define TRAJECTORY_FLOAT_GRAVITY 160.0f
#define TRAJECTORY_GRAVITY_HALF 400.0f
#define TRAJECTORY_GRAVITY_LOW_HALF 120.00001f /* original float32 0x42f00001 */
#define TRAJECTORY_FLOAT_GRAVITY_HALF 80.0f
#define MARK_DIR_NORMAL_MIN_LENGTH 1.0f
#define MARK_DIR_DEFAULT_DOT 0.3f
#define MARK_DIR_STEEP_NORMAL_Z 0.8f
#define MARK_DIR_STEEP_DOT 0.7f
#define MARK_DIR_NORMAL_BLEND 0.5f
#define PREDICTABLE_EVENT_RING_MASK 3
#define PREDICTABLE_EVENT_RING_COUNT 4u
#define PREDICTABLE_EVENT_BYTE_MASK 0xff
#define EXTRAPOLATE_PLAYER_STATE_DURATION 50
#define PRONE_CONTENTS_DEFAULT 0x00810031
#define PRONE_CONTENTS_ALT 0x00820011
#define PRONE_BLOCKING_CONTENTS 0x20
#define PRONE_RADIUS 6.0f
#define PRONE_FORWARD_CLEARANCE 38.0f
#define PRONE_FORWARD_PROBE 24.0f
#define PRONE_GROUND_NORMAL_MIN_Z 0.7f
#define PRONE_MAX_GROUND_DELTA 24.0f
#define PS_FLAG_FIRE_POSITION_CHECK_MASK 0x00106000u
#define PS_FLAG_LINKED_ENTITY_TYPE_MASK 0x000c0000u
#define ENTITY_STATE_DEAD_FLAG 0x00000001u
#define PLAYER_STATE_ADS_FLAG 0x00000020u
#define ENTITY_STATE_ADS_FLAG 0x00000100u

static const vec4_t BG_COLOR_RED = {1.0f, 0.0f, 0.0f, 1.0f};
static const vec4_t BG_COLOR_GREEN = {0.0f, 1.0f, 0.0f, 1.0f};
static const vec4_t BG_COLOR_YELLOW = {1.0f, 1.0f, 0.0f, 1.0f};
static const vec4_t BG_COLOR_CYAN = {0.0f, 1.0f, 1.0f, 1.0f};
static const vec4_t BG_COLOR_MD_CYAN = {0.0f, 0.5f, 1.0f, 1.0f};
static const vec4_t BG_COLOR_MAGENTA = {1.0f, 0.0f, 1.0f, 1.0f};

const vec3_t bgVehicleArtilleryPositionOffset = {0.0f, -24.0f, 0.0f};
const vec3_t bgVehicleTankPosition2Offset = {10.0f, 5.0f, -39.0f};
const vec3_t bgVehicleTankPosition1Offset = {0.0f, 0.0f, -20.0f};

/* NOT_FROM_ORIGINAL_SOURCE: defined binary32 snapshot conversion matching the
 * x87 fistp-toward-zero result without adding an emulation path. */
static float game_compat_bg_snap_float(float value)
{
    return (float)game_compat_int32_from_float_trunc(value);
}

/* NOT_FROM_ORIGINAL_SOURCE: local factoring for trajectory delta-time math. */
static float game_compat_bg_trajectory_delta_time(const trajectory_t *trajectory, int atTime)
{
    int32_t deltaMsec = coduo_int32_from_bits(
        (uint32_t)atTime - (uint32_t)trajectory->trTime);

    return (float)deltaMsec * TRAJECTORY_MSEC_TO_SEC;
}

/* NOT_FROM_ORIGINAL_SOURCE: local factoring for trajectory stop-time clamp. */
static int game_compat_bg_trajectory_clamped_time(const trajectory_t *trajectory, int atTime)
{
    int32_t endTime = coduo_int32_from_bits(
        (uint32_t)trajectory->trTime + (uint32_t)trajectory->trDuration);

    if (endTime < atTime) {
        return endTime;
    }
    return atTime;
}

/* NOT_FROM_ORIGINAL_SOURCE: local factoring for trajectory base copies. */
static void game_compat_bg_copy_trajectory_base(const trajectory_t *trajectory, float *out)
{
    out[0] = trajectory->trBase[0];
    out[1] = trajectory->trBase[1];
    out[2] = trajectory->trBase[2];
}

/* NOT_FROM_ORIGINAL_SOURCE: local factoring for trajectory delta copies. */
static void game_compat_bg_copy_trajectory_delta(const trajectory_t *trajectory, float *out)
{
    out[0] = trajectory->trDelta[0];
    out[1] = trajectory->trDelta[1];
    out[2] = trajectory->trDelta[2];
}

/* NOT_FROM_ORIGINAL_SOURCE: local factoring for zero trajectory deltas. */
static void game_compat_bg_clear_trajectory_delta(float *out)
{
    out[2] = 0.0f;
    out[1] = 0.0f;
    out[0] = 0.0f;
}

/* NOT_FROM_ORIGINAL_SOURCE: local factoring for linear trajectory evaluation. */
static void game_compat_bg_evaluate_linear_trajectory(const trajectory_t *trajectory,
                                        float deltaTime, float *out)
{
#if EMULATE_X87
    /* base + delta*deltaTime per lane, the product/add in 80-bit rounded to
     * float at the store. */
    for (int i = 0; i < 3; ++i) {
        out[i] = x87f_store_f32(x87f_add(
            x87f_mul(x87f_load_f32(trajectory->trDelta[i]),
                     x87f_load_f32(deltaTime)),
            x87f_load_f32(trajectory->trBase[i])));
    }
#else
    out[0] = trajectory->trBase[0] + trajectory->trDelta[0] * deltaTime;
    out[1] = trajectory->trBase[1] + trajectory->trDelta[1] * deltaTime;
    out[2] = trajectory->trBase[2] + trajectory->trDelta[2] * deltaTime;
#endif
}

/* VERIFIED_DECOMPILER(0x1fe63, 2fe63_BG_EvaluateTrajectory.c, VERIFY-WAVE3-BGSTATE-TRAJECTORY-2026-06-17): DATAFLOW_VERIFIED - trajectory switch cases, clamped linear stop time, sine/gravity/accel/decel calculations, VectorNormalize2 output reuse, and Com_Error argument checked against current decompiler output. */
void BG_EvaluateTrajectory(const trajectory_t *trajectory, int atTime, float *out)
{
    float deltaTime;

    switch (trajectory->trType) {
    case TR_STATIONARY:
    case TR_INTERPOLATE:
    case TR_LINKED:
        game_compat_bg_copy_trajectory_base(trajectory, out);
        break;

    case TR_LINEAR:
        game_compat_bg_evaluate_linear_trajectory(
            trajectory, game_compat_bg_trajectory_delta_time(trajectory, atTime), out);
        break;

    case TR_LINEAR_STOP:
        deltaTime = game_compat_bg_trajectory_delta_time(
            trajectory, game_compat_bg_trajectory_clamped_time(trajectory, atTime));
        if (deltaTime < 0.0f) {
            deltaTime = 0.0f;
        }
        game_compat_bg_evaluate_linear_trajectory(trajectory, deltaTime, out);
        break;

    case TR_SINE:
#if EMULATE_X87
        deltaTime = x87f_store_f32(
            x87f_div(x87f_load_i32(coduo_int32_from_bits(
                         (uint32_t)atTime - (uint32_t)trajectory->trTime)),
                     x87f_load_i32(trajectory->trDuration)));
        game_compat_bg_evaluate_linear_trajectory(
            trajectory,
            (float)CoduoLibm_Sin(x87f_store_f64(x87f_mul(
                x87f_mul(x87f_load_f32(deltaTime),
                         x87f_load_f64(TRAJECTORY_SINE_PI_DOUBLE)),
                x87f_load_f64(2.0)))),
            out);
#else
        deltaTime = ((float)coduo_int32_from_bits(
                         (uint32_t)atTime - (uint32_t)trajectory->trTime) /
                     (float)trajectory->trDuration);
        game_compat_bg_evaluate_linear_trajectory(
            trajectory,
            (float)CoduoLibm_Sin((double)deltaTime * TRAJECTORY_SINE_PI_DOUBLE * 2.0),
            out);
#endif
        break;

    case TR_GRAVITY:
        deltaTime = game_compat_bg_trajectory_delta_time(trajectory, atTime);
        game_compat_bg_evaluate_linear_trajectory(trajectory, deltaTime, out);
#if EMULATE_X87
        out[2] = x87f_store_f32(x87f_sub(
            x87f_load_f32(out[2]),
            x87f_mul(x87f_mul(x87f_load_f32(deltaTime),
                              x87f_load_f32(TRAJECTORY_GRAVITY_HALF)),
                     x87f_load_f32(deltaTime))));
#else
        out[2] -= deltaTime * TRAJECTORY_GRAVITY_HALF * deltaTime;
#endif
        break;

    case TR_GRAVITY_LOW:
        deltaTime = game_compat_bg_trajectory_delta_time(trajectory, atTime);
        game_compat_bg_evaluate_linear_trajectory(trajectory, deltaTime, out);
#if EMULATE_X87
        out[2] = x87f_store_f32(x87f_sub(
            x87f_load_f32(out[2]),
            x87f_mul(x87f_mul(x87f_load_f32(deltaTime),
                              x87f_load_f32(TRAJECTORY_GRAVITY_LOW_HALF)),
                     x87f_load_f32(deltaTime))));
#else
        out[2] -= deltaTime * TRAJECTORY_GRAVITY_LOW_HALF * deltaTime;
#endif
        break;

    case TR_GRAVITY_FLOAT:
        deltaTime = game_compat_bg_trajectory_delta_time(trajectory, atTime);
        game_compat_bg_evaluate_linear_trajectory(trajectory, deltaTime, out);
#if EMULATE_X87
        out[2] = x87f_store_f32(x87f_sub(
            x87f_load_f32(out[2]),
            x87f_mul(x87f_load_f32(deltaTime),
                     x87f_load_f32(TRAJECTORY_FLOAT_GRAVITY_HALF))));
#else
        out[2] -= deltaTime * TRAJECTORY_FLOAT_GRAVITY_HALF;
#endif
        break;

    case TR_ACCELERATE: {
        float speed;

        deltaTime = game_compat_bg_trajectory_delta_time(
            trajectory, game_compat_bg_trajectory_clamped_time(trajectory, atTime));
#if EMULATE_X87
        speed = x87f_store_f32(x87f_div(
            x87f_load_f32((float)CoduoLibm_Sqrt(x87f_store_f64(x87f_add(
                x87f_add(x87f_mul(x87f_load_f32(trajectory->trDelta[0]),
                                  x87f_load_f32(trajectory->trDelta[0])),
                         x87f_mul(x87f_load_f32(trajectory->trDelta[1]),
                                  x87f_load_f32(trajectory->trDelta[1]))),
                x87f_mul(x87f_load_f32(trajectory->trDelta[2]),
                         x87f_load_f32(trajectory->trDelta[2])))))),
            x87f_mul(x87f_load_i32(trajectory->trDuration),
                     x87f_load_f32(TRAJECTORY_MSEC_TO_SEC))));
        (void)VectorNormalize2(trajectory->trDelta, out);
        for (int i = 0; i < 3; ++i) {
            out[i] = x87f_store_f32(x87f_add(
                x87f_mul(
                    x87f_mul(x87f_mul(x87f_mul(x87f_load_f32(speed),
                                               x87f_load_f32(
                                                   TRAJECTORY_HALF_ACCEL)),
                                      x87f_load_f32(deltaTime)),
                             x87f_load_f32(deltaTime)),
                    x87f_load_f32(out[i])),
                x87f_load_f32(trajectory->trBase[i])));
        }
#else
        speed = (float)CoduoLibm_Sqrt((double)(trajectory->trDelta[0] *
                                         trajectory->trDelta[0] +
                                     trajectory->trDelta[1] *
                                         trajectory->trDelta[1] +
                                     trajectory->trDelta[2] *
                                         trajectory->trDelta[2])) /
                ((float)trajectory->trDuration * TRAJECTORY_MSEC_TO_SEC);
        (void)VectorNormalize2(trajectory->trDelta, out);
        out[0] = trajectory->trBase[0] +
                 speed * TRAJECTORY_HALF_ACCEL * deltaTime * deltaTime *
                     out[0];
        out[1] = trajectory->trBase[1] +
                 speed * TRAJECTORY_HALF_ACCEL * deltaTime * deltaTime *
                     out[1];
        out[2] = trajectory->trBase[2] +
                 speed * TRAJECTORY_HALF_ACCEL * deltaTime * deltaTime *
                     out[2];
#endif
        break;
    }

    case TR_DECCELERATE: {
        vec3_t linear;
        float speed;

        deltaTime = game_compat_bg_trajectory_delta_time(
            trajectory, game_compat_bg_trajectory_clamped_time(trajectory, atTime));
#if EMULATE_X87
        speed = x87f_store_f32(x87f_div(
            x87f_load_f32((float)CoduoLibm_Sqrt(x87f_store_f64(x87f_add(
                x87f_add(x87f_mul(x87f_load_f32(trajectory->trDelta[0]),
                                  x87f_load_f32(trajectory->trDelta[0])),
                         x87f_mul(x87f_load_f32(trajectory->trDelta[1]),
                                  x87f_load_f32(trajectory->trDelta[1]))),
                x87f_mul(x87f_load_f32(trajectory->trDelta[2]),
                         x87f_load_f32(trajectory->trDelta[2])))))),
            x87f_mul(x87f_load_i32(trajectory->trDuration),
                     x87f_load_f32(TRAJECTORY_MSEC_TO_SEC))));
        (void)VectorNormalize2(trajectory->trDelta, out);
        for (int i = 0; i < 3; ++i) {
            linear[i] = x87f_store_f32(x87f_add(
                x87f_load_f32(trajectory->trBase[i]),
                x87f_mul(x87f_load_f32(trajectory->trDelta[i]),
                         x87f_load_f32(deltaTime))));
        }
        for (int i = 0; i < 3; ++i) {
            out[i] = x87f_store_f32(x87f_add(
                x87f_mul(
                    x87f_mul(x87f_mul(x87f_mul(
                                               x87f_neg(x87f_load_f32(speed)),
                                               x87f_load_f32(
                                                   TRAJECTORY_HALF_ACCEL)),
                                      x87f_load_f32(deltaTime)),
                             x87f_load_f32(deltaTime)),
                    x87f_load_f32(out[i])),
                x87f_load_f32(linear[i])));
        }
#else
        speed = (float)CoduoLibm_Sqrt((double)(trajectory->trDelta[0] *
                                         trajectory->trDelta[0] +
                                     trajectory->trDelta[1] *
                                         trajectory->trDelta[1] +
                                     trajectory->trDelta[2] *
                                         trajectory->trDelta[2])) /
                ((float)trajectory->trDuration * TRAJECTORY_MSEC_TO_SEC);
        (void)VectorNormalize2(trajectory->trDelta, out);
        linear[0] = trajectory->trBase[0] + trajectory->trDelta[0] * deltaTime;
        linear[1] = trajectory->trBase[1] + trajectory->trDelta[1] * deltaTime;
        linear[2] = trajectory->trBase[2] + trajectory->trDelta[2] * deltaTime;
        out[0] = -speed * TRAJECTORY_HALF_ACCEL * deltaTime * deltaTime *
                     out[0] +
                 linear[0];
        out[1] = -speed * TRAJECTORY_HALF_ACCEL * deltaTime * deltaTime *
                     out[1] +
                 linear[1];
        out[2] = -speed * TRAJECTORY_HALF_ACCEL * deltaTime * deltaTime *
                     out[2] +
                 linear[2];
#endif
        break;
    }

    default:
        /* ORIGINAL_BINARY_BUG: 0x2046e..0x20489 passes trTime even though the
         * diagnostic names trType; preserve the mismatched field.  See
         * original-bugs/game-bg-trajectory-invalid-type-reports-time.md. */
        Com_Error(1, COM_ERROR_MARKER
                     "BG_EvaluateTrajectory: unknown trType: %i",
                  trajectory->trTime);
        break;
    }
}

/* ------------------------------------------------------------------ */
/*  0x20494  BG_EvaluateTrajectoryDelta                               */
/* ------------------------------------------------------------------ */

/* VERIFIED_DECOMPILER(0x20494, 30494_BG_EvaluateTrajectoryDelta.c, VERIFY-WAVE3-BGSTATE-TRAJECTORY-2026-06-17): DATAFLOW_VERIFIED - delta switch cases, linear-stop terminal zeroing, sine cosine scale, gravity delta adjustments, accel/decel terminal branches, and Com_Error argument checked against current decompiler output. */
void BG_EvaluateTrajectoryDelta(const trajectory_t *trajectory, int atTime,
                                float *out)
{
    float deltaTime;

    switch (trajectory->trType) {
    case TR_STATIONARY:
    case TR_INTERPOLATE:
        game_compat_bg_clear_trajectory_delta(out);
        break;

    case TR_LINEAR:
        game_compat_bg_copy_trajectory_delta(trajectory, out);
        break;

    case TR_LINEAR_STOP:
        if (coduo_int32_from_bits(
                (uint32_t)trajectory->trTime +
                (uint32_t)trajectory->trDuration) < atTime) {
            game_compat_bg_clear_trajectory_delta(out);
        } else {
            game_compat_bg_copy_trajectory_delta(trajectory, out);
        }
        break;

    case TR_SINE:
#if EMULATE_X87
        deltaTime = x87f_store_f32(
            x87f_div(x87f_load_i32(coduo_int32_from_bits(
                         (uint32_t)atTime - (uint32_t)trajectory->trTime)),
                     x87f_load_i32(trajectory->trDuration)));
        deltaTime = (float)CoduoLibm_Cos(x87f_store_f64(x87f_mul(
                        x87f_mul(x87f_load_f32(deltaTime),
                                 x87f_load_f64(TRAJECTORY_SINE_PI_DOUBLE)),
                        x87f_load_f64(2.0)))) *
                    TRAJECTORY_HALF_ACCEL;
#else
        deltaTime = ((float)coduo_int32_from_bits(
                         (uint32_t)atTime - (uint32_t)trajectory->trTime) /
                     (float)trajectory->trDuration);
        deltaTime = (float)CoduoLibm_Cos((double)deltaTime *
                                   TRAJECTORY_SINE_PI_DOUBLE * 2.0) *
                    TRAJECTORY_HALF_ACCEL;
#endif
        out[0] = trajectory->trDelta[0] * deltaTime;
        out[1] = trajectory->trDelta[1] * deltaTime;
        out[2] = trajectory->trDelta[2] * deltaTime;
        break;

    case TR_GRAVITY:
        game_compat_bg_copy_trajectory_delta(trajectory, out);
#if EMULATE_X87
        out[2] = x87f_store_f32(x87f_sub(
            x87f_load_f32(out[2]),
            x87f_mul(x87f_load_f32(game_compat_bg_trajectory_delta_time(trajectory, atTime)),
                     x87f_load_f32(TRAJECTORY_GRAVITY))));
#else
        out[2] -= game_compat_bg_trajectory_delta_time(trajectory, atTime) *
                  TRAJECTORY_GRAVITY;
#endif
        break;

    case TR_GRAVITY_LOW:
        game_compat_bg_copy_trajectory_delta(trajectory, out);
#if EMULATE_X87
        out[2] = x87f_store_f32(x87f_sub(
            x87f_load_f32(out[2]),
            x87f_mul(x87f_load_f32(game_compat_bg_trajectory_delta_time(trajectory, atTime)),
                     x87f_load_f32(TRAJECTORY_GRAVITY_LOW))));
#else
        out[2] -= game_compat_bg_trajectory_delta_time(trajectory, atTime) *
                  TRAJECTORY_GRAVITY_LOW;
#endif
        break;

    case TR_GRAVITY_FLOAT:
        game_compat_bg_copy_trajectory_delta(trajectory, out);
#if EMULATE_X87
        out[2] = x87f_store_f32(x87f_sub(
            x87f_load_f32(out[2]),
            x87f_mul(x87f_load_f32(game_compat_bg_trajectory_delta_time(trajectory, atTime)),
                     x87f_load_f32(TRAJECTORY_FLOAT_GRAVITY))));
#else
        out[2] -= game_compat_bg_trajectory_delta_time(trajectory, atTime) *
                  TRAJECTORY_FLOAT_GRAVITY;
#endif
        break;

    case TR_ACCELERATE:
        if (coduo_int32_from_bits(
                (uint32_t)trajectory->trTime +
                (uint32_t)trajectory->trDuration) < atTime) {
            game_compat_bg_clear_trajectory_delta(out);
        } else {
            deltaTime = game_compat_bg_trajectory_delta_time(trajectory, atTime);
#if EMULATE_X87
            for (int i = 0; i < 3; ++i) {
                out[i] = x87f_store_f32(
                    x87f_mul(x87f_mul(x87f_load_f32(trajectory->trDelta[i]),
                                      x87f_load_f32(deltaTime)),
                             x87f_load_f32(deltaTime)));
            }
#else
            out[0] = trajectory->trDelta[0] * deltaTime * deltaTime;
            out[1] = trajectory->trDelta[1] * deltaTime * deltaTime;
            out[2] = trajectory->trDelta[2] * deltaTime * deltaTime;
#endif
        }
        break;

    case TR_DECCELERATE:
        if (coduo_int32_from_bits(
                (uint32_t)trajectory->trTime +
                (uint32_t)trajectory->trDuration) < atTime) {
            game_compat_bg_clear_trajectory_delta(out);
        } else {
            deltaTime = game_compat_bg_trajectory_delta_time(trajectory, atTime);
            out[0] = trajectory->trDelta[0] * deltaTime;
            out[1] = trajectory->trDelta[1] * deltaTime;
            out[2] = trajectory->trDelta[2] * deltaTime;
        }
        break;

    default:
        /* ORIGINAL_BINARY_BUG: 0x20842..0x2085d also passes trTime even though
         * the diagnostic names trType; preserve that field.  See
         * original-bugs/game-bg-trajectory-invalid-type-reports-time.md. */
        Com_Error(1, COM_ERROR_MARKER
                     "BG_EvaluateTrajectoryDelta: unknown trType: %i",
                  trajectory->trTime);
        break;
    }
}

/* ------------------------------------------------------------------ */
/*  0x20869  BG_GetMarkDir                                            */
/* ------------------------------------------------------------------ */

/* VERIFIED_DECOMPILER(0x20869, 30869_BG_GetMarkDir.c, VERIFY-WAVE3-BGSTATE-TRAJECTORY-2026-06-17): DATAFLOW_VERIFIED - normal length fallback, inverse direction normalize, steep-normal dot threshold, blend loop, and output copy checked against current decompiler output. */
void BG_GetMarkDir(const float *dir, const float *normal, float *out)
{
    float minDot = MARK_DIR_DEFAULT_DOT;
    vec3_t safeNormal;
    vec3_t markDir;
    float normalLength;

    normalLength = (float)CoduoLibm_Sqrt((double)(
        (long double)normal[0] * (long double)normal[0] +
        (long double)normal[1] * (long double)normal[1] +
        (long double)normal[2] * (long double)normal[2]));
    if (normalLength < MARK_DIR_NORMAL_MIN_LENGTH) {
        safeNormal[0] = 0.0f;
        safeNormal[1] = 0.0f;
        safeNormal[2] = 1.0f;
    } else {
        safeNormal[0] = normal[0];
        safeNormal[1] = normal[1];
        safeNormal[2] = normal[2];
    }

    markDir[0] = -dir[0];
    markDir[1] = -dir[1];
    markDir[2] = -dir[2];
    VectorNormalize(markDir);

    if (normal[2] > MARK_DIR_STEEP_NORMAL_Z) {
        minDot = MARK_DIR_STEEP_DOT;
    }

    while ((long double)markDir[0] * (long double)safeNormal[0] +
               (long double)markDir[1] * (long double)safeNormal[1] +
               (long double)markDir[2] * (long double)safeNormal[2] <
           (long double)minDot) {
        markDir[0] = (float)((long double)markDir[0] +
                             (long double)safeNormal[0] *
                                 MARK_DIR_NORMAL_BLEND);
        markDir[1] = (float)((long double)markDir[1] +
                             (long double)safeNormal[1] *
                                 MARK_DIR_NORMAL_BLEND);
        markDir[2] = (float)((long double)markDir[2] +
                             (long double)safeNormal[2] *
                                 MARK_DIR_NORMAL_BLEND);
        VectorNormalize(markDir);
    }

    out[0] = markDir[0];
    out[1] = markDir[1];
    out[2] = markDir[2];
}

/* ------------------------------------------------------------------ */
/*  0x209eb  BG_AddPredictableEventToPlayerstate                      */
/* ------------------------------------------------------------------ */

/* VERIFIED_DECOMPILER(0x209eb, 309eb_BG_AddPredictableEventToPlayerstate.c, VERIFY-WAVE3-BGSTATE-REPLICATION-2026-06-17): DATAFLOW_VERIFIED - nonzero event gate, 4-slot ring index, byte event/parm stores, and eventIndex increment checked against current decompiler output. */
void BG_AddPredictableEventToPlayerstate(int event, int eventParm,
                                         playerState_t *ps)
{
    if (event == 0) {
        return;
    }

    ps->events[(uint32_t)ps->eventIndex & PREDICTABLE_EVENT_RING_MASK] =
        event & PREDICTABLE_EVENT_BYTE_MASK;
    ps->eventParms.ring[(uint32_t)ps->eventIndex &
                        PREDICTABLE_EVENT_RING_MASK] =
        eventParm & PREDICTABLE_EVENT_BYTE_MASK;
    ps->eventIndex =
        coduo_int32_from_bits((uint32_t)ps->eventIndex + 1u);
}

/* ------------------------------------------------------------------ */
/*  0x20a35  BG_PlayerStateToEntityState                              */
/* ------------------------------------------------------------------ */

/*
 * Convert player state to entity state for network transmission.
 *
 * This function copies relevant fields from the player state (gclient_t)
 * to the entity state (gentity_t) for network synchronization:
 *  1. Sets entity type based on player state flags
 *  2. Copies position trajectory from client origin
 *  3. Copies angle trajectory from client view angles
 *  4. Copies packed lean/yaw payload to client-info lean amount
 *  5. Handles stance-based view height lerp for prone
 *  6. Copies lean angles with lerp
 *
 * RECOVERED(UO-GAME-UNK-0209): This function has many field mappings
 * between player state and entity state. The reconstruction uses proper
 * struct field access instead of pointer arithmetic.
 */
/* VERIFIED_DECOMPILER(0x20a35, 30a35_BG_PlayerStateToEntityState.c, VERIFY-WAVE3-BGSTATE-REPLICATION-2026-06-17): DATAFLOW_VERIFIED - entity type, trajectory export/snap, anim/client fields, vehicle animation-state packing, stance flags, lean lerp, event rings, weapon byte, and ground entity stores checked against current decompiler output. */
void BG_PlayerStateToEntityState(gclient_t *client, gentity_t *ent, int snap)
{
    effectiveStance_t effectiveStance;
    float viewHeightLerp;
    int viewHeightLerpTime;
    int32_t viewHeightLerpElapsed;
    float normalizedAngle;

    /* Set entity type based on player state flags */
    if ((client->ps.playerStateFlags & PS_FLAG_LINKED_ENTITY_TYPE_MASK) == 0) {
        ent->s_eType = ET_INVISIBLE;
    } else {
        ent->s_eType = ET_PLAYER;
    }

    /* Set position trajectory type to TR_INTERPOLATE */
    ent->pos.trType = TR_INTERPOLATE;

    /* Copy origin from client to entity position trajectory */
    ent->pos.trBase[0] = client->ps.psOrigin[0];
    ent->pos.trBase[1] = client->ps.psOrigin[1];
    ent->pos.trBase[2] = client->ps.psOrigin[2];

    /* Binary uses x87 fistp with RC=truncate for snapshot snapping. */
    if (snap != 0) {
        ent->pos.trBase[0] = game_compat_bg_snap_float(ent->pos.trBase[0]);
        ent->pos.trBase[1] = game_compat_bg_snap_float(ent->pos.trBase[1]);
        ent->pos.trBase[2] = game_compat_bg_snap_float(ent->pos.trBase[2]);
    }

    /* Set angle trajectory type to TR_INTERPOLATE */
    ent->apos.trType = TR_INTERPOLATE;

    /* Copy angles from client to entity angle trajectory */
    ent->apos.trBase[0] = client->ps.viewAngles[0];
    ent->apos.trBase[1] = client->ps.viewAngles[1];
    ent->apos.trBase[2] = client->ps.viewAngles[2];

    /* Binary uses x87 fistp with RC=truncate for snapshot snapping. */
    if (snap != 0) {
        ent->apos.trBase[0] = game_compat_bg_snap_float(ent->apos.trBase[0]);
        ent->apos.trBase[1] = game_compat_bg_snap_float(ent->apos.trBase[1]);
        ent->apos.trBase[2] = game_compat_bg_snap_float(ent->apos.trBase[2]);
    }

    /* Decode the signed byte-like lean/yaw payload into BGS leanAmount. */
    if (client->ps.movementDir < 129) {
        ent->entityStateAngles2.clientInfo.leanAmount =
            (float)client->ps.movementDir;
    } else {
        ent->entityStateAngles2.clientInfo.leanAmount =
            (float)(client->ps.movementDir - 256);
    }

    /* Copy entity state fields */
    ent->entityStateAnim.playerAnim.legsAnim = client->ps.legsAnim;
    ent->entityStateAnim.playerAnim.torsoAnim = client->ps.torsoAnim;
    ent->clientNum = client->ps.psClientNum;
    ent->s_flags = client->ps.entityStateFlags;

    /* Handle vehicle position */
    if ((client->ps.entityStateFlags & PS_FLAG_FIRE_POSITION_CHECK_MASK) != 0) {
        ent->vehicleEntityNum = client->ps.viewLockedEntityNum;
    }

    /*
     * In this player-entity export path, eventParm carries vehicle animation
     * state: vehicle position, vehicle type, and vehicle animation pitch.
     * The decompiler clears the destination bit windows, then ORs unmasked
     * source fields. Stock i386 uses 32-bit AND/SHL/OR instructions, so the
     * casts make those modulo-32-bit operations explicit and avoid signed
     * left-shift overflow on native compilers.
     */
    ent->eventParm = coduo_int32_from_bits(
        ((uint32_t)ent->eventParm & ~VEHICLE_ANIM_STATE_POS_MASK) |
        ((uint32_t)client->ps.vehiclePosition <<
         VEHICLE_ANIM_STATE_POS_SHIFT));
    ent->eventParm = coduo_int32_from_bits(
        ((uint32_t)ent->eventParm & ~VEHICLE_ANIM_STATE_TYPE_MASK) |
        ((uint32_t)client->ps.vehicleType <<
         VEHICLE_ANIM_STATE_TYPE_SHIFT));
    ent->eventParm = coduo_int32_from_bits(
        ((uint32_t)ent->eventParm & ~VEHICLE_ANIM_STATE_MOTION_MASK) |
        ((uint32_t)client->ps.vehicleMotion <<
         VEHICLE_ANIM_STATE_MOTION_SHIFT));

    /* Set entity flag based on pmType */
    if (client->ps.pmType < PM_TYPE_DEAD) {
        ent->s_flags &= ~ENTITY_STATE_DEAD_FLAG;
    } else {
        ent->s_flags |= ENTITY_STATE_DEAD_FLAG;
    }

    /* Export the player ADS bit into its entity-state flag lane. */
    if ((client->ps.playerStateFlags & PLAYER_STATE_ADS_FLAG) == 0) {
        ent->s_flags &= ~ENTITY_STATE_ADS_FLAG;
    } else {
        ent->s_flags |= ENTITY_STATE_ADS_FLAG;
    }

    /* Copy lean-fraction payload from client to entity state. */
    ent->clientInfoLeanFraction = client->ps.leanFraction;

    /* Handle stance-based view height lerp */
    effectiveStance = PM_GetEffectiveStance(&client->ps);
    if (effectiveStance == EFFECTIVE_STANCE_PRONE) {
        /* Prone stance - calculate view height lerp */
        if (client->ps.viewHeightLerpTime == 0) {
            viewHeightLerp = 1.0f;
        } else {
            viewHeightLerpTime = PM_GetViewHeightLerpTime(&client->ps,
                                                          client->ps.viewHeightLerpTarget,
                                                          client->ps.viewHeightLerpDown);
            viewHeightLerpElapsed = coduo_int32_from_bits(
                (uint32_t)client->ps.commandTime -
                (uint32_t)client->ps.viewHeightLerpTime);
            viewHeightLerp = (float)viewHeightLerpElapsed /
                             (float)viewHeightLerpTime;
            if (viewHeightLerp < 0.0f) {
                viewHeightLerp = 0.0f;
            } else if (viewHeightLerp > 1.0f) {
                viewHeightLerp = 1.0f;
            }
            if (client->ps.viewHeightLerpDown == 0) {
                viewHeightLerp = 1.0f - viewHeightLerp;
            }
        }

        /* Copy lean angles with lerp */
        ent->entityAngles[0] = (float)client->ps.torsoHeight * viewHeightLerp;
        
        normalizedAngle = AngleNormalize180((float)client->ps.torsoPitch);
        ent->entityAngles[1] = normalizedAngle * viewHeightLerp;
        
        normalizedAngle = AngleNormalize180((float)client->ps.waistPitch);
        ent->entityAngles[2] = normalizedAngle * viewHeightLerp;
    } else {
        /* Not prone - clear lean angles */
        ent->entityAngles[0] = 0.0f;
        ent->entityAngles[1] = 0.0f;
        ent->entityAngles[2] = 0.0f;
    }

    /* Copy event buffer from client to entity */
    /* First, handle the "last event parameter" field at entity+0xa4 */
    if (coduo_int32_from_bits((uint32_t)client->ps.oldEventIndex -
                                    (uint32_t)client->ps.eventIndex) < 0) {
        /* Clamp oldEventIndex to prevent reading too far back */
        if (coduo_int32_from_bits((uint32_t)client->ps.eventIndex -
                                        (uint32_t)client->ps.oldEventIndex) >
            (int32_t)PREDICTABLE_EVENT_RING_COUNT) {
            client->ps.oldEventIndex = coduo_int32_from_bits(
                (uint32_t)client->ps.eventIndex -
                PREDICTABLE_EVENT_RING_COUNT);
        }
        /* Copy the oldest pending event parameter. Stock zero-extends the ring byte. */
        ent->tempEffectId =
            client->ps.eventParms.ring[
                client->ps.oldEventIndex & PREDICTABLE_EVENT_RING_MASK] &
            PREDICTABLE_EVENT_BYTE_MASK;
        client->ps.oldEventIndex = coduo_int32_from_bits(
            (uint32_t)client->ps.oldEventIndex + 1u);
    } else {
        ent->tempEffectId = 0;
    }

    /* Process all new events from lastEventIndex to eventIndex */
    int32_t lastEventIndex = client->ps.lastEventIndex;
    for (int32_t i = lastEventIndex; i != client->ps.eventIndex;
         i = coduo_int32_from_bits((uint32_t)i + 1u)) {
        int event = (uint8_t)client->ps.events[
            i & PREDICTABLE_EVENT_RING_MASK];
        G_PlayerEvent(ent->s_number, event);

        /* Check if this event is single-client only */
        int j;
        for (j = 0; pEventSingleClientList[j] > 0 &&
                    pEventSingleClientList[j] != event; j++) {
            /* Search for event in single-client list */
        }

        /* If not in single-client list (terminated by negative value), copy to entity */
        if (pEventSingleClientList[j] < 0) {
            ent->events[ent->eventCount & PREDICTABLE_EVENT_RING_MASK] =
                event;
            ent->eventParms[
                ent->eventCount & PREDICTABLE_EVENT_RING_MASK] =
                client->ps.eventParms.ring[
                    i & PREDICTABLE_EVENT_RING_MASK] &
                PREDICTABLE_EVENT_BYTE_MASK;
            ent->eventCount =
                coduo_int32_from_bits((uint32_t)ent->eventCount + 1u);
        }
    }

    /* Update client's last processed event index */
    client->ps.lastEventIndex = client->ps.eventIndex;

    /* Copy byte and ushort fields from client to entity */
    ent->entityStateWeapon = (uint8_t)client->ps.currentWeapon;
    ent->groundEntityNum = (uint16_t)client->ps.groundEntityNum;
}

/* ------------------------------------------------------------------ */
/*  0x21028  BG_PlayerStateToEntityStateExtrapolate                   */
/* ------------------------------------------------------------------ */

/* VERIFIED_DECOMPILER(0x21028, 31028_BG_PlayerStateToEntityStateExtrapolate.c, VERIFY-P1-BGSTATE-2026-06-17): DATAFLOW_VERIFIED - trajectory export, event rewind/flush behavior, snap truncation, stance flags, packed vehicle animation-state fields, and lean lerp checked against current decompiler output. */
void BG_PlayerStateToEntityStateExtrapolate(gclient_t *client, gentity_t *ent,
                                            int time, int snap)
{
    int32_t lastEventIndex;
    effectiveStance_t effectiveStance;

    ent->pos.trType = TR_LINEAR_STOP;
    ent->pos.trBase[0] = client->ps.psOrigin[0];
    ent->pos.trBase[1] = client->ps.psOrigin[1];
    ent->pos.trBase[2] = client->ps.psOrigin[2];
    ent->pos.trDelta[0] = client->ps.velocity[0];
    ent->pos.trDelta[1] = client->ps.velocity[1];
    ent->pos.trDelta[2] = client->ps.velocity[2];
    ent->pos.trTime = time;
    ent->pos.trDuration = EXTRAPOLATE_PLAYER_STATE_DURATION;

    ent->apos.trType = TR_INTERPOLATE;
    ent->apos.trBase[0] = client->ps.viewAngles[0];
    ent->apos.trBase[1] = client->ps.viewAngles[1];
    ent->apos.trBase[2] = client->ps.viewAngles[2];

    {
        ent->entityStateAngles2.clientInfo.leanAmount =
            (float)client->ps.movementDir;
    }
    ent->s_flags = client->ps.entityStateFlags;

    if (coduo_int32_from_bits((uint32_t)client->ps.oldEventIndex -
                                    (uint32_t)client->ps.eventIndex) < 0) {
        if (coduo_int32_from_bits((uint32_t)client->ps.eventIndex -
                                        (uint32_t)client->ps.oldEventIndex) >
            (int32_t)PREDICTABLE_EVENT_RING_COUNT) {
            client->ps.oldEventIndex = coduo_int32_from_bits(
                (uint32_t)client->ps.eventIndex -
                PREDICTABLE_EVENT_RING_COUNT);
        }
        ent->tempEffectId =
            client->ps.eventParms.ring[
                client->ps.oldEventIndex & PREDICTABLE_EVENT_RING_MASK] &
            PREDICTABLE_EVENT_BYTE_MASK;
        client->ps.oldEventIndex = coduo_int32_from_bits(
            (uint32_t)client->ps.oldEventIndex + 1u);
    } else {
        ent->tempEffectId = 0;
    }

    if (client->ps.lastEventIndex != client->ps.eventIndex &&
        coduo_int32_from_bits((uint32_t)client->ps.lastEventIndex -
                                    (uint32_t)client->ps.eventIndex) >= 0) {
        client->ps.lastEventIndex = client->ps.eventIndex;
    }

    for (lastEventIndex = client->ps.lastEventIndex;
         lastEventIndex != client->ps.eventIndex;
         lastEventIndex =
             coduo_int32_from_bits((uint32_t)lastEventIndex + 1u)) {
        int event = (uint8_t)client->ps.events[
            lastEventIndex & PREDICTABLE_EVENT_RING_MASK];
        int eventIndex;

        G_PlayerEvent(ent->s_number, event);

        for (eventIndex = 0; pEventSingleClientList[eventIndex] > 0 &&
                             pEventSingleClientList[eventIndex] != event;
             eventIndex++) {
        }

        if (pEventSingleClientList[eventIndex] < 0) {
            ent->events[ent->eventCount & PREDICTABLE_EVENT_RING_MASK] =
                event;
            ent->eventParms[
                ent->eventCount & PREDICTABLE_EVENT_RING_MASK] =
                client->ps.eventParms.ring[
                    lastEventIndex & PREDICTABLE_EVENT_RING_MASK] &
                PREDICTABLE_EVENT_BYTE_MASK;
            ent->eventCount =
                coduo_int32_from_bits((uint32_t)ent->eventCount + 1u);
        }
    }

    client->ps.lastEventIndex = client->ps.eventIndex;
    ent->entityStateWeapon = (uint8_t)client->ps.currentWeapon;
    ent->groundEntityNum = (uint16_t)client->ps.groundEntityNum;

    if ((client->ps.playerStateFlags & PS_FLAG_LINKED_ENTITY_TYPE_MASK) == 0) {
        ent->s_eType = ET_INVISIBLE;
    } else {
        ent->s_eType = ET_PLAYER;
    }

    /* Binary uses x87 fistp with RC=truncate for snapshot snapping. */
    if (snap != 0) {
        ent->pos.trBase[0] = game_compat_bg_snap_float(ent->pos.trBase[0]);
        ent->pos.trBase[1] = game_compat_bg_snap_float(ent->pos.trBase[1]);
        ent->pos.trBase[2] = game_compat_bg_snap_float(ent->pos.trBase[2]);
        ent->apos.trBase[0] = game_compat_bg_snap_float(ent->apos.trBase[0]);
        ent->apos.trBase[1] = game_compat_bg_snap_float(ent->apos.trBase[1]);
        ent->apos.trBase[2] = game_compat_bg_snap_float(ent->apos.trBase[2]);
    }

    ent->entityStateAnim.playerAnim.legsAnim = client->ps.legsAnim;
    ent->entityStateAnim.playerAnim.torsoAnim = client->ps.torsoAnim;
    ent->clientNum = client->ps.psClientNum;

    if ((client->ps.entityStateFlags & PS_FLAG_FIRE_POSITION_CHECK_MASK) != 0) {
        ent->vehicleEntityNum = client->ps.viewLockedEntityNum;
    }

    /*
     * In this player-entity export path, eventParm carries vehicle animation
     * state: vehicle position, vehicle type, and vehicle animation pitch.
     */
    ent->eventParm = coduo_int32_from_bits(
        ((uint32_t)ent->eventParm & ~VEHICLE_ANIM_STATE_POS_MASK) |
        ((uint32_t)client->ps.vehiclePosition <<
         VEHICLE_ANIM_STATE_POS_SHIFT));
    ent->eventParm = coduo_int32_from_bits(
        ((uint32_t)ent->eventParm & ~VEHICLE_ANIM_STATE_TYPE_MASK) |
        ((uint32_t)client->ps.vehicleType <<
         VEHICLE_ANIM_STATE_TYPE_SHIFT));
    ent->eventParm = coduo_int32_from_bits(
        ((uint32_t)ent->eventParm & ~VEHICLE_ANIM_STATE_MOTION_MASK) |
        ((uint32_t)client->ps.vehicleMotion <<
         VEHICLE_ANIM_STATE_MOTION_SHIFT));

    if (client->ps.pmType < PM_TYPE_DEAD) {
        ent->s_flags &= ~ENTITY_STATE_DEAD_FLAG;
    } else {
        ent->s_flags |= ENTITY_STATE_DEAD_FLAG;
    }

    if ((client->ps.playerStateFlags & PLAYER_STATE_ADS_FLAG) == 0) {
        ent->s_flags &= ~ENTITY_STATE_ADS_FLAG;
    } else {
        ent->s_flags |= ENTITY_STATE_ADS_FLAG;
    }

    ent->clientInfoLeanFraction = client->ps.leanFraction;
    effectiveStance = PM_GetEffectiveStance(&client->ps);
    if (effectiveStance == EFFECTIVE_STANCE_PRONE) {
        int viewHeightLerpTime =
            PM_GetViewHeightLerpTime(&client->ps, client->ps.viewHeightLerpTarget,
                                     client->ps.viewHeightLerpDown);
        int32_t viewHeightLerpElapsed = coduo_int32_from_bits(
            (uint32_t)client->ps.commandTime -
            (uint32_t)client->ps.viewHeightLerpTime);
        float lerp = (float)viewHeightLerpElapsed /
                     (float)viewHeightLerpTime;

        if (lerp < 0.0f) {
            lerp = 0.0f;
        } else if (lerp > 1.0f) {
            lerp = 1.0f;
        }
        if (client->ps.viewHeightLerpDown == 0) {
            lerp = 1.0f - lerp;
        }

        ent->entityAngles[0] = client->ps.torsoHeight * lerp;
        ent->entityAngles[1] = AngleNormalize180(client->ps.torsoPitch) * lerp;
        ent->entityAngles[2] = AngleNormalize180(client->ps.waistPitch) * lerp;
    } else {
        ent->entityAngles[0] = 0.0f;
        ent->entityAngles[1] = 0.0f;
        ent->entityAngles[2] = 0.0f;
    }
}

/* NOT_FROM_ORIGINAL_SOURCE: diagnostic-only accessor used by BG_CheckProneValid
 * debug drawing; no standalone original body. */
static int game_compat_bg_prone_debug_enabled(void)
{
    return g_debugProneCheck.integer != 0;
}

/* NOT_FROM_ORIGINAL_SOURCE: debug depth helper for prone diagnostic drawing. */
static int game_compat_bg_prone_debug_depth(void)
{
    return g_debugProneCheckDepthCheck.integer;
}

/* NOT_FROM_ORIGINAL_SOURCE: local vector construction helper for prone trace reconstruction. */
static void game_compat_bg_vec3_set(float *out, float x, float y, float z)
{
    out[0] = x;
    out[1] = y;
    out[2] = z;
}

/* NOT_FROM_ORIGINAL_SOURCE: local vector copy helper for prone trace reconstruction. */
static void game_compat_bg_vec3_copy(const float *in, float *out)
{
    out[0] = in[0];
    out[1] = in[1];
    out[2] = in[2];
}

/* NOT_FROM_ORIGINAL_SOURCE: debug-line helper gated by g_debugProneCheck. */
static void game_compat_bg_prone_debug_line(const float *start, const float *end,
                              const float *color)
{
    if (game_compat_bg_prone_debug_enabled()) {
        G_DebugLine(start, end, color, game_compat_bg_prone_debug_depth(), 1);
    }
}

/* NOT_FROM_ORIGINAL_SOURCE: local failure cleanup helper for BG_CheckProneValid. */
static int game_compat_bg_prone_failure(int allowFallback, float *groundOffset,
                           float *pitchDown, float *pitchUp)
{
    if (allowFallback != 0) {
        return 0;
    }
    if (groundOffset != NULL) {
        *groundOffset = 0.0f;
    }
    if (pitchDown != NULL) {
        *pitchDown = 0.0f;
    }
    if (pitchUp != NULL) {
        *pitchUp = 0.0f;
    }
    return 1;
}

/* ------------------------------------------------------------------ */
/*  0x2162c  BG_CheckProneValid                                       */
/* ------------------------------------------------------------------ */

/* VERIFIED_DECOMPILER(0x2162c, 3162c_BG_CheckProneValid.c, VERIFY-P1-BGSTATE-2026-06-17): DATAFLOW_VERIFIED - prone trace geometry, masks, failure tests, fraction/content checks, debug draw gates, and output ground/pitch values checked against current decompiler output. */
int BG_CheckProneValid(int clientNum, const float *origin, float radius,
                       float height, float yaw, float *groundOffset,
                       float *pitchDown, float *pitchUp,
                       int skipInitialTrace, int allowFallback,
                       const float *groundNormal, pm_trace_fn_t traceFunc,
                       pm_trace_fn_t traceDownFunc, int useAltContentMask,
                       float proneLength, int checkForwardClearance,
                       pm_entity_type_fn_t entityTypeFunc)
{
    trace_t trace;
    vec3_t mins;
    vec3_t maxs;
    vec3_t start;
    vec3_t end;
    vec3_t angles;
    vec3_t forward;
    vec3_t right;
    vec3_t up;
    vec3_t frontHit;
    vec3_t middleHit;
    vec3_t rearHit;
    vec3_t delta;
    float frontClearance = proneLength;
    int blockedForward = 0;
    int contentMask;

    if (game_compat_bg_prone_debug_enabled()) {
        vec3_t debugMins;
        vec3_t debugMaxs;

        game_compat_bg_vec3_set(debugMins, origin[0] - radius, origin[1] - radius,
                   origin[2]);
        game_compat_bg_vec3_set(debugMaxs, origin[0] + radius, origin[1] + radius,
                   origin[2] + height);
        G_DebugBox(debugMins, debugMaxs, BG_COLOR_MD_CYAN,
                   game_compat_bg_prone_debug_depth(), 1);
    }

    contentMask = useAltContentMask ? PRONE_CONTENTS_ALT
                                    : PRONE_CONTENTS_DEFAULT;

    if (skipInitialTrace == 0) {
        game_compat_bg_vec3_set(mins, -radius, -radius, 0.0f);
        game_compat_bg_vec3_set(maxs, radius, radius, height);
        game_compat_bg_vec3_copy(origin, start);
        game_compat_bg_vec3_set(end, origin[0], origin[1], origin[2] + 10.0f);
        traceDownFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
        if ((trace.contents & PRONE_BLOCKING_CONTENTS) != 0 &&
            trace.startsolid != 0) {
            return 0;
        }
        if (trace.allsolid != 0) {
            return 0;
        }

        if (entityTypeFunc != NULL) {
            game_compat_bg_vec3_set(end, origin[0], origin[1], origin[2] - 10.0f);
            traceDownFunc(&trace, start, mins, maxs, end, clientNum,
                          contentMask);
            if (entityTypeFunc(trace.entityNum) == ET_VEHICLE) {
                return 0;
            }
        }
    }

    if (allowFallback != 0 && groundNormal != NULL &&
        groundNormal[2] < PRONE_GROUND_NORMAL_MIN_Z) {
        return 0;
    }

    game_compat_bg_vec3_set(mins, -PRONE_RADIUS, -PRONE_RADIUS, -PRONE_RADIUS);
    game_compat_bg_vec3_set(maxs, PRONE_RADIUS, PRONE_RADIUS, PRONE_RADIUS);
    game_compat_bg_vec3_set(angles, 0.0f, yaw - 180.0f, 0.0f);
    AngleVectors(angles, forward, right, up);
    height -= PRONE_RADIUS;

    if (checkForwardClearance != 0) {
        game_compat_bg_vec3_set(start, origin[0], origin[1], origin[2] + 8.0f);
        game_compat_bg_vec3_set(end,
                   (float)((long double)start[0] -
                           (long double)forward[0] *
                               PRONE_FORWARD_CLEARANCE),
                   (float)((long double)start[1] -
                           (long double)forward[1] *
                               PRONE_FORWARD_CLEARANCE),
                   (float)((long double)start[2] -
                           (long double)forward[2] *
                               PRONE_FORWARD_CLEARANCE));
        traceFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
        if (trace.fraction < 1.0f) {
            game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_RED);
            return 0;
        }
    }

    game_compat_bg_vec3_set(start, origin[0], origin[1], origin[2] + height);
    game_compat_bg_vec3_set(end,
               (float)((long double)start[0] +
                       ((long double)proneLength - PRONE_RADIUS) *
                           (long double)forward[0]),
               (float)((long double)start[1] +
                       ((long double)proneLength - PRONE_RADIUS) *
                           (long double)forward[1]),
               (float)((long double)start[2] +
                       ((long double)proneLength - PRONE_RADIUS) *
                           (long double)forward[2]));
    traceFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
    if (game_compat_bg_prone_debug_enabled()) {
        G_DebugCircleEx(start, PRONE_RADIUS, right, BG_COLOR_MD_CYAN,
                        game_compat_bg_prone_debug_depth(), 1);
        G_DebugCircleEx(start, PRONE_RADIUS, up, BG_COLOR_MD_CYAN,
                        game_compat_bg_prone_debug_depth(), 1);
    }

    if (trace.fraction < 1.0f) {
        if (allowFallback == 0) {
            game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_RED);
            return 0;
        }

        blockedForward = 1;
        frontClearance = (float)(
            ((long double)proneLength - PRONE_RADIUS) *
                (long double)trace.fraction +
            PRONE_RADIUS);
        if (frontClearance < radius + 2.0f) {
            game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_RED);
            return 0;
        }

        if ((long double)frontClearance <
            (long double)height * 0.7f + PRONE_FORWARD_PROBE) {
            float traceDistance;

            game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_RED);
            blockedForward = 0;
            end[2] += 22.0f;
            game_compat_bg_vec3_set(delta, end[0] - start[0], end[1] - start[1],
                       end[2] - start[2]);
            traceDistance = VectorNormalize2(delta, forward);
            traceFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
            if (trace.fraction < 1.0f) {
                blockedForward = 1;
                frontClearance = (float)(
                    (long double)trace.fraction *
                        (long double)traceDistance +
                    PRONE_RADIUS);
                if ((long double)frontClearance <
                    (long double)height * 0.7f +
                        PRONE_FORWARD_PROBE) {
                    game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_RED);
                    return 0;
                }
                game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_YELLOW);
            } else {
                game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_GREEN);
                frontClearance = proneLength;
            }
        } else {
            game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_YELLOW);
        }
    } else {
        game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_GREEN);
        frontClearance = proneLength;
    }

    game_compat_bg_vec3_copy(trace.endpos, frontHit);

    game_compat_bg_vec3_set(end,
               (float)((long double)origin[0] +
                       (long double)forward[0] * PRONE_FORWARD_PROBE),
               (float)((long double)origin[1] +
                       (long double)forward[1] * PRONE_FORWARD_PROBE),
               (float)((long double)origin[2] +
                       (long double)forward[2] * PRONE_FORWARD_PROBE));
    /* 0x21fe6..0x22004: stock rounds origin+forward*24 to float first, then
     * adds height in a second float rounding. */
    end[2] += height;
    game_compat_bg_vec3_copy(end, start);
    end[2] = (float)((long double)start[2] -
                     (((long double)radius * 2.5f +
                       (long double)height) -
                      PRONE_RADIUS));
    traceFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
    if (trace.fraction == 1.0f) {
        game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_RED);
        return game_compat_bg_prone_failure(allowFallback, groundOffset, pitchDown, pitchUp);
    }
    /* VERIFIED_DECOMPILER(0x2162c, 3162c_BG_CheckProneValid.c, VERIFY-P1-BGSTATE-2026-06-17): DATAFLOW_VERIFIED - this middle downward probe rejects only no-hit and shallow-normal results; contents rejection is on later probes. */
    if (trace.normal[2] < PRONE_GROUND_NORMAL_MIN_Z) {
        return 0;
    }

    game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_GREEN);
    game_compat_bg_vec3_copy(trace.endpos, middleHit);
    if (blockedForward != 0) {
        float slopeDrop = (float)(
            (((long double)radius * 2.5f + (long double)height) -
             PRONE_RADIUS) *
                (long double)trace.fraction +
            PRONE_RADIUS);

        if ((long double)frontClearance - (long double)slopeDrop <
            (long double)slopeDrop * -0.75f) {
            game_compat_bg_prone_debug_line(frontHit, middleHit, BG_COLOR_RED);
            return game_compat_bg_prone_failure(allowFallback, groundOffset, pitchDown,
                                   pitchUp);
        }

        game_compat_bg_prone_debug_line(frontHit, middleHit, BG_COLOR_MD_CYAN);
        /* 0x22236..0x222ce: stock stores the raw hit difference to delta,
         * then accumulates forward*radius per component, then the extra z
         * radius pad -- each step is a separate float rounding. */
        game_compat_bg_vec3_set(delta, frontHit[0] - middleHit[0],
                   frontHit[1] - middleHit[1], frontHit[2] - middleHit[2]);
        delta[0] = (float)((long double)delta[0] +
                           (long double)forward[0] * PRONE_RADIUS);
        delta[1] = (float)((long double)delta[1] +
                           (long double)forward[1] * PRONE_RADIUS);
        delta[2] = (float)((long double)delta[2] +
                           (long double)forward[2] * PRONE_RADIUS);
        delta[2] += PRONE_RADIUS;
        VectorNormalize(delta);
        /* 0x222e4..0x2238f: stock first stores start + ((len-6)-24)*delta
         * into end[0..2] (one float rounding each), then averages x/y with
         * the far probe point, reloading the rounded end values. */
        end[0] = (float)((long double)start[0] +
                         (((long double)proneLength - PRONE_RADIUS) -
                          PRONE_FORWARD_PROBE) *
                             (long double)delta[0]);
        end[1] = (float)((long double)start[1] +
                         (((long double)proneLength - PRONE_RADIUS) -
                          PRONE_FORWARD_PROBE) *
                             (long double)delta[1]);
        end[2] = (float)((long double)start[2] +
                         (((long double)proneLength - PRONE_RADIUS) -
                          PRONE_FORWARD_PROBE) *
                             (long double)delta[2]);
        end[0] = (float)((
            ((long double)proneLength - PRONE_RADIUS) *
                (long double)forward[0] +
            (long double)origin[0] + (long double)end[0]) *
                         0.5f);
        end[1] = (float)((
            ((long double)proneLength - PRONE_RADIUS) *
                (long double)forward[1] +
            (long double)origin[1] + (long double)end[1]) *
                         0.5f);

        traceFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
        if (trace.fraction < 1.0f) {
            game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_RED);
            game_compat_bg_vec3_copy(trace.endpos, start);
            start[2] += 18.0f;
            end[2] += 18.0f;
            traceFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
            if (trace.fraction < 1.0f) {
                game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_RED);
                return game_compat_bg_prone_failure(allowFallback, groundOffset, pitchDown,
                                       pitchUp);
            }
        }

        game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_GREEN);
        game_compat_bg_vec3_copy(trace.endpos, frontHit);
    }

    game_compat_bg_vec3_copy(frontHit, start);
    game_compat_bg_vec3_set(end, frontHit[0], frontHit[1],
               (float)((long double)frontHit[2] -
                       (((long double)frontHit[2] -
                         (long double)middleHit[2]) +
                        ((long double)frontHit[2] -
                         (long double)middleHit[2]) +
                        (long double)radius)));
    traceFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
    if (trace.fraction == 1.0f) {
        game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_RED);
        return game_compat_bg_prone_failure(allowFallback, groundOffset, pitchDown, pitchUp);
    }
    if (trace.fraction < 1.0f &&
        (trace.contents & PRONE_BLOCKING_CONTENTS) != 0) {
        return game_compat_bg_prone_failure(allowFallback, groundOffset, pitchDown, pitchUp);
    }
    if (trace.normal[2] < PRONE_GROUND_NORMAL_MIN_Z) {
        return 0;
    }

    game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_GREEN);
    game_compat_bg_vec3_copy(trace.endpos, frontHit);

    game_compat_bg_vec3_set(start, origin[0], origin[1], origin[2] + height);
    game_compat_bg_vec3_set(
        end, origin[0], origin[1],
        (float)((long double)origin[2] - (long double)radius * 1.5f));
    traceFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
    if (trace.fraction == 1.0f) {
        game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_RED);
        return game_compat_bg_prone_failure(allowFallback, groundOffset, pitchDown, pitchUp);
    }
    if (trace.fraction < 1.0f &&
        (trace.contents & PRONE_BLOCKING_CONTENTS) != 0) {
        return game_compat_bg_prone_failure(allowFallback, groundOffset, pitchDown, pitchUp);
    }
    if (trace.normal[2] < PRONE_GROUND_NORMAL_MIN_Z) {
        return 0;
    }

    game_compat_bg_prone_debug_line(start, trace.endpos, BG_COLOR_GREEN);
    game_compat_bg_vec3_copy(trace.endpos, rearHit);
    {
        long double groundDelta =
            ((long double)rearHit[2] - (long double)origin[2]) -
            PRONE_RADIUS;

        if (groundDelta < 0.0L) {
            groundDelta = -groundDelta;
        }
        if (groundDelta > PRONE_MAX_GROUND_DELTA) {
            game_compat_bg_prone_debug_line(origin, rearHit, BG_COLOR_RED);
            return game_compat_bg_prone_failure(
                allowFallback, groundOffset, pitchDown, pitchUp);
        }
    }

    game_compat_bg_vec3_set(delta, middleHit[0] - rearHit[0], middleHit[1] - rearHit[1],
               middleHit[2] - rearHit[2]);
    {
        float pitch1 = vectopitch(delta);
        float pitch2;
        float pitchDelta;

        game_compat_bg_vec3_set(delta, frontHit[0] - middleHit[0],
                   frontHit[1] - middleHit[1], frontHit[2] - middleHit[2]);
        pitch2 = vectopitch(delta);
        pitchDelta = AngleSubtract(pitch2, pitch1);
        if (pitchDelta < -45.0f || pitchDelta > 70.0f) {
            game_compat_bg_prone_debug_line(rearHit, middleHit, BG_COLOR_MAGENTA);
            game_compat_bg_prone_debug_line(middleHit, frontHit, BG_COLOR_MAGENTA);
            return game_compat_bg_prone_failure(allowFallback, groundOffset, pitchDown,
                                   pitchUp);
        }
    }

    game_compat_bg_vec3_set(mins, -0.0f, -0.0f, -0.0f);
    game_compat_bg_vec3_set(maxs, 0.0f, 0.0f, 0.0f);
    game_compat_bg_vec3_set(start, rearHit[0], rearHit[1], rearHit[2] + 5.0f);
    game_compat_bg_vec3_set(end, middleHit[0], middleHit[1], middleHit[2] + 5.0f);
    traceFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
    if (trace.fraction < 1.0f) {
        game_compat_bg_prone_debug_line(start, end, BG_COLOR_RED);
        return game_compat_bg_prone_failure(allowFallback, groundOffset, pitchDown, pitchUp);
    }

    game_compat_bg_prone_debug_line(start, end, BG_COLOR_GREEN);
    game_compat_bg_vec3_copy(end, start);
    game_compat_bg_vec3_set(end, frontHit[0], frontHit[1], frontHit[2] + 5.0f);
    traceFunc(&trace, start, mins, maxs, end, clientNum, contentMask);
    if (trace.fraction < 1.0f) {
        game_compat_bg_prone_debug_line(start, end, BG_COLOR_RED);
        return game_compat_bg_prone_failure(allowFallback, groundOffset, pitchDown, pitchUp);
    }

    game_compat_bg_prone_debug_line(start, end, BG_COLOR_GREEN);
    if (game_compat_bg_prone_debug_enabled()) {
        G_DebugCircleEx(rearHit, PRONE_RADIUS, right, BG_COLOR_MD_CYAN,
                        game_compat_bg_prone_debug_depth(), 1);
        G_DebugCircleEx(rearHit, PRONE_RADIUS, up, BG_COLOR_MD_CYAN,
                        game_compat_bg_prone_debug_depth(), 1);
        G_DebugCircleEx(middleHit, PRONE_RADIUS, right, BG_COLOR_MD_CYAN,
                        game_compat_bg_prone_debug_depth(), 1);
        G_DebugCircleEx(middleHit, PRONE_RADIUS, up, BG_COLOR_MD_CYAN,
                        game_compat_bg_prone_debug_depth(), 1);
        G_DebugCircleEx(frontHit, PRONE_RADIUS, right, BG_COLOR_MD_CYAN,
                        game_compat_bg_prone_debug_depth(), 1);
        G_DebugCircleEx(frontHit, PRONE_RADIUS, up, BG_COLOR_MD_CYAN,
                        game_compat_bg_prone_debug_depth(), 1);
        G_DebugLine(rearHit, middleHit, BG_COLOR_CYAN, game_compat_bg_prone_debug_depth(),
                    1);
        G_DebugLine(middleHit, frontHit, BG_COLOR_CYAN, game_compat_bg_prone_debug_depth(),
                    1);
    }

    if (groundOffset != NULL) {
        *groundOffset = (float)(
            ((long double)rearHit[2] - (long double)origin[2]) -
            PRONE_RADIUS);
    }
    if (pitchDown != NULL) {
        game_compat_bg_vec3_set(delta, rearHit[0] - middleHit[0],
                   rearHit[1] - middleHit[1], rearHit[2] - middleHit[2]);
        *pitchDown = AngleNormalize180(vectopitch(delta));
    }
    if (pitchUp != NULL) {
        game_compat_bg_vec3_set(delta, middleHit[0] - frontHit[0],
                   middleHit[1] - frontHit[1], middleHit[2] - frontHit[2]);
        *pitchUp = AngleNormalize180(vectopitch(delta));
    }

    return 1;
}

/* ------------------------------------------------------------------ */
/*  0x22ff3  BG_CheckProne                                            */
/* ------------------------------------------------------------------ */

/* VERIFIED_DECOMPILER(0x22ff3, 32ff3_BG_CheckProne.c, VERIFY-WAVE3-BGSTATE-PRONE-WRAPPER-2026-06-17): DATAFLOW_VERIFIED - wrapper forwards all arguments to BG_CheckProneValid with inserted zero at arg16; decompiler's void prototype is an artifact, callers consume the preserved int return register. */
int BG_CheckProne(int clientNum, const float *origin, float radius,
                  float height, float yaw, float *groundOffset,
                  float *pitchDown, float *pitchUp, int skipInitialTrace,
                  int allowFallback, const float *groundNormal,
                  pm_trace_fn_t traceFunc, pm_trace_fn_t traceDownFunc,
                  int useAltContentMask, float proneLength,
                  pm_entity_type_fn_t entityTypeFunc)
{
    return BG_CheckProneValid(clientNum, origin, radius, height, yaw,
                              groundOffset, pitchDown, pitchUp,
                              skipInitialTrace, allowFallback, groundNormal,
                              traceFunc, traceDownFunc, useAltContentMask,
                              proneLength, 0, entityTypeFunc);
}

/* ------------------------------------------------------------------ */
/*  0x23087  BG_GetVehiclePosTag                                      */
/* ------------------------------------------------------------------ */

/* VERIFIED_DECOMPILER(0x23087, 33087_BG_GetVehiclePosTag.c, VERIFY-WAVE4-BGSTATE-VEHICLE-2026-06-17): DATAFLOW_VERIFIED - seven-entry vehicle position tag table and indexed return checked against current decompiler output. */
const char *BG_GetVehiclePosTag(int vehiclePos)
{
    const char *vehiclePosTags[7] = {
        "*unused*",
        "tag_player",
        "tag_secondary_player",
        "tag_passenger",
        "tag_passenger2",
        "tag_passenger3",
        "tag_passenger4",
    };

    return vehiclePosTags[vehiclePos];
}

/* ------------------------------------------------------------------ */
/*  0x230e5  BG_GetVehiclePosOffset                                   */
/* ------------------------------------------------------------------ */

/* VERIFIED_DECOMPILER(0x230e5, 330e5_BG_GetVehiclePosOffset.c, VERIFY-WAVE4-BGSTATE-VEHICLE-2026-06-17): DATAFLOW_VERIFIED - artillery, tank position 2, tank position 1, and vec3_origin fallback checked against current decompiler output. */
const float *BG_GetVehiclePosOffset(int vehicleType, int vehiclePos)
{
    if (vehicleType == VEHICLE_TYPE_ARTILLERY) {
        return bgVehicleArtilleryPositionOffset;
    }
    if (vehicleType == VEHICLE_TYPE_TANK && vehiclePos == 2) {
        return bgVehicleTankPosition2Offset;
    }
    if (vehicleType == VEHICLE_TYPE_TANK && vehiclePos == 1) {
        return bgVehicleTankPosition1Offset;
    }
    return vec3_origin;
}

/* ------------------------------------------------------------------ */
/*  0x23148  BG_AllowPlayerWeaponAtVehiclePos                         */
/* ------------------------------------------------------------------ */

/* VERIFIED_DECOMPILER(0x23148, 33148_BG_AllowPlayerWeaponAtVehiclePos.c, VERIFY-WAVE4-BGSTATE-VEHICLE-2026-06-17): DATAFLOW_VERIFIED - vehicle type 1 and vehicle position 3 predicate checked against current decompiler output. */
int BG_AllowPlayerWeaponAtVehiclePos(int vehicleType, int vehiclePos)
{
    return vehicleType == VEHICLE_TYPE_4_WHEEL && vehiclePos == 3;
}
