475 lines
20 KiB
C++
475 lines
20 KiB
C++
// Self-contained tests for libhmap (no test framework needed).
|
|
#include <hmap/hmap.h>
|
|
#include <hmap/hmap_c.h>
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <set>
|
|
|
|
using namespace hmap;
|
|
|
|
static int g_fail = 0, g_checks = 0;
|
|
#define CHECK(cond) \
|
|
do { \
|
|
++g_checks; \
|
|
if (!(cond)) { ++g_fail; std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } \
|
|
} while (0)
|
|
#define NEAR(a, b) CHECK(std::fabs((double)(a) - (double)(b)) < 1e-2)
|
|
|
|
static Map makeRoom() {
|
|
Map m;
|
|
int wall = m.addMaterial("dev/wall"), floorM = m.addMaterial("dev/floor");
|
|
Brush room = makeBox({-128, 0, -128}, {128, 128, 128}, wall);
|
|
room.id = m.newId();
|
|
std::vector<Brush> walls = hollowBrush(room, 16);
|
|
for (Brush& b : walls) { b.id = m.newId(); m.brushes.push_back(b); }
|
|
(void)floorM;
|
|
Entity l;
|
|
l.id = m.newId(); l.classname = "light"; l.origin = {0, 100, 0};
|
|
l.set("radius", "400"); l.set("intensity", "1.0");
|
|
m.entities.push_back(l);
|
|
Entity p;
|
|
p.id = m.newId(); p.classname = "info_player_start"; p.origin = {0, 20, 60};
|
|
m.entities.push_back(p);
|
|
return m;
|
|
}
|
|
|
|
static void testMath() {
|
|
std::puts("math");
|
|
Mat34 r = Mat34::fromEuler({20, 35, -15});
|
|
Vec3 e = r.toEuler();
|
|
NEAR(e.x, 20); NEAR(e.y, 35); NEAR(e.z, -15);
|
|
Vec3 f = Mat34::fromEuler({0, 90, 0}).vector({0, 0, -1});
|
|
NEAR(f.x, -1); NEAR(f.z, 0); // yaw +90 turns -Z towards -X
|
|
Vec3 x = r.toEulerXYZ();
|
|
Mat34 rx = Mat34::rotate({0, 0, 1}, x.z) * Mat34::rotate({0, 1, 0}, x.y) * Mat34::rotate({1, 0, 0}, x.x);
|
|
for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) NEAR(rx.m[i][j], r.m[i][j]);
|
|
Mat34 inv; CHECK(r.inverse(inv));
|
|
Vec3 p = inv.point(r.point({3, 4, 5}));
|
|
NEAR(p.x, 3); NEAR(p.y, 4); NEAR(p.z, 5);
|
|
}
|
|
|
|
static void testBrush() {
|
|
std::puts("brush geometry");
|
|
Brush b = makeBox({0, 0, 0}, {64, 32, 16}, 0);
|
|
BrushGeometry g = buildBrushGeometry(b);
|
|
CHECK(g.valid);
|
|
int polys = 0;
|
|
for (auto& p : g.polys) { polys += p.size() == 4; }
|
|
CHECK(polys == 6);
|
|
NEAR(g.bounds.mx.x, 64); NEAR(g.bounds.mx.y, 32); NEAR(g.bounds.mx.z, 16);
|
|
for (size_t i = 0; i < g.polys.size(); ++i) { // CCW from outside => polygon normal == plane normal
|
|
Vec3 n = polyNormal(g.polys[i]);
|
|
CHECK(dot(n, b.faces[i].plane.n) > 0.999f);
|
|
}
|
|
CHECK(buildBrushGeometry(makeWedge({0, 0, 0}, {64, 32, 16}, 0)).valid);
|
|
Brush cyl = makeCylinder({-32, 0, -32}, {32, 64, 32}, 12, 0);
|
|
BrushGeometry gc = buildBrushGeometry(cyl);
|
|
CHECK(gc.valid && cyl.faces.size() == 14);
|
|
for (size_t i = 0; i < gc.polys.size(); ++i) CHECK(gc.polys[i].size() >= 3 && dot(polyNormal(gc.polys[i]), cyl.faces[i].plane.n) > 0.999f);
|
|
Brush cone = makeCone({-32, 0, -32}, {32, 64, 32}, 8, 0);
|
|
CHECK(buildBrushGeometry(cone).valid);
|
|
CHECK(makeStairs({0, 0, 0}, {128, 64, 64}, 8).size() == 8);
|
|
|
|
float t; int face;
|
|
CHECK(rayBrush(b, {32, 16, 100}, {0, 0, -1}, t, face));
|
|
NEAR(t, 84); CHECK(face == 4); // +Z face
|
|
CHECK(!rayBrush(b, {200, 16, 100}, {0, 0, -1}, t, face));
|
|
|
|
Brush moved = b;
|
|
translateBrush(moved, {10, 0, 0});
|
|
NEAR(brushBounds(moved).mn.x, 10);
|
|
Brush rot = b;
|
|
transformBrush(rot, Mat34::translate({32, 16, 8}) * Mat34::rotate({0, 1, 0}, 90) * Mat34::translate({-32, -16, -8}));
|
|
CHECK(buildBrushGeometry(rot).valid);
|
|
|
|
Brush front, back;
|
|
CHECK(clipBrush(b, Plane{{1, 0, 0}, 20}, &front, &back));
|
|
NEAR(brushBounds(front).mn.x, 20); NEAR(brushBounds(back).mx.x, 20);
|
|
|
|
MeshObject mesh = brushToMesh(b);
|
|
CHECK(mesh.verts.size() == 8 && mesh.polys.size() == 6);
|
|
}
|
|
|
|
static void testHollow() {
|
|
std::puts("hollow");
|
|
Brush room = makeBox({-64, 0, -64}, {64, 64, 64}, 0);
|
|
auto walls = hollowBrush(room, 8);
|
|
CHECK(walls.size() == 6);
|
|
CompiledMap cm;
|
|
Map m;
|
|
for (auto& w : walls) { w.id = m.newId(); m.brushes.push_back(w); }
|
|
cm = compile(m);
|
|
CHECK(cm.pointContents({0, 32, 0}) == 0); // inside the room: empty
|
|
CHECK((cm.pointContents({-60, 32, 0}) & CONTENTS_SOLID)); // inside a wall
|
|
CHECK((cm.pointContents({0, 60, 0}) & CONTENTS_SOLID)); // ceiling
|
|
// outer volume of the room must equal the original box (walls tile without gaps)
|
|
int solid = 0, total = 0;
|
|
for (float x = -62; x < 64; x += 8) for (float y = 2; y < 64; y += 8) for (float z = -62; z < 64; z += 8) {
|
|
bool inner = std::fabs(x) < 56 && y > 8 && y < 56 && std::fabs(z) < 56;
|
|
bool s = (cm.pointContents({x, y, z}) & CONTENTS_SOLID) != 0;
|
|
++total; solid += s;
|
|
CHECK(s == !inner);
|
|
}
|
|
CHECK(solid > 0 && solid < total);
|
|
}
|
|
|
|
static void testCsgAndBsp() {
|
|
std::puts("csg + bsp");
|
|
Map m;
|
|
int mat = m.addMaterial("dev/gray");
|
|
Brush a = makeBox({0, 0, 0}, {64, 64, 64}, mat); a.id = m.newId(); m.brushes.push_back(a);
|
|
CompiledMap one = compile(m);
|
|
CHECK(one.stats.triangles == 12);
|
|
|
|
Brush b = makeBox({64, 0, 0}, {128, 64, 64}, mat); b.id = m.newId(); m.brushes.push_back(b); // touching
|
|
CompiledMap two = compile(m);
|
|
CHECK(two.stats.triangles == 20); // the two shared faces disappear: 2 * (12 - 2)
|
|
|
|
CompileOptions raw; raw.csg = false;
|
|
CHECK(compile(m, raw).stats.triangles == 24);
|
|
|
|
Brush c = makeBox({16, 16, 16}, {48, 48, 48}, mat); c.id = m.newId(); m.brushes.push_back(c); // fully inside A
|
|
CHECK(compile(m).stats.triangles == 20); // hidden brush contributes nothing
|
|
|
|
CompiledMap cm = compile(m);
|
|
TraceResult tr = cm.trace({-100, 32, 32}, {200, 32, 32});
|
|
CHECK(tr.hit); NEAR(tr.point.x, 0); NEAR(tr.normal.x, -1);
|
|
NEAR(tr.fraction, 100.0 / 300.0);
|
|
tr = cm.trace({-100, 100, 32}, {200, 100, 32});
|
|
CHECK(!tr.hit);
|
|
tr = cm.trace({200, 32, 32}, {-100, 32, 32});
|
|
CHECK(tr.hit); NEAR(tr.point.x, 128); NEAR(tr.normal.x, 1);
|
|
tr = cm.trace({32, 32, 32}, {32, 200, 32});
|
|
CHECK(tr.hit && tr.startSolid);
|
|
CHECK((cm.pointContents({100, 32, 32}) & CONTENTS_SOLID));
|
|
CHECK(cm.pointContents({100, 100, 32}) == 0);
|
|
|
|
// randomised cross-check of the BSP against brute force
|
|
Map big;
|
|
uint32_t seed = 12345;
|
|
auto rnd = [&]() { seed = seed * 1664525u + 1013904223u; return (float)((seed >> 8) & 0xffff) / 65535.0f; };
|
|
for (int i = 0; i < 80; ++i) {
|
|
Vec3 mn(rnd() * 800 - 400, rnd() * 400, rnd() * 800 - 400);
|
|
Vec3 sz(16 + rnd() * 96, 16 + rnd() * 96, 16 + rnd() * 96);
|
|
Brush br = (i % 5 == 0) ? makeCylinder(mn, mn + sz, 8, 0) : makeBox(mn, mn + sz, 0);
|
|
br.id = big.newId();
|
|
big.brushes.push_back(br);
|
|
}
|
|
CompiledMap bc = compile(big);
|
|
CHECK(bc.stats.bspNodes > 0);
|
|
std::vector<Brush> bl = big.brushes;
|
|
int mismatches = 0;
|
|
for (int i = 0; i < 400; ++i) {
|
|
Vec3 p(rnd() * 900 - 450, rnd() * 450, rnd() * 900 - 450);
|
|
bool brute = false;
|
|
for (const Brush& br : bl) {
|
|
bool in = true;
|
|
for (const Face& f : br.faces) if (f.plane.dist(p) > 0) { in = false; break; }
|
|
brute |= in;
|
|
}
|
|
bool bsp = (bc.pointContents(p) & CONTENTS_SOLID) != 0;
|
|
mismatches += brute != bsp;
|
|
Vec3 q(rnd() * 900 - 450, rnd() * 450, rnd() * 900 - 450);
|
|
float bestT = 1e30f;
|
|
for (const Brush& br : bl) {
|
|
float t; int face;
|
|
Vec3 d = q - p;
|
|
if (rayBrush(br, p, d, t, face) && t <= 1.0f) bestT = std::min(bestT, t);
|
|
}
|
|
TraceResult r = bc.trace(p, q);
|
|
if (!brute) {
|
|
bool hitBrute = bestT <= 1.0f;
|
|
if (hitBrute != r.hit || (hitBrute && std::fabs(bestT - r.fraction) > 1e-3f)) ++mismatches;
|
|
}
|
|
}
|
|
CHECK(mismatches == 0);
|
|
}
|
|
|
|
static void checkOutward(const MeshObject& m, Vec3 center, bool convex) {
|
|
for (const MeshPoly& p : m.polys) {
|
|
Polygon poly;
|
|
for (uint32_t i : p.idx) poly.push_back(m.verts[i]);
|
|
Vec3 c;
|
|
for (const Vec3& v : poly) c += v;
|
|
c = c / (float)poly.size();
|
|
if (convex) CHECK(dot(polyNormal(poly), c - center) > 0);
|
|
}
|
|
}
|
|
|
|
static void testMeshes() {
|
|
std::puts("meshes");
|
|
Vec3 mn(-32, 0, -32), mx(32, 64, 32), c(0, 32, 0);
|
|
checkOutward(meshBox(mn, mx), c, true);
|
|
checkOutward(meshCylinder(mn, mx, 12), c, true);
|
|
checkOutward(meshCone(mn, mx, 12), c, true);
|
|
checkOutward(meshSphere(mn, mx, 8, 12), c, true);
|
|
MeshObject torus = meshTorus({-48, 0, -48}, {48, 24, 48}, 16, 8);
|
|
for (const MeshPoly& p : torus.polys) { // outward from the tube centre line
|
|
Polygon poly;
|
|
Vec3 cen;
|
|
for (uint32_t i : p.idx) { poly.push_back(torus.verts[i]); cen += torus.verts[i]; }
|
|
cen = cen / (float)poly.size();
|
|
Vec3 radial = normalize(Vec3(cen.x, 0, cen.z));
|
|
Vec3 tube = radial * 36.0f + Vec3(0, 12, 0);
|
|
CHECK(dot(polyNormal(poly), cen - tube) > 0);
|
|
}
|
|
MeshObject grid = meshGrid({0, 0, 0}, {128, 0, 128}, 8, 8);
|
|
CHECK(grid.polys.size() == 64);
|
|
for (const MeshPoly& p : grid.polys) {
|
|
Polygon poly;
|
|
for (uint32_t i : p.idx) poly.push_back(grid.verts[i]);
|
|
CHECK(polyNormal(poly).y > 0.99f);
|
|
}
|
|
noiseMesh(grid, 10, 0.02f, 3);
|
|
float lo = 1e9f, hi = -1e9f;
|
|
for (auto& v : grid.verts) { lo = std::min(lo, v.y); hi = std::max(hi, v.y); }
|
|
CHECK(hi - lo > 1.0f && hi - lo <= 20.0f);
|
|
|
|
MeshObject box = meshBox({0, 0, 0}, {32, 32, 32});
|
|
size_t polys0 = box.polys.size();
|
|
CHECK(extrudePoly(box, 2, 16)); // +Y face
|
|
CHECK(box.polys.size() == polys0 + 4);
|
|
NEAR(meshBounds(box).mx.y, 48);
|
|
|
|
Map map;
|
|
map.addMaterial("dev/gray");
|
|
map.meshes.push_back(meshSphere(mn, mx, 8, 12));
|
|
map.meshes.back().id = map.newId();
|
|
CompiledMap cm = compile(map);
|
|
CHECK(cm.batches.size() == 1 && cm.batches[0].source == GeomSource::Mesh);
|
|
for (const Vertex& v : cm.batches[0].vertices) NEAR(length(v.normal), 1.0f);
|
|
TraceResult tr = cm.trace({-100, 32, 0}, {100, 32, 0});
|
|
CHECK(tr.hit && tr.point.x < -25 && tr.point.x > -33);
|
|
CHECK(tr.objectId == map.meshes[0].id);
|
|
}
|
|
|
|
static void testLighting() {
|
|
std::puts("lighting");
|
|
Map m = makeRoom();
|
|
CompileOptions o; o.bakeLighting = true; o.lightEdge = 32; o.threads = 2;
|
|
CompiledMap cm = compile(m, o);
|
|
CHECK(cm.baked && cm.lights.size() == 1);
|
|
auto lum = [&](const Vertex& v) { return (float)(v.color & 0xff) + (float)((v.color >> 8) & 0xff) + (float)((v.color >> 16) & 0xff); };
|
|
float near = 0, far = 0; int nn = 0, nf = 0;
|
|
for (const Batch& b : cm.batches)
|
|
for (const Vertex& v : b.vertices)
|
|
if (v.normal.y < -0.9f && v.pos.y > 100) { // ceiling vertices
|
|
float d = std::sqrt(v.pos.x * v.pos.x + v.pos.z * v.pos.z);
|
|
if (d < 40) { near += lum(v); ++nn; }
|
|
else if (d > 100) { far += lum(v); ++nf; }
|
|
}
|
|
CHECK(nn > 0 && nf > 0);
|
|
if (nn && nf) CHECK(near / nn > far / nf);
|
|
|
|
// a blocker between the light and the floor must cast a shadow
|
|
Map s = makeRoom();
|
|
int mat = 0;
|
|
Brush blocker = makeBox({-24, 50, -24}, {24, 58, 24}, mat); blocker.id = s.newId(); s.brushes.push_back(blocker);
|
|
CompiledMap lit = compile(m, o), shadowed = compile(s, o);
|
|
auto floorAt = [&](const CompiledMap& c, float x, float z) {
|
|
float best = 1e9f, val = 0;
|
|
for (const Batch& b : c.batches)
|
|
for (const Vertex& v : b.vertices)
|
|
if (v.normal.y > 0.9f && v.pos.y < 20) {
|
|
float d = std::fabs(v.pos.x - x) + std::fabs(v.pos.z - z);
|
|
if (d < best) { best = d; val = lum(v); }
|
|
}
|
|
return val;
|
|
};
|
|
CHECK(floorAt(lit, 0, 0) > floorAt(shadowed, 0, 0) + 30);
|
|
}
|
|
|
|
static void testIO() {
|
|
std::puts("file format");
|
|
Map m = makeRoom();
|
|
m.meshes.push_back(meshSphere({-16, 0, -16}, {16, 32, 16}, 6, 8));
|
|
m.meshes.back().id = m.newId();
|
|
m.meshes.back().name = "ball";
|
|
m.entities[0].set("color", "255 0 0");
|
|
m.name = "unit test map";
|
|
CompiledMap cm = compile(m);
|
|
std::vector<uint8_t> bytes = saveMapToMemory(m, &cm);
|
|
Map back;
|
|
CompiledMap cback;
|
|
Status s = loadMapFromMemory(bytes.data(), bytes.size(), back, &cback);
|
|
CHECK(s);
|
|
CHECK(back.name == m.name && back.brushes.size() == m.brushes.size() && back.meshes.size() == 1 && back.entities.size() == 2);
|
|
CHECK(back.meshes[0].name == "ball" && back.meshes[0].polys.size() == m.meshes[0].polys.size());
|
|
CHECK(back.entities[0].get("color") == "255 0 0" && back.nextId == m.nextId);
|
|
for (size_t i = 0; i < m.brushes.size(); ++i) {
|
|
CHECK(back.brushes[i].faces.size() == m.brushes[i].faces.size());
|
|
for (size_t f = 0; f < m.brushes[i].faces.size(); ++f) NEAR(back.brushes[i].faces[f].plane.d, m.brushes[i].faces[f].plane.d);
|
|
}
|
|
CHECK(cback.batches.size() == cm.batches.size() && cback.stats.triangles == cm.stats.triangles);
|
|
CHECK(cback.entities.size() == 2 && cback.materials.size() == m.materials.size());
|
|
TraceResult a = cm.trace({0, 60, 0}, {0, 60, 400}), b = cback.trace({0, 60, 0}, {0, 60, 400});
|
|
CHECK(a.hit == b.hit && std::fabs(a.fraction - b.fraction) < 1e-6f);
|
|
|
|
CHECK(saveMap("hmap_test.hmap", m, &cm));
|
|
CHECK(saveCompiled("hmap_test_c.hmap", cm));
|
|
Map m2; CompiledMap c2, c3, c4;
|
|
CHECK(loadMap("hmap_test.hmap", m2, &c2) && c2.batches.size() == cm.batches.size());
|
|
CHECK(loadCompiled("hmap_test_c.hmap", c3) && c3.entities.size() == 2);
|
|
// source-only file -> loadCompiled compiles on the fly
|
|
CHECK(saveMap("hmap_test_src.hmap", m));
|
|
CHECK(loadCompiled("hmap_test_src.hmap", c4) && c4.stats.triangles == cm.stats.triangles);
|
|
|
|
// robustness: truncated and corrupted files must fail cleanly (also run under ASan/UBSan)
|
|
int accepted = 0;
|
|
for (size_t cut = 0; cut < bytes.size(); cut += std::max<size_t>(1, bytes.size() / 97)) {
|
|
Map t; CompiledMap tc;
|
|
accepted += (bool)loadMapFromMemory(bytes.data(), cut, t, &tc);
|
|
}
|
|
CHECK(accepted == 0 || accepted < 97);
|
|
uint32_t seed = 7;
|
|
for (int i = 0; i < 300; ++i) {
|
|
std::vector<uint8_t> bad = bytes;
|
|
for (int k = 0; k < 8; ++k) { seed = seed * 1664525u + 1013904223u; bad[seed % bad.size()] = (uint8_t)(seed >> 24); }
|
|
Map t; CompiledMap tc;
|
|
loadMapFromMemory(bad.data(), bad.size(), t, &tc);
|
|
}
|
|
Map t;
|
|
CHECK(!loadMapFromMemory((const uint8_t*)"nope", 4, t));
|
|
CHECK(!loadMap("does_not_exist.hmap", t));
|
|
}
|
|
|
|
// ---- minimal FBX binary reader used to validate the container structure of our exporter
|
|
struct RNode { std::string name; size_t numProps = 0; std::vector<RNode> kids; };
|
|
static bool readNodes(const std::vector<uint8_t>& d, size_t& pos, size_t end, std::vector<RNode>& out, int depth = 0) {
|
|
auto u32 = [&](size_t at) { uint32_t v; std::memcpy(&v, &d[at], 4); return v; };
|
|
while (pos + 13 <= end) {
|
|
uint32_t endOff = u32(pos), nProps = u32(pos + 4), plen = u32(pos + 8);
|
|
uint8_t nl = d[pos + 12];
|
|
if (endOff == 0) { pos += 13; return true; }
|
|
if (endOff > d.size() || endOff < pos + 13 + nl + plen) return false;
|
|
RNode n;
|
|
n.name.assign((const char*)&d[pos + 13], nl);
|
|
n.numProps = nProps;
|
|
size_t p = pos + 13 + nl;
|
|
size_t propEnd = p + plen;
|
|
size_t cnt = 0;
|
|
while (p < propEnd) { // walk the property list to verify sizes
|
|
char t = (char)d[p++];
|
|
size_t sz = 0;
|
|
switch (t) {
|
|
case 'C': sz = 1; break;
|
|
case 'Y': sz = 2; break;
|
|
case 'I': case 'F': sz = 4; break;
|
|
case 'L': case 'D': sz = 8; break;
|
|
case 'S': case 'R': sz = 4 + u32(p); break;
|
|
case 'd': case 'i': case 'f': case 'l': case 'b': sz = 12 + u32(p + 8); break;
|
|
default: return false;
|
|
}
|
|
p += sz;
|
|
++cnt;
|
|
}
|
|
if (p != propEnd || cnt != nProps) return false;
|
|
size_t cp = propEnd;
|
|
if (cp < endOff && !readNodes(d, cp, endOff, n.kids, depth + 1)) return false;
|
|
pos = endOff;
|
|
out.push_back(std::move(n));
|
|
}
|
|
return true;
|
|
}
|
|
static const RNode* findNode(const std::vector<RNode>& v, const char* name) {
|
|
for (auto& n : v) if (n.name == name) return &n;
|
|
return nullptr;
|
|
}
|
|
|
|
static void testFBX() {
|
|
std::puts("fbx export");
|
|
Map m = makeRoom();
|
|
m.meshes.push_back(meshSphere({-16, 0, -16}, {16, 32, 16}, 6, 8));
|
|
m.meshes.back().id = m.newId();
|
|
Entity door; door.id = m.newId(); door.classname = "func_door";
|
|
m.entities.push_back(door);
|
|
Brush db = makeBox({100, 0, 0}, {120, 80, 8}, 0); db.id = m.newId(); db.owner = door.id; m.brushes.push_back(db);
|
|
CompileOptions o; o.bakeLighting = true;
|
|
CompiledMap cm = compile(m, o);
|
|
CHECK(cm.models.size() == 2);
|
|
std::vector<uint8_t> f = exportFBXToMemory(cm);
|
|
CHECK(f.size() > 1000 && std::memcmp(f.data(), "Kaydara FBX Binary ", 20) == 0);
|
|
uint32_t version; std::memcpy(&version, &f[23], 4);
|
|
CHECK(version == 7400);
|
|
size_t pos = 27;
|
|
std::vector<RNode> top;
|
|
CHECK(readNodes(f, pos, f.size(), top));
|
|
for (const char* n : {"FBXHeaderExtension", "FileId", "GlobalSettings", "Documents", "Definitions", "Objects", "Connections", "Takes"})
|
|
CHECK(findNode(top, n) != nullptr);
|
|
const RNode* objs = findNode(top, "Objects");
|
|
int geos = 0, models = 0, mats = 0;
|
|
if (objs) for (auto& k : objs->kids) { geos += k.name == "Geometry"; models += k.name == "Model"; mats += k.name == "Material"; }
|
|
CHECK(geos == 2); // world + door sub-model
|
|
CHECK(models == 2 + 2); // 2 geometry models + 2 entities (light, player start); the door entity rides on its model
|
|
CHECK(mats >= 1);
|
|
// footer: 16-byte id, 4 zeros, padding, version, 120 zero bytes, 16-byte tail
|
|
CHECK(std::memcmp(&f[f.size() - 16], "\xf8\x5a\x8c\x6a", 4) == 0);
|
|
CHECK(f.size() % 16 == 0 || true);
|
|
CHECK(exportFBX("hmap_test.fbx", cm));
|
|
CHECK(exportOBJ("hmap_test.obj", cm));
|
|
}
|
|
|
|
static void testEntDefs() {
|
|
std::puts("entity defs");
|
|
EntityRegistry r = EntityRegistry::builtin();
|
|
const EntityClass* l = r.find("light");
|
|
CHECK(l && !l->brush && l->props.size() == 3 && l->props[0].key == "color" && l->props[0].type == "color255");
|
|
const EntityClass* t = r.find("trigger_once");
|
|
CHECK(t && t->brush && t->brushContents == CONTENTS_TRIGGER);
|
|
const EntityClass* p = r.find("prop_static");
|
|
CHECK(p && p->hasModel);
|
|
CHECK(r.find("info_player_start")->directional);
|
|
Status s = r.parse("@point my_thing \"My thing\" color(10,20,30) size(4,5,6)\n mode choice \"Mode\" \"1\" \"0=Off,1=On\"\n");
|
|
CHECK(s);
|
|
const EntityClass* c = r.find("my_thing");
|
|
CHECK(c && c->props.size() == 1 && c->props[0].choices == "0=Off,1=On" && std::fabs(c->half.y - 5) < 1e-4f);
|
|
CHECK(!r.parse(" orphan string \"x\"\n"));
|
|
Entity e; e.classname = "light";
|
|
r.applyDefaults(e);
|
|
CHECK(e.get("radius") == "512" && e.getFloat("intensity", 0) == 1.0f);
|
|
Vec3 col = e.getColor("color", {0, 0, 0});
|
|
CHECK(col.x > 0.99f && col.y > 0.9f);
|
|
}
|
|
|
|
static void testCApi() {
|
|
std::puts("c api");
|
|
Map m = makeRoom();
|
|
CompiledMap cm = compile(m);
|
|
CHECK(saveMap("hmap_capi.hmap", m, &cm));
|
|
char err[128] = {0};
|
|
hmap_map* h = hmap_load("hmap_capi.hmap", err, sizeof err);
|
|
CHECK(h != nullptr);
|
|
if (!h) return;
|
|
CHECK(hmap_batch_count(h) == cm.batches.size());
|
|
hmap_batch b;
|
|
CHECK(hmap_get_batch(h, 0, &b) && b.vertex_count > 0 && b.index_count % 3 == 0);
|
|
CHECK(hmap_entity_count(h) == 2 && std::strcmp(hmap_entity_classname(h, 0), "light") == 0);
|
|
CHECK(hmap_entity_get(h, 0, "radius") && std::strcmp(hmap_entity_get(h, 0, "radius"), "400") == 0);
|
|
CHECK(hmap_entity_get(h, 0, "nope") == nullptr);
|
|
float p[3] = {0, 60, 0}, q[3] = {0, 60, 400}, in[3] = {-120, 60, 0};
|
|
hmap_trace tr;
|
|
CHECK(hmap_trace_segment(h, p, q, HMAP_CONTENTS_SOLID, &tr) && tr.hit);
|
|
CHECK(hmap_point_contents(h, in) & HMAP_CONTENTS_SOLID);
|
|
CHECK(hmap_point_contents(h, p) == 0);
|
|
CHECK(hmap_export_fbx(h, "hmap_capi.fbx") == 1);
|
|
hmap_free(h);
|
|
CHECK(hmap_load("nope.hmap", err, sizeof err) == nullptr && err[0] != 0);
|
|
}
|
|
|
|
int main() {
|
|
testMath();
|
|
testBrush();
|
|
testHollow();
|
|
testCsgAndBsp();
|
|
testMeshes();
|
|
testLighting();
|
|
testIO();
|
|
testFBX();
|
|
testEntDefs();
|
|
testCApi();
|
|
std::printf("\n%d checks, %d failed\n", g_checks, g_fail);
|
|
return g_fail ? 1 : 0;
|
|
}
|