Index: source/simulation2/components/CCmpFootprint.cpp =================================================================== --- source/simulation2/components/CCmpFootprint.cpp +++ source/simulation2/components/CCmpFootprint.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -135,6 +135,11 @@ Init(paramNode); } + virtual entity_pos_t GetHeight() const + { + return m_Height; + } + virtual void GetShape(EShape& shape, entity_pos_t& size0, entity_pos_t& size1, entity_pos_t& height) const { shape = m_Shape; Index: source/simulation2/components/CCmpRangeManager.cpp =================================================================== --- source/simulation2/components/CCmpRangeManager.cpp +++ source/simulation2/components/CCmpRangeManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -17,6 +17,8 @@ #include "precompiled.h" +#include + #include "simulation2/system/Component.h" #include "ICmpRangeManager.h" @@ -24,6 +26,7 @@ #include "simulation2/system/EntityMap.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpFogging.h" +#include "simulation2/components/ICmpFootprint.h" #include "simulation2/components/ICmpMirage.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" @@ -43,7 +46,9 @@ #include "renderer/Scene.h" #define LOS_TILES_RATIO 8 -#define DEBUG_RANGE_MANAGER_BOUNDS 0 +// Enables runtime tests for vision blocking computation +// Program will trigger exception when finds problem +#define DEBUG_VIS_COMP 0 /** * Representation of a range query. @@ -213,15 +218,19 @@ { EntityData() : visibilities(0), size(0), visionSharing(0), - owner(-1), flags(FlagMasks::Normal) + owner(-1), height(0), flags(FlagMasks::Normal) { } entity_pos_t x, z; entity_pos_t visionRange; - u32 visibilities; // 2-bit visibility, per player + u16 height; + // 2-bit visibility, per player + u32 visibilities; u32 size; - u16 visionSharing; // 1-bit per player + // 1-bit per player + u16 visionSharing; i8 owner; - u8 flags; // See the FlagMasks enum + // See the FlagMasks enum + u8 flags; template inline bool HasFlag() const { return (flags & mask) != 0; } @@ -232,7 +241,7 @@ inline void SetFlag(u8 mask, bool val) { flags = val ? (flags | mask) : (flags & ~mask); } }; -cassert(sizeof(EntityData) == 24); +cassert(sizeof(EntityData) == 28); /** * Serialization helper template for Query @@ -326,7 +335,7 @@ * queries. * * LOS implementation is based on the model described in GPG2. - * (TODO: would be nice to make it cleverer, so e.g. mountains and walls + * (TODO: would be nice to make it cleverer, so e.g. walls * can block vision) */ class CCmpRangeManager : public ICmpRangeManager @@ -393,6 +402,25 @@ // Lazily constructed when it's needed, to save memory in smaller games. std::vector > m_LosPlayerCounts; + // Large amount of calls so cache it + CTerrain* m_Terrain; + // Map used by all players and vertexes with the same structure as m_LosState + // Used in los update to determine if given tile was visible + // No need to serialise + boost::dynamic_bitset<> m_Vismap; + // Map used by all players and vertexes with the same structure as m_LosState + // Used in los update to determine if given tile blocked vision + // No need to serialise + boost::dynamic_bitset<> m_IsBlockingVision; + // Used in los update to know if in current computation was + // vision blocked + bool m_VisionBlocked; + +#if DEBUG_VIS_COMP + // Holds if tile was checked by los update + boost::dynamic_bitset<> m_VisComputedBefore; +#endif + // 2-bit ELosState per player, starting with player 1 (not 0!) up to player MAX_LOS_PLAYER_ID (inclusive) std::vector m_LosState; @@ -442,6 +470,8 @@ m_LosCircular = false; m_TerrainVerticesPerSide = 0; + + m_Terrain = nullptr; } virtual void Deinit() @@ -538,6 +568,11 @@ if (cmpObstruction) entdata.size = cmpObstruction->GetSize().ToInt_RoundToInfinity(); + // Store height + CmpPtr cmpFootprint(GetSimContext(), ent); + if (cmpFootprint) + entdata.height = cmpFootprint->GetHeight().ToInt_RoundToNearest(); + // Remember this entity m_EntityData.insert(ent, entdata); break; @@ -561,9 +596,9 @@ CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Move(ent, from, to, it->second.size); if (it->second.HasFlag()) - SharingLosMove(it->second.visionSharing, it->second.visionRange, from, to); + SharingLosMove(it->second.visionSharing, it->second.visionRange, from, to, it->second.height); else - LosMove(it->second.owner, it->second.visionRange, from, to); + LosMove(it->second.owner, it->second.visionRange, from, to, it->second.height); i32 oldLosTile = PosToLosTilesHelper(it->second.x, it->second.z); i32 newLosTile = PosToLosTilesHelper(msgData.x, msgData.z); if (oldLosTile != newLosTile) @@ -577,9 +612,9 @@ CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Add(ent, to, it->second.size); if (it->second.HasFlag()) - SharingLosAdd(it->second.visionSharing, it->second.visionRange, to); + SharingLosAdd(it->second.visionSharing, it->second.visionRange, to, it->second.height); else - LosAdd(it->second.owner, it->second.visionRange, to); + LosAdd(it->second.owner, it->second.visionRange, to, it->second.height); AddToTile(PosToLosTilesHelper(msgData.x, msgData.z), ent); } @@ -594,9 +629,9 @@ CFixedVector2D from(it->second.x, it->second.z); m_Subdivision.Remove(ent, from, it->second.size); if (it->second.HasFlag()) - SharingLosRemove(it->second.visionSharing, it->second.visionRange, from); + SharingLosRemove(it->second.visionSharing, it->second.visionRange, from, it->second.height); else - LosRemove(it->second.owner, it->second.visionRange, from); + LosRemove(it->second.owner, it->second.visionRange, from, it->second.height); RemoveFromTile(PosToLosTilesHelper(it->second.x, it->second.z), ent); } @@ -627,14 +662,14 @@ if (!it->second.HasFlag()) { CFixedVector2D pos(it->second.x, it->second.z); - LosRemove(it->second.owner, it->second.visionRange, pos); - LosAdd(msgData.to, it->second.visionRange, pos); + LosRemove(it->second.owner, it->second.visionRange, pos, it->second.height); + LosAdd(msgData.to, it->second.visionRange, pos, it->second.height); } if (it->second.HasFlag()) { - RevealShore(it->second.owner, false); - RevealShore(msgData.to, true); + RevealShore(it->second.owner, false, it->second.height); + RevealShore(msgData.to, true, it->second.height); } } @@ -696,13 +731,13 @@ CFixedVector2D pos(it->second.x, it->second.z); if (it->second.HasFlag()) { - SharingLosRemove(it->second.visionSharing, oldRange, pos); - SharingLosAdd(it->second.visionSharing, newRange, pos); + SharingLosRemove(it->second.visionSharing, oldRange, pos, it->second.height); + SharingLosAdd(it->second.visionSharing, newRange, pos, it->second.height); } else { - LosRemove(it->second.owner, oldRange, pos); - LosAdd(it->second.owner, newRange, pos); + LosRemove(it->second.owner, oldRange, pos, it->second.height); + LosAdd(it->second.owner, newRange, pos, it->second.height); } } @@ -736,9 +771,9 @@ entity_pos_t range = it->second.visionRange; CFixedVector2D pos(it->second.x, it->second.z); if (msgData.add) - LosAdd(msgData.player, range, pos); + LosAdd(msgData.player, range, pos, it->second.height); else - LosRemove(msgData.player, range, pos); + LosRemove(msgData.player, range, pos, it->second.height); } if (msgData.add) @@ -771,6 +806,9 @@ m_WorldZ1 = z1; m_TerrainVerticesPerSide = (i32)vertices; + CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY); + if (cmpTerrain) + m_Terrain = cmpTerrain->GetCTerrain(); ResetDerivedData(); } @@ -835,6 +873,13 @@ m_LosPlayerCounts.resize(MAX_LOS_PLAYER_ID+1); m_ExploredVertices.clear(); m_ExploredVertices.resize(MAX_LOS_PLAYER_ID+1, 0); + + // Just clear them so they are resized before computations + m_Vismap.clear(); + ENSURE(m_Vismap.empty()); + m_IsBlockingVision.clear(); + ENSURE(m_IsBlockingVision.empty()); + if (m_Deserializing) { // recalc current exploration stats. @@ -866,13 +911,13 @@ if (it->second.HasFlag()) { if (it->second.HasFlag()) - SharingLosAdd(it->second.visionSharing, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z)); + SharingLosAdd(it->second.visionSharing, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z), it->second.height); else - LosAdd(it->second.owner, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z)); + LosAdd(it->second.owner, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z), it->second.height); AddToTile(PosToLosTilesHelper(it->second.x, it->second.z), it->first); if (it->second.HasFlag()) - RevealShore(it->second.owner, true); + RevealShore(it->second.owner, true, it->second.height); } m_TotalInworldVertices = 0; @@ -1189,7 +1234,7 @@ // elevationBonus is part of the 3D position, as the source is really that much heigher CmpPtr cmpSourcePosition(q.source); CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+ - CFixedVector3D(entity_pos_t::Zero(), q.elevationBonus, entity_pos_t::Zero()) ; + CFixedVector3D(entity_pos_t::Zero(), q.elevationBonus, entity_pos_t::Zero()) ; // Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange m_SubdivisionResults.clear(); m_Subdivision.GetNear(m_SubdivisionResults, pos, q.maxRange * 2); @@ -1583,6 +1628,13 @@ it->second.SetFlag(status); } + virtual bool TestVisibility(int i, int j, player_id_t player, ELosState state) const + { + if (LosIsOffWorld(i, j) || !(i >= 0 && j >= 0 && i < m_TerrainVerticesPerSide && j < m_TerrainVerticesPerSide)) + return false; + return m_LosState[j*m_TerrainVerticesPerSide + i] & (state << (2 * (player - 1))); + } + ELosVisibility ComputeLosVisibility(CEntityHandle ent, player_id_t player) const { // Entities not with positions in the world are never visible @@ -1591,15 +1643,13 @@ CmpPtr cmpPosition(ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return VIS_HIDDEN; - // Mirage entities, whatever the situation, are visible for one specific player CmpPtr cmpMirage(ent); if (cmpMirage && cmpMirage->GetPlayer() != player) return VIS_HIDDEN; - CFixedVector2D pos = cmpPosition->GetPosition2D(); - int i = (pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); - int j = (pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); + int i = (pos.X / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToNearest(); + int j = (pos.Y / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToNearest(); // Reveal flag makes all positioned entities visible and all mirages useless if (GetLosRevealAll(player)) @@ -1609,7 +1659,6 @@ else return VIS_VISIBLE; } - // Get visible regions CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); @@ -1627,7 +1676,6 @@ if (cmpVisibility && cmpVisibility->IsActivated()) return cmpVisibility->GetVisibility(player, los.IsVisible(i, j), los.IsExplored(i, j)); } - // Else, default behavior if (los.IsVisible(i, j)) @@ -1637,7 +1685,6 @@ return VIS_VISIBLE; } - if (!los.IsExplored(i, j)) return VIS_HIDDEN; @@ -1690,32 +1737,25 @@ virtual ELosVisibility GetLosVisibility(CEntityHandle ent, player_id_t player) const { entity_id_t entId = ent.GetId(); - // Entities not with positions in the world are never visible if (entId == INVALID_ENTITY) return VIS_HIDDEN; - CmpPtr cmpPosition(ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return VIS_HIDDEN; - // Gaia and observers do not have a visibility cache if (player <= 0) return ComputeLosVisibility(ent, player); - CFixedVector2D pos = cmpPosition->GetPosition2D(); i32 n = PosToLosTilesHelper(pos.X, pos.Y); if (IsVisibilityDirty(m_DirtyVisibility[n], player)) return ComputeLosVisibility(ent, player); - if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), entId) != m_ModifiedEntities.end()) return ComputeLosVisibility(ent, player); - EntityMap::const_iterator it = m_EntityData.find(entId); if (it == m_EntityData.end()) return ComputeLosVisibility(ent, player); - return static_cast(GetPlayerVisibility(it->second.visibilities, player)); } @@ -1983,7 +2023,7 @@ } } - virtual void RevealShore(player_id_t p, bool enable) + virtual void RevealShore(player_id_t p, bool enable, u16 h) { if (p <= 0 || p > MAX_LOS_PLAYER_ID) return; @@ -2007,10 +2047,7 @@ continue; // Maybe we could be more clever and don't add dummy strips of one tile - if (enable) - LosAddStripHelper(p, i, i, j, countsData); - else - LosRemoveStripHelper(p, i, i, j, countsData); + LosManageStripHelper(enable, p, i, i, j, countsData, h, i, j); } } @@ -2033,75 +2070,417 @@ ssize_t r = m_TerrainVerticesPerSide/2 - edgeSize + 1; // subtract a bit from the radius to ensure nice // SoD blurring around the edges of the map - return (dist2 >= r*r); } - else - { - // With a square map, the outermost edge of the map should be off-world, - // so the SoD texture blends out nicely + // With a square map, the outermost edge of the map should be off-world, + // so the SoD texture blends out nicely + return (i < edgeSize || j < edgeSize || i >= m_TerrainVerticesPerSide - edgeSize || j >= m_TerrainVerticesPerSide - edgeSize); + } - return (i < edgeSize || j < edgeSize || i >= m_TerrainVerticesPerSide-edgeSize || j >= m_TerrainVerticesPerSide-edgeSize); - } + /** + * Returns true iff both tiles (a1, b1), (a2, b2) are visible from source + * Using visibility map + */ + inline bool CheckTilesForVisibility(i32 a1, i32 b1, i32 a2, i32 b2) + { + #if DEBUG_VIS_COMP + ENSURE(b1 > -1); + ENSURE(a1 > -1); + ENSURE(b2 > -1); + ENSURE(a2 > -1); + #endif + i32 idx = (b1)*m_TerrainVerticesPerSide + (a1); + i32 idx2 = (b2)*m_TerrainVerticesPerSide + (a2); + #if DEBUG_VIS_COMP + ENSURE(m_VisComputedBefore[idx]); + ENSURE(m_VisComputedBefore[idx2]); + #endif + return m_Vismap[idx] && m_Vismap[idx2]; } /** - * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). + * Check if at least one of tiles (a1, b1), (a2, b2) + * is was higher then source + * Using blocking vision map + */ + inline bool AreTilesHigher(i32 a1, i32 b1, i32 a2, i32 b2) + { + i32 idx = (b1)*m_TerrainVerticesPerSide + (a1); + i32 idx2 = (b2)*m_TerrainVerticesPerSide + (a2); + return m_IsBlockingVision[idx] || m_IsBlockingVision[idx2]; + } + +#if DEBUG_VIS_COMP + #define ensurecomputedbefore(x) \ + {\ + ENSURE(m_VisComputedBefore[idx]);\ + } + #else + #define ensurecomputedbefore(x) + #endif + /** + * Returns true if tile (i, j) is visible from given starting tile (iFrom, jFrom) + * Every tile above given height (hFrom) will block vision + * This is using global visibility map and blocking vision map + * so correct computation requires to check all tiles closer to source first + * Source tile is allways marked as visible and has to be checked + * This is using m_VisionBlocked to know if there was + * blocking of vision in previous computations + * Has to be called from single thread + * @param {idxx} position of (i, j) in visibility map */ - inline void LosAddStripHelper(u8 owner, i32 i0, i32 i1, i32 j, u16* counts) + inline bool IsVisibleTile(i32 iFrom, i32 jFrom, float hFrom, i32 i, i32 j, i32 idxx) { - if (i1 < i0) - return; + // Zero: The same tile is allways visible + if (iFrom == i && jFrom == j) { + m_IsBlockingVision[idxx] = false; + return true; + } - i32 idx0 = j*m_TerrainVerticesPerSide + i0; - i32 idx1 = j*m_TerrainVerticesPerSide + i1; - u32 &explored = m_ExploredVertices.at(owner); - for (i32 idx = idx0; idx <= idx1; ++idx) - { - // Increasing from zero to non-zero - move from unexplored/explored to visible+explored - if (counts[idx] == 0) - { - i32 i = i0 + idx - idx0; - if (!LosIsOffWorld(i, j)) - { - explored += !(m_LosState[idx] & (LOS_EXPLORED << (2*(owner-1)))); - m_LosState[idx] |= ((LOS_VISIBLE | LOS_EXPLORED) << (2*(owner-1))); - } + // First: Check if we see it by high difference + float h2 = m_Terrain->GetVertexGroundLevel(i, j); + m_IsBlockingVision[idxx] = hFrom < h2; + if (m_IsBlockingVision[idxx]) + m_VisionBlocked = true; - MarkVisibilityDirtyAroundTile(owner, i, j); + // Vision was not blocked yet so abort + if (!m_VisionBlocked) + return true; + + float h3 = 0; + // Check distances + i32 iDistA = std::abs(iFrom - i); + i32 jDistA = std::abs(jFrom - j); + i32 idx; + + #if DEBUG_VIS_COMP + ENSURE(i > 0); + ENSURE(j > 0); + ENSURE(iFrom > 0); + ENSURE(jFrom > 0); + ENSURE(i < m_TerrainVerticesPerSide); + ENSURE(j < m_TerrainVerticesPerSide); + ENSURE(iFrom < m_TerrainVerticesPerSide); + ENSURE(jFrom < m_TerrainVerticesPerSide); + #endif + + // Second: Check if tile before is visible + // 1) straight vision - check tiles at line (and return right away) + // 1.1) check on i line + if (i == iFrom) { + // 1.1.a) we are down so check to up + if (j < jFrom) { + idx = (j + 1)*m_TerrainVerticesPerSide + i; + ensurecomputedbefore(idx); + if (!m_Vismap[idx]) + return false; + // tile before is visible + if (!m_IsBlockingVision[idx]) + return true; + // tile before is blocking vision + h3 = m_Terrain->GetVertexGroundLevel(i, j + 1); + return h2 - h3 > 1; + } + // 1.1.b) we are up so check to down + if (j > jFrom) { + idx = (j - 1)*m_TerrainVerticesPerSide + i; + ensurecomputedbefore(idx); + if (!m_Vismap[idx]) + return false; + if (!m_IsBlockingVision[idx]) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i, j - 1); + return h2 - h3 > 1; + } + } + // 1.2) check on j line + else if (j == jFrom) { + // 1.2.a) we are left so check to right + if (i < iFrom) { + idx = j*m_TerrainVerticesPerSide + (i + 1); + ensurecomputedbefore(idx); + if (!m_Vismap[idx]) + return false; + if (!m_IsBlockingVision[idx]) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i + 1, j); + return h2 - h3 > 1; + } + // 1.2.b) we are right so check to left + if (i > iFrom) { + idx = j*m_TerrainVerticesPerSide + (i - 1); + ensurecomputedbefore(idx); + if (!m_Vismap[idx]) + return false; + if (!m_IsBlockingVision[idx]) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i - 1, j); + return h2 - h3 > 1; + } + } + // 2) check diagonals to bottom + else if (j < jFrom) { + // 2.1) check diagonals to bottom right + if (i > iFrom) { + // 2.1.a) true diagonal + // - check only next diagonal tile + if (iDistA == jDistA) { + idx = (j + 1)*m_TerrainVerticesPerSide + (i - 1); + ensurecomputedbefore(idx); + if (!m_Vismap[idx]) + return false; + if (!m_IsBlockingVision[idx]) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i - 1, j + 1); + return h2 - h3 > 1; + } + // 2.1.b) we are under so check to top and top left + else if (jDistA > iDistA) { + if (!CheckTilesForVisibility(i, j + 1, i - 1, j + 1)) + return false; + // Tiles before are visible + if (!AreTilesHigher(i, j + 1, i - 1, j + 1)) + return true; + // Tiles before are blocking vision + h3 = m_Terrain->GetVertexGroundLevel(i, j + 1); + if (h2 - h3 <= 1) + return false; + h3 = m_Terrain->GetVertexGroundLevel(i - 1, j + 1); + return h2 - h3 > 1; + } + // 2.1.c) we are up so check to left and left top + else if (jDistA < iDistA) { + if (!CheckTilesForVisibility(i - 1, j, i - 1, j + 1)) + return false; + if (!AreTilesHigher(i - 1, j, i - 1, j + 1)) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i - 1, j); + if (h2 - h3 <= 1) + return false; + h3 = m_Terrain->GetVertexGroundLevel(i - 1, j + 1); + return h2 - h3 > 1; + } + } + // 2.2) check diagonals to bottom left + else if (i < iFrom) { + // 2.2.a) true diagonal + // - check only next diagonal tile + if (iDistA == jDistA) { + idx = (j + 1)*m_TerrainVerticesPerSide + (i + 1); + ensurecomputedbefore(idx); + if (!m_Vismap[idx]) + return false; + if (!m_IsBlockingVision[idx]) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i + 1, j + 1); + return h2 - h3 > 1; + } + // 2.2.b) we are under so check to top and top right + else if (jDistA > iDistA) { + if (!CheckTilesForVisibility(i, j + 1, i + 1, j + 1)) + return false; + if (!AreTilesHigher(i, j + 1, i + 1, j + 1)) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i, j + 1); + if (h2 - h3 <= 1) + return false; + h3 = m_Terrain->GetVertexGroundLevel(i + 1, j + 1); + return h2 - h3 > 1; + } + // 2.2.c) we are up so check to right and right top + else if (jDistA < iDistA) { + if (!CheckTilesForVisibility(i + 1, j, i + 1, j + 1)) + return false; + if (!AreTilesHigher(i + 1, j, i + 1, j + 1)) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i + 1, j); + if (h2 - h3 <= 1) + return false; + h3 = m_Terrain->GetVertexGroundLevel(i + 1, j + 1); + return h2 - h3 > 1; + } + } + } + // 3) check diagonals to top + else if (j > jFrom) { + // 3.1) check diagonals to top right + if (i > iFrom) { + // 3.1.a) true diagonal + // - check only next diagonal tile + if (iDistA == jDistA) { + idx = (j - 1)*m_TerrainVerticesPerSide + (i - 1); + ensurecomputedbefore(idx); + if (!m_Vismap[idx]) + return false; + if (!m_IsBlockingVision[idx]) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i - 1, j - 1); + return h2 - h3 > 1; + } + // 3.1.b) we are up so check down and down left + else if (jDistA < iDistA) { + if (!CheckTilesForVisibility(i - 1, j, i - 1, j - 1)) + return false; + if (!AreTilesHigher(i - 1, j, i - 1, j - 1)) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i - 1, j); + if (h2 - h3 <= 1) + return false; + h3 = m_Terrain->GetVertexGroundLevel(i - 1, j - 1); + return h2 - h3 > 1; + } + // 3.1.c) we are under so check to left and left down + else if (jDistA > iDistA) { + if (!CheckTilesForVisibility(i, j - 1, i - 1, j - 1)) + return false; + if (!AreTilesHigher(i, j - 1, i - 1, j - 1)) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i, j - 1); + if (h2 - h3 <= 1) + return false; + h3 = m_Terrain->GetVertexGroundLevel(i - 1, j - 1); + return h2 - h3 > 1; + } + } + // 3.2) check diagonals to top left + else if (i < iFrom) {// + // 3.2.a) true diagonal + // - check only next diagonal tile + if (iDistA == jDistA) { + idx = (j - 1)*m_TerrainVerticesPerSide + (i + 1); + ensurecomputedbefore(idx); + if (!m_Vismap[idx]) + return false; + if (!m_IsBlockingVision[idx]) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i + 1, j - 1); + return h2 - h3 > 1; + } + // 3.2.b) we are down so check to right and down right + else if (jDistA < iDistA) { + if (!CheckTilesForVisibility(i + 1, j, i + 1, j - 1)) + return false; + if (!AreTilesHigher(i + 1, j, i + 1, j - 1)) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i + 1, j); + if (h2 - h3 <= 1) + return false; + h3 = m_Terrain->GetVertexGroundLevel(i + 1, j - 1); + return h2 - h3 > 1; + } + // 3.2.c) we are up so check to right and right down + else if (jDistA > iDistA) { + if (!CheckTilesForVisibility(i, j - 1, i + 1, j - 1)) + return false; + if (!AreTilesHigher(i, j - 1, i + 1, j - 1)) + return true; + h3 = m_Terrain->GetVertexGroundLevel(i, j - 1); + if (h2 - h3 <= 1) + return false; + h3 = m_Terrain->GetVertexGroundLevel(i + 1, j - 1); + return h2 - h3 > 1; + } } + } + return true; + } - ASSERT(counts[idx] < 65535); - counts[idx] = (u16)(counts[idx] + 1); // ignore overflow; the player should never have 64K units + /** + * Check if tile is visible from given tile taking direction into account + * This is updating visibility map shared among all tiles so make sure + * this is called only from one thread + * @param iFrom position from starting tile + * @param jFrom position from starting tile + * @param hFrom height tiles higher than this will block visibility + * @param i position of tile we are checking + * @param j position of tile we are checking + * @param idx index of checking position in visibility map + * @return true iff given tile is visible + */ + inline bool IsVisibleTileMark(i32 iFrom, i32 jFrom, float hFrom, i32 i, i32 j, i32 idx) + { + #if DEBUG_VIS_COMP + if (m_VisComputedBefore.empty()) { + m_VisComputedBefore.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); + ENSURE(!m_VisComputedBefore.at(1)); + } + #endif + // not visible if off world + // if is of world do not bother updating visibility map + // there will be allways false + if (LosIsOffWorld(i, j)) { + #if DEBUG_VIS_COMP + if (i > -1 && j > -1 && idx < m_VisComputedBefore.size()) { + m_VisComputedBefore[idx] = true; + ENSURE(m_VisComputedBefore[idx]); + } + #endif + return false; } + if (!m_Terrain) + return true; + m_Vismap[idx] = IsVisibleTile(iFrom, jFrom, hFrom, i, j, idx); + #if DEBUG_VIS_COMP + m_VisComputedBefore[idx] = true; + ENSURE(m_VisComputedBefore[idx]); + #endif + return m_Vismap[idx]; } /** - * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). + * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive) + * Looking from tile (x, y) from height h + * @see IsVisibleTileMark */ - inline void LosRemoveStripHelper(u8 owner, i32 i0, i32 i1, i32 j, u16* counts) + inline void LosManageStripHelper(bool add, u8 owner, i32 i0, i32 i1, i32 j, u16* counts, float h, i32 x, i32 y) { if (i1 < i0) return; - i32 idx0 = j*m_TerrainVerticesPerSide + i0; i32 idx1 = j*m_TerrainVerticesPerSide + i1; + u32 &explored = m_ExploredVertices.at(owner); for (i32 idx = idx0; idx <= idx1; ++idx) { - ASSERT(counts[idx] > 0); - counts[idx] = (u16)(counts[idx] - 1); + i32 i = i0 + idx - idx0; + if (!IsVisibleTileMark(x, y, h, i, j, idx)) + continue; + if (add) { + // Increasing from zero to non-zero - move from unexplored/explored to visible+explored + if (counts[idx] == 0) + { + explored += !(m_LosState[idx] & (LOS_EXPLORED << (2 * (owner - 1)))); + m_LosState[idx] |= ((LOS_VISIBLE | LOS_EXPLORED) << (2 * (owner - 1))); - // Decreasing from non-zero to zero - move from visible+explored to explored - if (counts[idx] == 0) - { - // (If LosIsOffWorld then this is a no-op, so don't bother doing the check) - m_LosState[idx] &= ~(LOS_VISIBLE << (2*(owner-1))); + MarkVisibilityDirtyAroundTile(owner, i, j); + } - i32 i = i0 + idx - idx0; - MarkVisibilityDirtyAroundTile(owner, i, j); + ENSURE(counts[idx] < 65535); + counts[idx] = (u16)(counts[idx] + 1); // ignore overflow; the player should never have 64K units + } + else { + if (counts[idx]) { + counts[idx] = (u16)(counts[idx] - 1); + // Decreasing from non-zero to zero - move from visible+explored to explored + if (counts[idx] == 0) + { + m_LosState[idx] &= ~(LOS_VISIBLE << (2 * (owner - 1))); + + MarkVisibilityDirtyAroundTile(owner, i, j); + } + ENSURE(counts[idx] > -1); + } } } } + /** + * Inverted function compared to LosManagerStripHelper iterating from (i, j0) to (i, j1) + * @see IsVisibleTileMark + */ + inline void LosManageStripHelperJ(bool add, u8 owner, i32 j0, i32 j1, i32 i, u16* counts, float h, i32 x, i32 y) + { + for (i32 j = j0; j <= j1; ++j) + LosManageStripHelper(add, owner, i, i, j, counts, h, x, y); + } inline void MarkVisibilityDirtyAroundTile(u8 owner, i32 i, i32 j) { @@ -2132,9 +2511,10 @@ * Update the LOS state of tiles within a given circular range, * either adding or removing visibility depending on the template parameter. * Assumes owner is in the valid range. + * Provide vision blocking by terrain. + * Height (eH) is added to ground/water level. */ - template - void LosUpdateHelper(u8 owner, entity_pos_t visionRange, CFixedVector2D pos) + template void LosUpdateHelper(u8 owner, entity_pos_t visionRange, CFixedVector2D pos, u16 eH) { if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet return; @@ -2149,265 +2529,189 @@ u16* countsData = &counts[0]; - // Compute the circular region as a series of strips. - // Rather than quantise pos to vertexes, we do more precise sub-tile computations - // to get smoother behaviour as a unit moves rather than jumping a whole tile - // at once. - // To avoid the cost of sqrt when computing the outline of the circle, - // we loop from the bottom to the top and estimate the width of the current - // strip based on the previous strip, then adjust each end of the strip - // inwards or outwards until it's the widest that still falls within the circle. - - // Compute top/bottom coordinates, and clamp to exclude the 1-tile border around the map - // (so that we never render the sharp edge of the map) - i32 j0 = ((pos.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); - i32 j1 = ((pos.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); - i32 j0clamp = std::max(j0, 1); - i32 j1clamp = std::min(j1, m_TerrainVerticesPerSide-2); + // Create empty vision map if needed + if (m_Vismap.empty()) + m_Vismap.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); + // Create empty blocking map if needed + if (m_IsBlockingVision.empty()) + m_IsBlockingVision.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); + // Reset this as we are starting new detection + m_VisionBlocked = false; // Translate world coordinates into fractional tile-space coordinates - entity_pos_t x = pos.X / (int)TERRAIN_TILE_SIZE; - entity_pos_t y = pos.Y / (int)TERRAIN_TILE_SIZE; - entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE; + entity_pos_t x = pos.X / static_cast(TERRAIN_TILE_SIZE); + entity_pos_t y = pos.Y / static_cast(TERRAIN_TILE_SIZE); + entity_pos_t r = visionRange / static_cast(TERRAIN_TILE_SIZE); entity_pos_t r2 = r.Square(); - // Compute the integers on either side of x i32 xfloor = (x - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); - i32 xceil = (x + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); - - // Initialise the strip (i0, i1) to a rough guess - i32 i0 = xfloor; - i32 i1 = xceil; - - for (i32 j = j0clamp; j <= j1clamp; ++j) - { - // Adjust i0 and i1 to be the outermost values that don't exceed - // the circle's radius (i.e. require dy^2 + dx^2 <= r^2). - // When moving the points inwards, clamp them to xceil+1 or xfloor-1 - // so they don't accidentally shoot off in the wrong direction forever. - - entity_pos_t dy = entity_pos_t::FromInt(j) - y; - entity_pos_t dy2 = dy.Square(); - while (dy2 + (entity_pos_t::FromInt(i0-1) - x).Square() <= r2) - --i0; - while (i0 < xceil && dy2 + (entity_pos_t::FromInt(i0) - x).Square() > r2) - ++i0; - while (dy2 + (entity_pos_t::FromInt(i1+1) - x).Square() <= r2) - ++i1; - while (i1 > xfloor && dy2 + (entity_pos_t::FromInt(i1) - x).Square() > r2) - --i1; + i32 yfloor = (y - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); + i32 rfloor = (r - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); -#if DEBUG_RANGE_MANAGER_BOUNDS - if (i0 <= i1) - { - ENSURE(dy2 + (entity_pos_t::FromInt(i0) - x).Square() <= r2); - ENSURE(dy2 + (entity_pos_t::FromInt(i1) - x).Square() <= r2); - } - ENSURE(dy2 + (entity_pos_t::FromInt(i0 - 1) - x).Square() > r2); - ENSURE(dy2 + (entity_pos_t::FromInt(i1 + 1) - x).Square() > r2); -#endif - - // Clamp the strip to exclude the 1-tile border, - // then add or remove the strip as requested - i32 i0clamp = std::max(i0, 1); - i32 i1clamp = std::min(i1, m_TerrainVerticesPerSide-2); - if (adding) - LosAddStripHelper(owner, i0clamp, i1clamp, j, countsData); - else - LosRemoveStripHelper(owner, i0clamp, i1clamp, j, countsData); - } - } - - /** - * Update the LOS state of tiles within a given circular range, - * by removing visibility around the 'from' position - * and then adding visibility around the 'to' position. - */ - void LosUpdateHelperIncremental(u8 owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) - { - if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet + #if DEBUG_VIS_COMP + if (m_VisComputedBefore.empty()) { + m_VisComputedBefore.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); + ENSURE(!m_VisComputedBefore.at(1)); + } + std::fill(m_VisComputedBefore.begin(), m_VisComputedBefore.end(), false); + ENSURE(m_VisComputedBefore.size() == m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); + #endif + // Do nothing if of world + if (LosIsOffWorld(xfloor, yfloor)) { + #if DEBUG_VIS_COMP + i32 idxd = yfloor*m_TerrainVerticesPerSide + xfloor; + m_VisComputedBefore.at(idxd) = true; + ENSURE(m_VisComputedBefore.at(idxd)); + #endif return; + } - PROFILE("LosUpdateHelperIncremental"); - - std::vector& counts = m_LosPlayerCounts.at(owner); - - // Lazy initialisation of counts: - if (counts.empty()) - counts.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); - - u16* countsData = &counts[0]; - - // See comments in LosUpdateHelper. - // This does exactly the same, except computing the strips for - // both circles simultaneously. - // (The idea is that the circles will be heavily overlapping, - // so we can compute the difference between the removed/added strips - // and only have to touch tiles that have a net change.) - - i32 j0_from = ((from.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); - i32 j1_from = ((from.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); - i32 j0_to = ((to.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); - i32 j1_to = ((to.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); - i32 j0clamp = std::max(std::min(j0_from, j0_to), 1); - i32 j1clamp = std::min(std::max(j1_from, j1_to), m_TerrainVerticesPerSide-2); - - entity_pos_t x_from = from.X / (int)TERRAIN_TILE_SIZE; - entity_pos_t y_from = from.Y / (int)TERRAIN_TILE_SIZE; - entity_pos_t x_to = to.X / (int)TERRAIN_TILE_SIZE; - entity_pos_t y_to = to.Y / (int)TERRAIN_TILE_SIZE; - entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE; - entity_pos_t r2 = r.Square(); - - i32 xfloor_from = (x_from - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); - i32 xceil_from = (x_from + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); - i32 xfloor_to = (x_to - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); - i32 xceil_to = (x_to + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); - - i32 i0_from = xfloor_from; - i32 i1_from = xceil_from; - i32 i0_to = xfloor_to; - i32 i1_to = xceil_to; - - for (i32 j = j0clamp; j <= j1clamp; ++j) - { - entity_pos_t dy_from = entity_pos_t::FromInt(j) - y_from; - entity_pos_t dy2_from = dy_from.Square(); - while (dy2_from + (entity_pos_t::FromInt(i0_from-1) - x_from).Square() <= r2) - --i0_from; - while (i0_from < xceil_from && dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() > r2) - ++i0_from; - while (dy2_from + (entity_pos_t::FromInt(i1_from+1) - x_from).Square() <= r2) - ++i1_from; - while (i1_from > xfloor_from && dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() > r2) - --i1_from; - - entity_pos_t dy_to = entity_pos_t::FromInt(j) - y_to; - entity_pos_t dy2_to = dy_to.Square(); - while (dy2_to + (entity_pos_t::FromInt(i0_to-1) - x_to).Square() <= r2) - --i0_to; - while (i0_to < xceil_to && dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() > r2) - ++i0_to; - while (dy2_to + (entity_pos_t::FromInt(i1_to+1) - x_to).Square() <= r2) - ++i1_to; - while (i1_to > xfloor_to && dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() > r2) - --i1_to; - -#if DEBUG_RANGE_MANAGER_BOUNDS - if (i0_from <= i1_from) - { - ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() <= r2); - ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() <= r2); - } - ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from - 1) - x_from).Square() > r2); - ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from + 1) - x_from).Square() > r2); - if (i0_to <= i1_to) - { - ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() <= r2); - ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() <= r2); + // Get height of terrain/water at entity position + float h = 0.0; + if (m_Terrain) + h = m_Terrain->GetVertexGroundLevel(xfloor, yfloor); + CmpPtr cmpWaterManager(GetSystemEntity()); + if (cmpWaterManager) { + float wl = cmpWaterManager->GetWaterLevel(pos.X, pos.Y).ToFloat(); + if (wl > h) + h = wl; + } + // Add entity heigth + h = h + eH; + + // Direction of incrementation of square + i32 ipp = 1; + // Guess how many tiles we should check at squre side + // Incremented by ipp at each step + i32 ip = 0; + // incrementally make squares larger + for (i32 range = 0; range < rfloor + 4; ++range) { + // This defines square with side 2*range + // and centre at entity position + i32 i0 = xfloor - range; + i32 i1 = xfloor + range; + i32 j0 = yfloor - range; + i32 j1 = yfloor + range; + + // This defines which tiles at given + // square we have to check + i32 ifrom = xfloor - ip; + i32 ito = xfloor + ip; + // we cut corners so we do not check twice + i32 jfrom = yfloor - ip + 1; + i32 jto = yfloor + ip - 1; + entity_pos_t dy; + entity_pos_t dy2; + if (ipp > 0) { + dy = entity_pos_t::FromInt(range + 1); + dy2 = dy.Square(); + // We have filled square and half length of diagonal is larger + // than vision range so invert direction + if (dy2 + dy2 > r2) + ipp = -1; + } + else if (ipp < 0) { + dy = entity_pos_t::FromInt(range); + dy2 = dy.Square(); + // We went out of vision range so abort + if (dy2 > r2) + break; + } + // We might overshoot vision range so make it more looks like circle + while (ifrom < xfloor && dy2 + (entity_pos_t::FromInt(ifrom) - x).Square() > r2) + ifrom++; + while (ito > xfloor + 1 && dy2 + (entity_pos_t::FromInt(ito) - x).Square() > r2) + ito--; + while (jfrom < yfloor && dy2 + (entity_pos_t::FromInt(jfrom) - y).Square() > r2) + jfrom++; + while (jto > yfloor + 1 && dy2 + (entity_pos_t::FromInt(jto) - y).Square() > r2) + jto--; + + ip = ip + ipp; + // Keep square envelope cordinates in map + i0 = std::max(i0, 1); + i1 = std::min(i1, m_TerrainVerticesPerSide - 2); + j0 = std::max(j0, 1); + j1 = std::min(j1, m_TerrainVerticesPerSide - 2); + // The same for side cuts + ifrom = std::max(ifrom, 1); + ito = std::min(ito, m_TerrainVerticesPerSide - 2); + jfrom = std::max(jfrom, 1); + jto = std::min(jto, m_TerrainVerticesPerSide - 2); + + // First check bottom line + LosManageStripHelper(adding, owner, ifrom, ito, j0, countsData, h, xfloor, yfloor); + // Check top line + if (j0 != j1) + LosManageStripHelper(adding, owner, ifrom, ito, j1, countsData, h, xfloor, yfloor); + // Make sure jfrom is not greather than jto because of cutting corners + if (jfrom <= jto) { + // Now check remaining tiles at left side of square + LosManageStripHelperJ(adding, owner, jfrom, jto, i0, countsData, h, xfloor, yfloor); + // At last do the check for tiles at right side of square, + if (i0 != i1) + LosManageStripHelperJ(adding, owner, jfrom, jto, i1, countsData, h, xfloor, yfloor); } - ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to - 1) - x_to).Square() > r2); - ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to + 1) - x_to).Square() > r2); -#endif - // Check whether this strip moved at all - if (!(i0_to == i0_from && i1_to == i1_from)) - { - i32 i0clamp_from = std::max(i0_from, 1); - i32 i1clamp_from = std::min(i1_from, m_TerrainVerticesPerSide-2); - i32 i0clamp_to = std::max(i0_to, 1); - i32 i1clamp_to = std::min(i1_to, m_TerrainVerticesPerSide-2); - - // Check whether one strip is negative width, - // and we can just add/remove the entire other strip - if (i1clamp_from < i0clamp_from) - { - LosAddStripHelper(owner, i0clamp_to, i1clamp_to, j, countsData); - } - else if (i1clamp_to < i0clamp_to) - { - LosRemoveStripHelper(owner, i0clamp_from, i1clamp_from, j, countsData); - } - else - { - // There are four possible regions of overlap between the two strips - // (remove before add, remove after add, add before remove, add after remove). - // Process each of the regions as its own strip. - // (If this produces negative-width strips then they'll just get ignored - // which is fine.) - // (If the strips don't actually overlap (which is very rare with normal unit - // movement speeds), the region between them will be both added and removed, - // so we have to do the add first to avoid overflowing to -1 and triggering - // assertion failures.) - LosAddStripHelper(owner, i0clamp_to, i0clamp_from-1, j, countsData); - LosAddStripHelper(owner, i1clamp_from+1, i1clamp_to, j, countsData); - LosRemoveStripHelper(owner, i0clamp_from, i0clamp_to-1, j, countsData); - LosRemoveStripHelper(owner, i1clamp_to+1, i1clamp_from, j, countsData); - } - } + // We have no more tiles to check + if (ip < 1) + break; } } - void LosAdd(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) + void LosAdd(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos, u16 eH) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; - LosUpdateHelper((u8)owner, visionRange, pos); + LosUpdateHelper((u8)owner, visionRange, pos, eH); } - void SharingLosAdd(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos) + void SharingLosAdd(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos, u16 eH) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) - LosAdd(i, visionRange, pos); + LosAdd(i, visionRange, pos, eH); } - void LosRemove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) + void LosRemove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos, u16 eH) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; - LosUpdateHelper((u8)owner, visionRange, pos); + LosUpdateHelper((u8)owner, visionRange, pos, eH); } - void SharingLosRemove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos) + void SharingLosRemove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos, u16 eH) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) - LosRemove(i, visionRange, pos); + LosRemove(i, visionRange, pos, eH); } - void LosMove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) + void LosMove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to, u16 eH) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; - if ((from - to).CompareLength(visionRange) > 0) - { - // If it's a very large move, then simply remove and add to the new position - LosUpdateHelper((u8)owner, visionRange, from); - LosUpdateHelper((u8)owner, visionRange, to); - } - else - // Otherwise use the version optimised for mostly-overlapping circles - LosUpdateHelperIncremental((u8)owner, visionRange, from, to); + LosUpdateHelper((u8)owner, visionRange, from, eH); + LosUpdateHelper((u8)owner, visionRange, to, eH); } - void SharingLosMove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) + void SharingLosMove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to, u16 eH) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) - LosMove(i, visionRange, from, to); + LosMove(i, visionRange, from, to, eH); } virtual u8 GetPercentMapExplored(player_id_t player) const @@ -2415,13 +2719,18 @@ return m_ExploredVertices.at((u8)player) * 100 / m_TotalInworldVertices; } + virtual u32 GetExploredVertices(player_id_t player) const + { + return m_ExploredVertices.at((u8)player); + } + virtual u8 GetUnionPercentMapExplored(const std::vector& players) const { u32 exploredVertices = 0; std::vector::const_iterator playerIt; - for (i32 j = 0; j < m_TerrainVerticesPerSide; j++) - for (i32 i = 0; i < m_TerrainVerticesPerSide; i++) + for (i32 j = 0; j < m_TerrainVerticesPerSide; ++j) + for (i32 i = 0; i < m_TerrainVerticesPerSide; ++i) { if (LosIsOffWorld(i, j)) continue; Index: source/simulation2/components/ICmpFootprint.h =================================================================== --- source/simulation2/components/ICmpFootprint.h +++ source/simulation2/components/ICmpFootprint.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -56,6 +56,8 @@ */ JS::Value GetShape_wrapper() const; + virtual entity_pos_t GetHeight() const = 0; + /** * Pick a sensible position to place a newly-spawned entity near this footprint, * such that it won't be in an invalid (obstructed) location regardless of the spawned unit's Index: source/simulation2/components/ICmpFootprint.cpp =================================================================== --- source/simulation2/components/ICmpFootprint.cpp +++ source/simulation2/components/ICmpFootprint.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -71,5 +71,6 @@ BEGIN_INTERFACE_WRAPPER(Footprint) DEFINE_INTERFACE_METHOD_CONST_1("PickSpawnPoint", CFixedVector3D, ICmpFootprint, PickSpawnPoint, entity_id_t) DEFINE_INTERFACE_METHOD_CONST_1("PickSpawnPointBothPass", CFixedVector3D, ICmpFootprint, PickSpawnPointBothPass, entity_id_t) +DEFINE_INTERFACE_METHOD_CONST_0("GetHeight", entity_pos_t, ICmpFootprint,GetHeight) DEFINE_INTERFACE_METHOD_CONST_0("GetShape", JS::Value, ICmpFootprint, GetShape_wrapper) END_INTERFACE_WRAPPER(Footprint) Index: source/simulation2/components/ICmpRangeManager.h =================================================================== --- source/simulation2/components/ICmpRangeManager.h +++ source/simulation2/components/ICmpRangeManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -375,7 +375,7 @@ * will be necessary to call it the same number of times with !enabled to make the shore * fall back into the FoW. */ - virtual void RevealShore(player_id_t p, bool enable) = 0; + virtual void RevealShore(player_id_t p, bool enable, u16 h) = 0; /** * Set whether the whole map should be made visible to the given player. @@ -425,6 +425,17 @@ */ virtual void Verify() = 0; + /* + * Return if tile with given coordinates has given los state for player + * Used by tests + */ + virtual bool TestVisibility(int i, int j, player_id_t player, ELosState state) const = 0; + + /** + * Get amount of explored vertices by player + * Used by tests + */ + virtual u32 GetExploredVertices(player_id_t player) const = 0; DECLARE_INTERFACE_TYPE(RangeManager) }; Index: source/simulation2/components/tests/test_RangeManager.h =================================================================== --- source/simulation2/components/tests/test_RangeManager.h +++ source/simulation2/components/tests/test_RangeManager.h @@ -17,26 +17,63 @@ #include "simulation2/system/ComponentTest.h" #include "simulation2/components/ICmpRangeManager.h" +#include "simulation2/components/ICmpWaterManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpVision.h" +#include "simulation2/components/ICmpFootprint.h" +#include "simulation2/components/ICmpOwnership.h" + +#include "graphics/Terrain.h" +#include "simulation2/helpers/Geometry.h" #include #include +class MockOwnership : public ICmpOwnership +{ + private: + player_id_t m_Owner; + public: + virtual int GetComponentTypeId() const { return IID_Ownership; } + virtual void Init(const CParamNode& UNUSED(paramNode)) { } + virtual void Deinit() {} + virtual void Serialize(ISerializer& UNUSED(serialize)) { } + virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) + { Init(paramNode);} + virtual void SetOwner(player_id_t owner) { m_Owner = owner; } + virtual player_id_t GetOwner() const { return m_Owner; }; + virtual void SetOwnerQuiet(player_id_t owner) { m_Owner = owner; } +}; + class MockVision : public ICmpVision { +private: + entity_pos_t m_Range; public: - DEFAULT_MOCK_COMPONENT() - - virtual entity_pos_t GetRange() const { return entity_pos_t::FromInt(66); } + virtual int GetComponentTypeId() const { return IID_Vision; } + virtual void Init(const CParamNode& UNUSED(paramNode)) { } + virtual void Deinit() {} + virtual void Serialize(ISerializer& UNUSED(serialize)) { } + virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) + { Init(paramNode); } + virtual void SetRange(entity_pos_t range) { m_Range = range; } + virtual entity_pos_t GetRange() const { return m_Range; } virtual bool GetRevealShore() const { return false; } }; class MockPosition : public ICmpPosition { +private: + CFixedVector2D m_Pos2d; public: - DEFAULT_MOCK_COMPONENT() + virtual int GetComponentTypeId() const { return IID_Position; } + virtual void Init(const CParamNode& UNUSED(paramNode)) { } + virtual void Deinit() {} + virtual void Serialize(ISerializer& UNUSED(serialize)) { } + virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) + { Init(paramNode); } + virtual void SetPosition(entity_pos_t x, entity_pos_t y) { m_Pos2d.X = x; m_Pos2d.Y = y;} virtual void SetTurretParent(entity_id_t UNUSED(id), const CFixedVector3D& UNUSED(pos)) {} virtual entity_id_t GetTurretParent() const {return INVALID_ENTITY;} virtual void UpdateTurretPosition() {} @@ -57,7 +94,7 @@ virtual void SetActorFloating(bool UNUSED(flag)) { } virtual void SetConstructionProgress(fixed UNUSED(progress)) { } virtual CFixedVector3D GetPosition() const { return CFixedVector3D(); } - virtual CFixedVector2D GetPosition2D() const { return CFixedVector2D(); } + virtual CFixedVector2D GetPosition2D() const { return m_Pos2d; } virtual CFixedVector3D GetPreviousPosition() const { return CFixedVector3D(); } virtual CFixedVector2D GetPreviousPosition2D() const { return CFixedVector2D(); } virtual void TurnTo(entity_angle_t UNUSED(y)) { } @@ -69,6 +106,188 @@ virtual CMatrix3D GetInterpolatedTransform(float UNUSED(frameOffset)) const { return CMatrix3D(); } }; +class MockWater : public ICmpWaterManager +{ +public: + DEFAULT_MOCK_COMPONENT() + + virtual entity_pos_t GetWaterLevel(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) const + { + return entity_pos_t::FromInt(0); + } + + virtual float GetExactWaterLevel(float UNUSED(x), float UNUSED(z)) const + { + return 0.f; + } + + virtual void RecomputeWaterData() + { + } + + virtual void SetWaterLevel(entity_pos_t UNUSED(h)) + { + } +}; + +class MockFullTerrain : public ICmpTerrain +{ +private: + CTerrain* m_Terrain; +public: + virtual int GetComponentTypeId() const + { + return -1; + } + + static void ClassInit(CComponentManager& UNUSED(componentManager)) + { + } + + virtual bool IsLoaded() const + { + return m_Terrain->GetVerticesPerSide() != 0; + } + + virtual void Init(const CParamNode& UNUSED(paramNode)) + { + } + + virtual void Deinit() + { + m_Terrain = nullptr; + } + + virtual void Serialize(ISerializer& UNUSED(serialize)) + { + + } + + virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) + { + Init(paramNode); + } + + virtual CFixedVector3D CalcNormal(entity_pos_t x, entity_pos_t z) const + { + CFixedVector3D normal; + m_Terrain->CalcNormalFixed((x / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), (z / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), normal); + return normal; + } + + virtual CVector3D CalcExactNormal(float x, float z) const + { + return m_Terrain->CalcExactNormal(x, z); + } + + virtual entity_pos_t GetGroundLevel(entity_pos_t x, entity_pos_t z) const + { + return m_Terrain->GetExactGroundLevelFixed(x, z); + } + + virtual float GetExactGroundLevel(float x, float z) const + { + return m_Terrain->GetExactGroundLevel(x, z); + } + + virtual u16 GetTilesPerSide() const + { + ssize_t tiles = m_Terrain->GetTilesPerSide(); + + if (tiles == -1) + return 0; + ENSURE(1 <= tiles && tiles <= 65535); + return (u16)tiles; + } + + virtual u32 GetMapSize() const + { + return GetTilesPerSide() * TERRAIN_TILE_SIZE; + } + + virtual u16 GetVerticesPerSide() const + { + ssize_t vertices = m_Terrain->GetVerticesPerSide(); + ENSURE(1 <= vertices && vertices <= 65535); + return (u16)vertices; + } + + virtual CTerrain* GetCTerrain() + { + return m_Terrain; + } + + virtual void SetTerrain(CTerrain* t) + { + m_Terrain = t; + } + + virtual void MakeDirty(i32 i0, i32 j0, i32 i1, i32 j1) + { + CMessageTerrainChanged msg(i0, j0, i1, j1); + GetSimContext().GetComponentManager().BroadcastMessage(msg); + } + + virtual void ReloadTerrain(bool UNUSED(ReloadWater)) + { + u16 tiles = GetTilesPerSide(); + u16 vertices = GetVerticesPerSide(); + + CmpPtr cmpObstructionManager(GetSystemEntity()); + if (cmpObstructionManager) + { + cmpObstructionManager->SetBounds(entity_pos_t::Zero(), entity_pos_t::Zero(), + entity_pos_t::FromInt(tiles * static_cast(TERRAIN_TILE_SIZE)), + entity_pos_t::FromInt(tiles * static_cast(TERRAIN_TILE_SIZE))); + } + + CmpPtr cmpRangeManager(GetSystemEntity()); + if (cmpRangeManager) + { + cmpRangeManager->SetBounds(entity_pos_t::Zero(), entity_pos_t::Zero(), + entity_pos_t::FromInt(tiles * static_cast(TERRAIN_TILE_SIZE)), + entity_pos_t::FromInt(tiles * static_cast(TERRAIN_TILE_SIZE)), + vertices); + } + MakeDirty(0, 0, tiles + 1, tiles + 1); + } +}; + +class MockFootprint : public ICmpFootprint +{ + private: + entity_pos_t m_Height; + public: + + virtual int GetComponentTypeId() const { return IID_Footprint; } + virtual void Init(const CParamNode& UNUSED(paramNode)) { } + virtual void Deinit() {} + virtual void Serialize(ISerializer& UNUSED(serialize)) { } + virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) + { Init(paramNode); } + virtual void GetShape(EShape& UNUSED(shape), entity_pos_t& UNUSED(size0), entity_pos_t& UNUSED(size1), entity_pos_t& height) const + { + height = m_Height; + } + virtual entity_pos_t GetHeight() const + { + return m_Height; + } + virtual void SetHeight(entity_pos_t h) + { + m_Height = h; + } + virtual CFixedVector3D PickSpawnPoint(entity_id_t UNUSED(spawned)) const + { + return CFixedVector3D(); + } + virtual CFixedVector3D PickSpawnPointBothPass(entity_id_t UNUSED(spawned)) const + { + return CFixedVector3D(); + } +}; + +#include class TestCmpRangeManager : public CxxTest::TestSuite { public: @@ -82,21 +301,252 @@ CXeromyces::Terminate(); } + void test_losComputation() + { + ComponentTestHelper test(g_ScriptRuntime); + + ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); + + entity_id_t entityId = 100; + int playerId = 1; + int playerId2 = 2; + + MockVision vision; + vision.SetRange(entity_pos_t::FromInt(64)); + test.AddMock(entityId, IID_Vision, vision); + + MockPosition position; + test.AddMock(entityId, IID_Position, position); + + MockFootprint footprint; + footprint.SetHeight(entity_pos_t::FromInt(5)); + test.AddMock(entityId, IID_Footprint, footprint); + + CTerrain mTerrain; + MockFullTerrain terrain; + terrain.SetTerrain(&mTerrain); + // Flat terrain + mTerrain.Initialize(8, nullptr); + + MockWater water; + test.AddMock(SYSTEM_ENTITY, IID_WaterManager, water); + + int xMax = 512; + ssize_t tvps = xMax / TERRAIN_TILE_SIZE + 1; + + cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(xMax), entity_pos_t::FromInt(xMax), tvps); + + // Check that nothing is explored + TS_ASSERT_EQUALS(cmp->GetPercentMapExplored(playerId), 0); + + { CMessageCreate msg(entityId); cmp->HandleMessage(msg, false); } + { CMessageOwnershipChanged msg(entityId, -1, playerId); cmp->HandleMessage(msg, false); } + { CMessagePositionChanged msg(entityId, true, entity_pos_t::FromInt(247), entity_pos_t::FromDouble(257.95), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } + + // Check that something is explored for player1 + u8 expl = cmp->GetPercentMapExplored(playerId); + TS_ASSERT_LESS_THAN(0, expl); + // And the same amount is visible for player1 and nothing explored or visible for player2 + u32 vis = 0; + u32 ee = 0; + for (int i = 0; i < tvps; ++i) { + for (int j = 0; j < tvps; ++j) { + if (cmp->TestVisibility(i, j, playerId, ICmpRangeManager::LOS_VISIBLE)) + vis = vis + 1; + if (cmp->TestVisibility(i, j, playerId, ICmpRangeManager::LOS_EXPLORED)) + ee = ee + 1; + TS_ASSERT_EQUALS(cmp->TestVisibility(i, j, playerId2, ICmpRangeManager::LOS_VISIBLE), false); + TS_ASSERT_EQUALS(cmp->TestVisibility(i, j, playerId2, ICmpRangeManager::LOS_EXPLORED), false); + } + } + TS_ASSERT_LESS_THAN(0, ee); + TS_ASSERT_LESS_THAN(0, vis); + u32 explV = cmp->GetExploredVertices(playerId); + // Explored vertices matches count of explored vertices in map + TS_ASSERT_EQUALS(ee, explV); + // The same amount is explored and visible + TS_ASSERT_EQUALS(ee, vis); + + { CMessageOwnershipChanged msg(entityId, playerId, -1); cmp->HandleMessage(msg, false); } + { CMessageDestroy msg(entityId); cmp->HandleMessage(msg, false); } + + // Check that nothing is visible + u32 ee2 = 0; + for (int i = 0; i < tvps; ++i) + for (int j = 0; j < tvps; ++j) { + TS_ASSERT_EQUALS(cmp->TestVisibility(i, j, playerId, ICmpRangeManager::LOS_VISIBLE), false); + if (cmp->TestVisibility(i, j, playerId, ICmpRangeManager::LOS_EXPLORED)) + ee2 = ee2 + 1; + } + // And still the same amount is explored as before + TS_ASSERT_EQUALS(ee, ee2); + + } + + void test_visibility() + { + ComponentTestHelper test(g_ScriptRuntime); + + ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); + + entity_id_t entityId = 100; + entity_id_t entityId2 = 200; + player_id_t playerId = 1; + player_id_t playerId2 = 2; + + // Setup entity 1 + int numInt1 = IID__LastNative; + MockVision vision1; + vision1.SetRange(entity_pos_t::FromInt(10)); + test.AddMock(entityId, IID_Vision, vision1); + + MockPosition position1; + test.AddMock(entityId, IID_Position, position1); + + MockFootprint footprint1; + footprint1.SetHeight(entity_pos_t::FromInt(5)); + test.AddMock(entityId, IID_Footprint, footprint1); + + MockOwnership ownership1; + ownership1.SetOwner(playerId); + test.AddMock(entityId, IID_Ownership, ownership1); + + SEntityComponentCache* compCache1 = (SEntityComponentCache*)calloc(1, + sizeof(SEntityComponentCache) + sizeof(IComponent*) * numInt1); + ENSURE(compCache1 != nullptr); + compCache1->numInterfaces = numInt1 + 1; + compCache1->interfaces[IID_Position] = &position1; + compCache1->interfaces[IID_Ownership] = &ownership1; + compCache1->interfaces[IID_Footprint] = &footprint1; + compCache1->interfaces[IID_Vision] = &vision1; + CEntityHandle handle1(entityId, compCache1); + + // Setup entity 2 + MockVision vision2; + vision2.SetRange(entity_pos_t::FromInt(20)); + test.AddMock(entityId2, IID_Vision, vision2); + + MockPosition position2; + test.AddMock(entityId2, IID_Position, position2); + + MockFootprint footprint2; + footprint2.SetHeight(entity_pos_t::FromInt(5)); + test.AddMock(entityId2, IID_Footprint, footprint2); + + MockOwnership ownership2; + ownership2.SetOwner(playerId2); + test.AddMock(entityId2, IID_Ownership, ownership2); + + SEntityComponentCache* compCache2 = (SEntityComponentCache*)calloc(1, + sizeof(SEntityComponentCache) + sizeof(IComponent*) * numInt1); + ENSURE(compCache2 != nullptr); + compCache2->numInterfaces = numInt1 + 1; + compCache2->interfaces[IID_Position] = &position2; + compCache2->interfaces[IID_Ownership] = &ownership2; + compCache2->interfaces[IID_Footprint] = &footprint2; + compCache2->interfaces[IID_Vision] = &vision2; + CEntityHandle handle2(entityId2, compCache2); + + // Setup players + std::vector see1; + see1.push_back(playerId); + std::vector see2; + see2.push_back(playerId2); + + CTerrain mTerrain; + MockFullTerrain terrain; + terrain.SetTerrain(&mTerrain); + // Flat terrain + mTerrain.Initialize(8, nullptr); + + MockWater water; + test.AddMock(SYSTEM_ENTITY, IID_WaterManager, water); + + int xMax = 512; + ssize_t tvps = xMax / TERRAIN_TILE_SIZE + 1; + + cmp->SetSharedLos(playerId, see1); + TS_ASSERT_EQUALS(cmp->GetSharedLosMask(playerId), ICmpRangeManager::LOS_MASK << (2 * (playerId - 1))); + cmp->SetSharedLos(playerId2, see2); + cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(xMax), entity_pos_t::FromInt(xMax), tvps); + + { CMessageCreate msg(entityId); cmp->HandleMessage(msg, false); } + { CMessageCreate msg(entityId2); cmp->HandleMessage(msg, false); } + + { CMessageOwnershipChanged msg(entityId, -1, playerId); cmp->HandleMessage(msg, false); } + { CMessageOwnershipChanged msg(entityId2, -1, playerId2); cmp->HandleMessage(msg, false); } + + position1.SetPosition(entity_pos_t::FromInt(100), entity_pos_t::FromDouble(100)); + position2.SetPosition(entity_pos_t::FromInt(115), entity_pos_t::FromDouble(100)); + + { CMessagePositionChanged msg(entityId, true, entity_pos_t::FromInt(100), entity_pos_t::FromDouble(100), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } + { CMessagePositionChanged msg(entityId2, true, entity_pos_t::FromInt(115), entity_pos_t::FromDouble(100), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } + CmpPtr cmpPosition(handle1); + if (!cmpPosition) + TS_FAIL("entity with handle1 has not position component"); + CFixedVector2D pos = cmpPosition->GetPosition2D(); + int i = (pos.X / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToNearest(); + int j = (pos.Y / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToNearest(); + if (!cmp->TestVisibility(i, j, playerId, ICmpRangeManager::LOS_VISIBLE)) + TS_FAIL("Entity does not see its own position"); + + // Players see their own entities + TS_ASSERT_EQUALS(cmp->GetLosVisibility(handle1, playerId), ICmpRangeManager::VIS_VISIBLE); + TS_ASSERT_EQUALS(cmp->GetLosVisibility(handle2, playerId2), ICmpRangeManager::VIS_VISIBLE); + // Player1 cannot see entity2 + TS_ASSERT_EQUALS(cmp->GetLosVisibility(handle2, playerId), ICmpRangeManager::VIS_HIDDEN); + // Player2 can see entity1 + TS_ASSERT_EQUALS(cmp->GetLosVisibility(handle1, playerId2), ICmpRangeManager::VIS_VISIBLE); + + position2.SetPosition(entity_pos_t::FromInt(121), entity_pos_t::FromDouble(100)); + { CMessagePositionChanged msg(entityId2, true, entity_pos_t::FromInt(121), entity_pos_t::FromDouble(100), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } + // Player2 cannot see entity1 + TS_ASSERT_EQUALS(cmp->GetLosVisibility(handle1, playerId2), ICmpRangeManager::VIS_HIDDEN); + + position1.SetPosition(entity_pos_t::FromInt(118), entity_pos_t::FromDouble(100)); + { CMessagePositionChanged msg(entityId, true, entity_pos_t::FromInt(118), entity_pos_t::FromDouble(100), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } + // Both players can see both entities + TS_ASSERT_EQUALS(cmp->GetLosVisibility(handle1, playerId2), ICmpRangeManager::VIS_VISIBLE); + TS_ASSERT_EQUALS(cmp->GetLosVisibility(handle2, playerId), ICmpRangeManager::VIS_VISIBLE); + } + // TODO It would be nice to call Verify() with the shore revealing system // but that means testing on an actual map, with water and land. void test_basic() { + ComponentTestHelper test(g_ScriptRuntime); ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); MockVision vision; + vision.SetRange(entity_pos_t::FromInt(64)); test.AddMock(100, IID_Vision, vision); MockPosition position; test.AddMock(100, IID_Position, position); + MockFootprint footprint; + footprint.SetHeight(entity_pos_t::FromInt(5)); + test.AddMock(100, IID_Footprint, footprint); + + CTerrain mTerrain; + MockFullTerrain terrain; + terrain.SetTerrain(&mTerrain); + // Flat terrain + mTerrain.Initialize(8, nullptr); + if (mTerrain.GetVertexGroundLevel(61, 64) > 0) + TS_FAIL("terrain get vertex ground level"); + test.AddMock(SYSTEM_ENTITY, IID_Terrain, terrain); + terrain.Init(CParamNode()); + + if (!terrain.GetCTerrain()) + TS_FAIL("GetCTerrain returns null"); + + MockWater water; + test.AddMock(SYSTEM_ENTITY, IID_WaterManager, water); + // This tests that the incremental computation produces the correct result // in various edge cases @@ -152,5 +602,7 @@ previousOwner = newOwner; } } + + terrain.Deinit(); } };