Skip to content

Commit 5c0a270

Browse files
henderkesclaude
andcommitted
test(terrain): add drapeverify to prove pruned SurfaceZ matches all-planes query
TerrainDrape::DrapeCompare returns, for a point, the old all-planes-highest altitude, the new SurfaceZ, and a pruning-isolating value (game plane-0 + only the trapezoid-containing non-zero planes) so the plane selection can be checked apart from the native-vs-game plane-0 sampling difference. Harness verb `drapeverify [n radius]` runs it over N random points and reports plane-selection mismatches plus the total new-vs-old delta. Result over 1500 points on map 474 (42 planes): 0 plane-selection mismatches (max 0.00), including 38 points whose highest surface came from a non-zero bridge/prop plane -- all selected identically to the all-planes query. The only residual is the plane-0 method: native point interpolation vs the client's radius-5 disk-max, avg ~2-7 gwinches, with rare outliers up to ~128 confined to near-vertical cliffs (verified: neighbor samples +-8gw span 200+ gwinches). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013qkJXMrytQ75KYP4o2yZgX
1 parent 0abec18 commit 5c0a270

3 files changed

Lines changed: 114 additions & 0 deletions

File tree

GWToolboxdll/Modules/TestHarness.cpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,78 @@ namespace {
563563
write_status(b);
564564
return;
565565
}
566+
if (verb == "drapeverify") { // drapeverify [n radius]: does new SurfaceZ match the old all-planes-highest-point query?
567+
uint32_t n = 1500;
568+
float radius = 1500.f;
569+
is >> n >> radius;
570+
n = std::clamp(n, 100u, 100000u);
571+
radius = std::clamp(radius, 100.f, 5000.f);
572+
const auto self = GW::Agents::GetControlledCharacter();
573+
const auto* pm = GW::Map::GetPathingMap();
574+
if (!self || !pm || !pm->size()) {
575+
write_status("drapeverify: no character or pathing map");
576+
return;
577+
}
578+
const auto n_planes = static_cast<uint32_t>(pm->size());
579+
uint32_t seed = 0x1234567u;
580+
const auto next01 = [&seed] {
581+
seed = seed * 1664525u + 1013904223u;
582+
return (seed >> 8) * (1.f / 16777216.f);
583+
};
584+
Log::Log("[drapeverify] map=%d n=%u planes=%u radius=%.0f", static_cast<int>(GW::Map::GetMapID()), n, n_planes, radius);
585+
uint32_t both = 0; // points with data on both old and new
586+
uint32_t prune_mismatch = 0; // points where pruned selection != all-planes selection (the real test)
587+
uint32_t nonzero_hits = 0; // points whose highest surface came from a non-zero plane
588+
uint32_t logged = 0;
589+
float max_prune = 0.f, max_total = 0.f;
590+
double sum_total = 0.0;
591+
for (uint32_t i = 0; i < n; ++i) {
592+
const float ang = next01() * 6.2831853f, r = radius * std::sqrt(next01());
593+
const float x = self->pos.x + std::cos(ang) * r, y = self->pos.y + std::sin(ang) * r;
594+
float old_all = 0.f, new_z = 0.f, prune = 0.f;
595+
TerrainDrape::DrapeCompare(x, y, n_planes, &old_all, &new_z, &prune);
596+
597+
// Pruning correctness: prune uses the SAME game values as old_all, differing only in which
598+
// planes are considered. Any gap here is a plane the trapezoid pruning wrongly dropped/added.
599+
const float dp = std::fabs(old_all - prune);
600+
if (dp > max_prune) max_prune = dp;
601+
if (dp > 0.5f) {
602+
++prune_mismatch;
603+
if (logged++ < 15)
604+
Log::Log("[drapeverify] PRUNE MISMATCH (%.0f,%.0f) old_all=%.1f prune=%.1f new=%.1f d=%.1f",
605+
x, y, old_all, prune, new_z, dp);
606+
}
607+
// Did the highest surface come from a non-zero plane here? (i.e. was a bridge/prop plane picked)
608+
if (old_all != 0.f) {
609+
GW::GamePos g0{x, y, 0};
610+
if (std::fabs(GW::Map::QueryAltitude(&g0) - old_all) > 0.5f) ++nonzero_hits;
611+
}
612+
// Total new-vs-old (includes native point vs game radius-5 on plane 0).
613+
if (old_all != 0.f && new_z != 0.f) {
614+
++both;
615+
const float dt = std::fabs(old_all - new_z);
616+
sum_total += dt;
617+
if (dt > max_total) max_total = dt;
618+
if (dt > 25.f && logged < 25) {
619+
++logged;
620+
// Sample the game terrain in a tiny cross to reveal a cliff (why radius-5 disk-max != point).
621+
GW::GamePos c(x, y, 0), e(x + 8.f, y, 0), w(x - 8.f, y, 0), nq(x, y + 8.f, 0), s(x, y - 8.f, 0);
622+
Log::Log("[drapeverify] BIG DELTA (%.0f,%.0f) old=%.1f new=%.1f d=%.1f | game@center=%.1f nbrs(+-8gw)=[%.0f %.0f %.0f %.0f]",
623+
x, y, old_all, new_z, dt, GW::Map::QueryAltitude(&c),
624+
GW::Map::QueryAltitude(&e), GW::Map::QueryAltitude(&w),
625+
GW::Map::QueryAltitude(&nq), GW::Map::QueryAltitude(&s));
626+
}
627+
}
628+
}
629+
char b[240];
630+
snprintf(b, sizeof(b),
631+
"drapeverify: n=%u both=%u nonzero_plane_hits=%u | PRUNE mismatches=%u max=%.2f | total new-vs-old avg=%.2f max=%.2f",
632+
n, both, nonzero_hits, prune_mismatch, max_prune, both ? sum_total / both : 0.0, max_total);
633+
Log::Log("[harness] %s", b);
634+
Log::FlushFile();
635+
write_status(b);
636+
return;
637+
}
566638
if (verb == "drapebench") { // drapebench [n radius]: A/B the old all-planes QueryAltitude loop vs the new pruned SurfaceZ
567639
uint32_t n = 2000;
568640
float radius = 1200.f;

GWToolboxdll/Utils/TerrainDrape.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,37 @@ uint32_t TerrainDrape::PathingPlaneCount()
208208
const GW::PathingMapArray* pm = GW::Map::GetPathingMap();
209209
return pm ? static_cast<uint32_t>(pm->size()) : 0;
210210
}
211+
212+
void TerrainDrape::DrapeCompare(const float x, const float y, const uint32_t n_planes,
213+
float* old_all, float* new_z, float* prune_gp0)
214+
{
215+
// OLD behavior: QueryAltitude on every plane, keep the highest surface (min z); 0 = no data.
216+
if (old_all) {
217+
float best = 0.f;
218+
for (uint32_t zp = 0; zp < n_planes; ++zp) {
219+
GW::GamePos p{x, y, zp};
220+
const float a = GW::Map::QueryAltitude(&p);
221+
if (a != 0.f && (best == 0.f || a < best)) best = a;
222+
}
223+
*old_all = best;
224+
}
225+
226+
if (new_z) *new_z = SurfaceZ(x, y, 0, n_planes);
227+
228+
// Pruning isolated: game's plane-0 value + only the trapezoid-containing non-zero planes.
229+
if (prune_gp0) {
230+
GW::GamePos p0{x, y, 0};
231+
float best = GW::Map::QueryAltitude(&p0); // raw game plane-0, identical to old_all's zp==0 step
232+
const GW::PathingMapArray* pm = GW::Map::GetPathingMap();
233+
if (pm) {
234+
const auto planes = std::min<uint32_t>(n_planes, static_cast<uint32_t>(pm->size()));
235+
for (uint32_t zp = 1; zp < planes; ++zp) {
236+
if (!PlaneContains((*pm)[zp], x, y)) continue;
237+
GW::GamePos p{x, y, zp};
238+
const float a = GW::Map::QueryAltitude(&p);
239+
if (a != 0.f && (best == 0.f || a < best)) best = a;
240+
}
241+
}
242+
*prune_gp0 = best;
243+
}
244+
}

GWToolboxdll/Utils/TerrainDrape.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,12 @@ namespace TerrainDrape {
2525

2626
// Plane count of the current pathing map; 0 while the map is still loading.
2727
uint32_t PathingPlaneCount();
28+
29+
// Verification helper (harness `drapeverify`). Fills three altitudes at (x,y):
30+
// old_all -- the previous behavior: GW::Map::QueryAltitude on EVERY plane, highest surface (min z)
31+
// new_z -- the current SurfaceZ (native plane-0 read + trapezoid-pruned non-zero planes)
32+
// prune_gp0 -- the pruned non-zero planes combined with the GAME's plane-0 value, so the trapezoid
33+
// pruning can be checked in isolation from the native-vs-game plane-0 sampling delta.
34+
// Any nullptr out-pointer is skipped. Render thread only (uses QueryAltitude).
35+
void DrapeCompare(float x, float y, uint32_t n_planes, float* old_all, float* new_z, float* prune_gp0);
2836
}

0 commit comments

Comments
 (0)