From f7fe623bb67c384539472f3fa9864405d2286036 Mon Sep 17 00:00:00 2001 From: Jonas Ornfelt Date: Thu, 29 Oct 2026 22:51:51 +0100 Subject: [PATCH] [Terrain] Keep ADT liquid out of points buried under its terrain TerrainInfo::getLiquidStatus took the highest liquid in the column under a point and only checked it against the floor the point stands on. The ADT liquid goes into that column whatever its height, so in a city or a cave below the surface the tile's water counted as water the player stands in, however far above the ceiling it was. In the Undercity Rogues' Quarter, at the rogue trainer (1418, 58, -62), the column holds Tirisfal's ADT water at 32.93 -- under its own terrain at 34.14 and about 95 yards above the floor. Every player there gets a breath timer and drowns once it runs out (60s, four times that for Undead). The GridMap::getLiquidStatus this replaced judged ADT liquid against the ADT terrain at the point: no liquid when the level is below that terrain, or the point is more than 2 yards under it. Carry the terrain height with the ADT liquid (LiquidInfo / Surface terrainZ; none over a hole or for a model's liquid, as INVALID_HEIGHT made it before) and select with Column::LiquidFor(z), which skips an ADT liquid that does not reach the point. getLiquidStatus and mangos-height-check use it; GetTerrainType, which has no z, keeps HighestLiquid. --- src/game/WorldHandlers/GridMap.cpp | 2 +- src/shared/terrain/Column.hpp | 34 ++++++++++++ src/shared/terrain/FusedTerrain.cpp | 3 ++ src/shared/terrain/Terrain.hpp | 3 ++ src/tests/TerrainModelTest.cpp | 75 ++++++++++++++++++++++++++ src/tools/height-check/HeightCheck.cpp | 2 +- 6 files changed, 117 insertions(+), 2 deletions(-) diff --git a/src/game/WorldHandlers/GridMap.cpp b/src/game/WorldHandlers/GridMap.cpp index 73a1d8337..b335527ac 100644 --- a/src/game/WorldHandlers/GridMap.cpp +++ b/src/game/WorldHandlers/GridMap.cpp @@ -352,7 +352,7 @@ GridMapLiquidStatus TerrainInfo::getLiquidStatus(float x, float y, float z, const world::terrain::Column column = ColumnAt(x, y, z + FLOOR_BURIED_LIFT, z - FLOOR_SEARCH_DOWN); - const auto liquid = column.HighestLiquid(); + const auto liquid = column.LiquidFor(z); if (!liquid || !liquid->liquidEntry) { return LIQUID_MAP_NO_WATER; diff --git a/src/shared/terrain/Column.hpp b/src/shared/terrain/Column.hpp index 3f9abd330..fea6deb84 100644 --- a/src/shared/terrain/Column.hpp +++ b/src/shared/terrain/Column.hpp @@ -29,6 +29,7 @@ namespace world::terrain LiquidKind liquid = LiquidKind::None; uint16_t liquidEntry = 0; bool deep = false; + std::optional terrainZ; ///< see LiquidInfo::terrainZ bool Solid() const { return kind != SurfaceKind::Liquid; } @@ -39,10 +40,15 @@ namespace world::terrain info.kind = liquid; info.entry = liquidEntry; info.deep = deep; + info.terrainZ = terrainZ; return info; } }; + /// How far under its ADT terrain a point may be and still be in that tile's + /// liquid. + constexpr float ADT_LIQUID_BURIED_LIMIT = 2.0f; + class Column { public: @@ -62,6 +68,7 @@ namespace world::terrain s.liquid = info.kind; s.liquidEntry = info.entry; s.deep = info.deep; + s.terrainZ = info.terrainZ; m_surfaces.push_back(s); } @@ -131,6 +138,33 @@ namespace world::terrain return best; } + /// The liquid a point at z is in, under or over: the highest one + /// that reaches it. An ADT liquid does not reach a point when it + /// lies under its own terrain, or the point is more than + /// ADT_LIQUID_BURIED_LIMIT under that terrain -- a cave, or a city + /// built below the surface (the Undercity under Tirisfal) -- as the + /// reference GridMap::getLiquidStatus had it. + std::optional LiquidFor(float z) const + { + std::optional best; + for (const Surface& s : m_surfaces) + { + if (s.kind != SurfaceKind::Liquid || + (best && s.z <= best->z)) + { + continue; + } + if (s.terrainZ && + (s.z < *s.terrainZ || + z < *s.terrainZ - ADT_LIQUID_BURIED_LIMIT)) + { + continue; + } + best = s; + } + return best; + } + private: std::vector m_surfaces; }; diff --git a/src/shared/terrain/FusedTerrain.cpp b/src/shared/terrain/FusedTerrain.cpp index 437334d75..7db4fe313 100644 --- a/src/shared/terrain/FusedTerrain.cpp +++ b/src/shared/terrain/FusedTerrain.cpp @@ -345,6 +345,9 @@ namespace world::terrain { if (auto adt = tile->LiquidAt(x, y)) { + // The terrain it lies on: a point buried under that terrain is + // not in it (Column::LiquidFor), however far below the floor. + adt->terrainZ = tile->TerrainHeight(x, y); column.AddLiquid(*adt); } } diff --git a/src/shared/terrain/Terrain.hpp b/src/shared/terrain/Terrain.hpp index 7ebd27964..92a3874c4 100644 --- a/src/shared/terrain/Terrain.hpp +++ b/src/shared/terrain/Terrain.hpp @@ -39,6 +39,9 @@ namespace world::terrain LiquidKind kind = LiquidKind::None; uint16_t entry = 0; ///< LiquidType.dbc row id (0 = unknown) bool deep = false; ///< drives swim fatigue ("dark water") + /// The ADT terrain under an ADT liquid; none for a model's liquid or + /// over a hole. + std::optional terrainZ; }; struct StaticInstance diff --git a/src/tests/TerrainModelTest.cpp b/src/tests/TerrainModelTest.cpp index bc82432fd..4a43738ae 100644 --- a/src/tests/TerrainModelTest.cpp +++ b/src/tests/TerrainModelTest.cpp @@ -26,6 +26,7 @@ #include "TestHarness.h" #include "terrain/CollisionModel.hpp" +#include "terrain/Column.hpp" #include "terrain/Terrain.hpp" #include "terrain/WmoModel.hpp" @@ -336,3 +337,77 @@ TEST(WmoAreaInfoNamesTheGroupTheRayHit) REQUIRE(below.has_value()); CHECK_EQ(below->groupId, uint32_t(111)); } + +namespace +{ + LiquidInfo AdtWater(float level, std::optional terrainZ) + { + LiquidInfo info; + info.level = level; + info.kind = LiquidKind::Water; + info.entry = 1; + info.terrainZ = terrainZ; + return info; + } +} + +TEST(AdtLiquidDoesNotReachAPointFarUnderItsTerrain) +{ + // The Undercity, Miles Dexter: Tirisfal's water at 32.93, under its own + // terrain at 34.14, the city's floor 95 yards down. The reference GridMap + // left it dry. + Column c; + c.AddSolid(-62.28f, SurfaceKind::Static); + c.AddLiquid(AdtWater(32.93f, 34.14f)); + CHECK(!c.LiquidFor(-62.2f).has_value()); + // still there for a caller that asks for any liquid + CHECK(c.HighestLiquid().has_value()); +} + +TEST(AdtLiquidReachesAPointAboveOrJustUnderItsTerrain) +{ + Column c; + c.AddSolid(10.f, SurfaceKind::Terrain); + c.AddLiquid(AdtWater(15.f, 10.f)); + const auto swimming = c.LiquidFor(12.f); + REQUIRE(swimming.has_value()); + CHECK_EQ(swimming->z, 15.f); + CHECK(c.LiquidFor(10.f - ADT_LIQUID_BURIED_LIMIT + 0.5f).has_value()); + CHECK(!c.LiquidFor(10.f - ADT_LIQUID_BURIED_LIMIT - 0.5f).has_value()); +} + +TEST(AdtLiquidUnderItsOwnTerrainIsNoLiquid) +{ + Column c; + c.AddLiquid(AdtWater(32.93f, 34.14f)); + CHECK(!c.LiquidFor(40.f).has_value()); +} + +TEST(AdtLiquidOverAHoleAndModelLiquidAreNotFiltered) +{ + // No terrain under it (a hole cut for a WMO entrance) or a WMO's own + // liquid: the floor checks of the caller decide, as before. + Column c; + c.AddLiquid(AdtWater(32.93f, std::nullopt)); + LiquidInfo wmo = AdtWater(-60.f, std::nullopt); + wmo.kind = LiquidKind::Slime; + c.AddLiquid(wmo); + const auto hole = c.LiquidFor(-62.2f); + REQUIRE(hole.has_value()); + CHECK_EQ(hole->z, 32.93f); +} + +TEST(BuriedAdtLiquidLeavesAModelLiquidBelowIt) +{ + // The canal of a city under a lake: the lake is out, the canal is the + // answer. + Column c; + c.AddLiquid(AdtWater(32.93f, 30.f)); + LiquidInfo canal = AdtWater(-63.f, std::nullopt); + canal.kind = LiquidKind::Slime; + c.AddLiquid(canal); + const auto liquid = c.LiquidFor(-64.f); + REQUIRE(liquid.has_value()); + CHECK_EQ(liquid->z, -63.f); + CHECK(liquid->liquid == LiquidKind::Slime); +} diff --git a/src/tools/height-check/HeightCheck.cpp b/src/tools/height-check/HeightCheck.cpp index 08973c505..eb5fb6b76 100644 --- a/src/tools/height-check/HeightCheck.cpp +++ b/src/tools/height-check/HeightCheck.cpp @@ -224,7 +224,7 @@ int main(int argc, char** argv) if (verbose) { - const auto liquid = column.HighestLiquid(); + const auto liquid = column.LiquidFor(p.z); char liq[80] = "none"; if (liquid) {