Files
BT411/game/reconstructed/btvisgnd.cpp
T
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00

287 lines
9.6 KiB
C++

//###########################################################################
//
// btvisgnd.cpp
//
// PORT ADDITION (presentation layer -- NOT a reconstruction): visual-ground
// conform for the external chase view.
//
// WHY: the mech SIM rides the coarse 1995 collision solids (cones/blocks;
// the authentic snap puts origin.y = solid surfaceY exactly -- see
// Mech::AuthenticGroundAndCollide). The VISUAL terrain mesh runs 0..~2.1u
// ABOVE those solids on mound/mesa slopes (LOD0-exact measurement, dbase),
// so an upright mech's feet clip into the visible sand there. In 1995 this
// was INVISIBLE: the game rendered cockpit-only (you can never see your own
// feet; other mechs are 100+m away). Our port adds a close external camera,
// so the port also adds this presentation fix: sample the RENDERED terrain
// triangles under the mech and lift the render matrix (localToWorld ONLY --
// never localOrigin) by the visual-minus-solid gap. Collision, aim, damage,
// network dead-reckoning all stay on the authentic solid model.
//
// Data source: the map's MakeMessage stream in BTL4.RES (class-42 static
// terrain instances) + each instance's LOD0 geometry via the engine BGF
// loader (bgfload.h). No tuning constants -- the exact content geometry.
//
// Gate: BT_VISUAL_GROUND (default ON; =0 restores raw solid-height render).
// Diagnostics under BT_GROUND_LOG.
//
//###########################################################################
#include <bt.hpp>
#include <APP.hpp> // application / GetCurrentMission / GetResourceFile
#include <MISSION.hpp> // Mission::GetMapID
#include <bgfload.h>
#include <vector>
#include <string>
#include <map>
#include <cstring>
#include <cstdlib>
#include <cmath>
//---------------------------------------------------------------------------//
// Geometry cache
//---------------------------------------------------------------------------//
namespace
{
struct VgTri
{
float ax, ay, az, bx, by, bz, cx, cy, cz;
};
struct VgMesh
{
std::vector<VgTri> tris;
float minx, maxx, minz, maxz; // model-local XZ bound
};
struct VgInst
{
int mesh; // index into gMeshes
float px, py, pz; // world placement
float minx, maxx, minz, maxz; // world XZ bound (placed)
};
std::vector<VgMesh> gMeshes;
std::vector<VgInst> gInsts;
int gState = 0; // 0 = not tried, 1 = ready, -1 = unavailable
bool SkippedName(const char *n)
{
// backdrop / sky pieces -- vertical scenery, never ground underfoot
if (strstr(n, "sky") || strstr(n, "SKY")) return true;
if (_strnicmp(n, "profile", 7) == 0) return true;
if (_strnicmp(n, "rwin", 4) == 0) return true;
return false;
}
int LoadMeshFor(const char *resource_name)
{
static std::map<std::string, int> cache; // name -> gMeshes idx (-1 = no geometry)
std::string key(resource_name);
std::map<std::string, int>::iterator it = cache.find(key);
if (it != cache.end())
return it->second;
int idx = -1;
BgfData data;
std::string file = key + ".bgf";
if (LoadBgfFile(file, data) && data.ok && !data.indices.empty())
{
VgMesh m;
m.minx = m.minz = 1e9f;
m.maxx = m.maxz = -1e9f;
// The loader emits DOUBLE-SIDED triangles: emitTri pushes (a,b,c) then
// (a,c,b) -- 6 indices per source triangle. Take the forward one.
for (size_t i = 0; i + 5 < data.indices.size(); i += 6)
{
const BgfVtx &A = data.verts[data.indices[i]];
const BgfVtx &B = data.verts[data.indices[i + 1]];
const BgfVtx &C = data.verts[data.indices[i + 2]];
VgTri t = { A.x, A.y, A.z, B.x, B.y, B.z, C.x, C.y, C.z };
m.tris.push_back(t);
float xs[3] = { A.x, B.x, C.x }, zs[3] = { A.z, B.z, C.z };
for (int k = 0; k < 3; ++k)
{
if (xs[k] < m.minx) m.minx = xs[k];
if (xs[k] > m.maxx) m.maxx = xs[k];
if (zs[k] < m.minz) m.minz = zs[k];
if (zs[k] > m.maxz) m.maxz = zs[k];
}
}
if (!m.tris.empty())
{
gMeshes.push_back(m);
idx = (int)gMeshes.size() - 1;
}
}
cache[key] = idx;
return idx;
}
// Build the instance table from the CURRENT mission's map message stream.
// Map stream record layout (BTL4.RES, byte-verified):
// [0] u32 count
// records: u32 mlen | i32 mid | u32 mflags | ... i32 class @+28 |
// i32 resourceID @+40 | float pos[3] @+48 ; advance (mlen+3)&~3
// mflags bit1 = include-marker (skip); class 42 = static terrain/cultural.
bool BuildTables()
{
if (application == 0) return false;
Mission *mission = application->GetCurrentMission();
ResourceFile *rf = application->GetResourceFile();
if (mission == 0 || rf == 0) return false;
ResourceDescription *map = rf->FindResourceDescription(mission->GetMapID());
if (map == 0) return false;
map->Lock();
const unsigned char *base = (const unsigned char *)map->resourceAddress;
size_t size = map->resourceSize;
if (base == 0 || size < 4) return false;
int count = *(const int *)base;
size_t p = 4;
int made = 0, skipped = 0;
for (int i = 0; i < count && p + 52 <= size; ++i)
{
unsigned mlen = *(const unsigned *)(base + p);
unsigned mflags = *(const unsigned *)(base + p + 8);
int cls = *(const int *)(base + p + 28);
int rid = *(const int *)(base + p + 40);
const float *pos = (const float *)(base + p + 48);
p += (mlen + 3) & ~3u;
if (mflags & 0x2) continue; // include marker
if (cls != 42) continue; // terrain/static cultural only
ResourceDescription *rd = rf->FindResourceDescription(rid);
if (rd == 0) continue;
if (SkippedName(rd->resourceName)) { ++skipped; continue; }
int mi = LoadMeshFor(rd->resourceName);
if (mi < 0) { ++skipped; continue; }
VgInst inst;
inst.mesh = mi;
inst.px = pos[0]; inst.py = pos[1]; inst.pz = pos[2];
inst.minx = gMeshes[mi].minx + pos[0];
inst.maxx = gMeshes[mi].maxx + pos[0];
inst.minz = gMeshes[mi].minz + pos[2];
inst.maxz = gMeshes[mi].maxz + pos[2];
gInsts.push_back(inst);
++made;
}
if (getenv("BT_GROUND_LOG"))
DEBUG_STREAM << "[visgnd] sampler ready: " << made << " terrain instances ("
<< gMeshes.size() << " meshes, " << skipped << " skipped)\n" << std::flush;
return made > 0;
}
// Highest visual surface at world (x,z) whose height lies in [yLo, yHi].
// The band keeps the query LOCAL: standing at a butte base it picks the skirt
// underfoot, not the mesa top 40u above.
bool SampleBand(float x, float z, float yLo, float yHi, float *out)
{
bool found = false;
float best = 0.0f;
for (size_t ii = 0; ii < gInsts.size(); ++ii)
{
const VgInst &inst = gInsts[ii];
if (x < inst.minx || x > inst.maxx || z < inst.minz || z > inst.maxz)
continue;
const float lx = x - inst.px, lz = z - inst.pz;
const VgMesh &m = gMeshes[inst.mesh];
for (size_t ti = 0; ti < m.tris.size(); ++ti)
{
const VgTri &t = m.tris[ti];
// point-in-triangle (XZ), sign-consistent
const float d1 = (t.bx - t.ax) * (lz - t.az) - (t.bz - t.az) * (lx - t.ax);
const float d2 = (t.cx - t.bx) * (lz - t.bz) - (t.cz - t.bz) * (lx - t.bx);
const float d3 = (t.ax - t.cx) * (lz - t.cz) - (t.az - t.cz) * (lx - t.cx);
if (!((d1 >= 0 && d2 >= 0 && d3 >= 0) || (d1 <= 0 && d2 <= 0 && d3 <= 0)))
continue;
const float area = (t.bx - t.ax) * (t.cz - t.az) - (t.bz - t.az) * (t.cx - t.ax);
if (area > -1e-6f && area < 1e-6f)
continue;
const float w1 = ((t.bx - lx) * (t.cz - lz) - (t.bz - lz) * (t.cx - lx)) / area;
const float w2 = ((t.cx - lx) * (t.az - lz) - (t.cz - lz) * (t.ax - lx)) / area;
const float w3 = 1.0f - w1 - w2;
const float y = w1 * t.ay + w2 * t.by + w3 * t.cy + inst.py;
if (y < yLo || y > yHi)
continue;
if (!found || y > best) { found = true; best = y; }
}
}
if (found) *out = best;
return found;
}
} // namespace
//---------------------------------------------------------------------------//
// BTVisualGroundLift -- render-only Y delta so the mech's feet meet the
// VISIBLE ground. Returns 0 when gated off, unavailable, or no local surface.
//---------------------------------------------------------------------------//
float
BTVisualGroundLift(float x, float y, float z)
{
static int s_on = -1;
if (s_on < 0)
{
const char *v = getenv("BT_VISUAL_GROUND");
s_on = (v == 0 || v[0] != '0') ? 1 : 0;
}
if (!s_on)
return 0.0f;
if (gState == 0)
gState = BuildTables() ? 1 : -1;
if (gState != 1)
return 0.0f;
// Local band: up to 3u of visual rising above the solid (measured max ~2.2),
// up to 1u of visual dipping below it. Outside the band = not the ground
// underfoot (cliff tops, building roofs) -> no conform.
float surf;
if (!SampleBand(x, z, y - 1.0f, y + 3.0f, &surf))
return 0.0f;
float lift = surf - y;
if (lift < -1.0f) lift = -1.0f;
if (lift > 3.0f) lift = 3.0f;
return lift;
}
//---------------------------------------------------------------------------//
// BTGroundRayHit -- march a ray (o + dir*t) against the visible terrain and
// return the first point where it meets the surface (rising ground: butte
// faces, dune fronts). Used by the weapon fire path to end the beam + place
// the impact on the ground. Returns false (no hit) for a level shot over open
// desert -- the caller then flies the beam to max range.
//---------------------------------------------------------------------------//
bool
BTGroundRayHit(float ox, float oy, float oz,
float dx, float dy, float dz,
float maxDist, float *hx, float *hy, float *hz)
{
if (gState == 0)
gState = BuildTables() ? 1 : -1;
if (gState != 1)
return false;
const float step = 4.0f;
for (float t = step; t <= maxDist; t += step)
{
const float x = ox + dx * t;
const float y = oy + dy * t;
const float z = oz + dz * t;
float surf;
// wide downward band: catch terrain rising into a mostly-horizontal ray.
if (SampleBand(x, z, y - 90.0f, y + 6.0f, &surf) && y <= surf)
{
*hx = x; *hy = surf; *hz = z;
return true;
}
}
return false;
}