Index: ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp (revision 27927) +++ ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp (revision 27928) @@ -1,862 +1,872 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpTerritoryManager.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "graphics/TerritoryBoundary.h" #include "maths/MathUtil.h" #include "ps/Profile.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "renderer/Scene.h" #include "renderer/TerrainOverlay.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpTerritoryDecayManager.h" #include "simulation2/components/ICmpTerritoryInfluence.h" #include "simulation2/helpers/Grid.h" #include "simulation2/helpers/Render.h" #include class CCmpTerritoryManager; class TerritoryOverlay final : public TerrainTextureOverlay { NONCOPYABLE(TerritoryOverlay); public: CCmpTerritoryManager& m_TerritoryManager; TerritoryOverlay(CCmpTerritoryManager& manager); void BuildTextureRGBA(u8* data, size_t w, size_t h) override; }; class CCmpTerritoryManager : public ICmpTerritoryManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); componentManager.SubscribeGloballyToMessageType(MT_PlayerColorChanged); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); componentManager.SubscribeGloballyToMessageType(MT_ValueModification); componentManager.SubscribeToMessageType(MT_ObstructionMapShapeChanged); componentManager.SubscribeToMessageType(MT_TerrainChanged); componentManager.SubscribeToMessageType(MT_WaterChanged); componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeToMessageType(MT_Interpolate); componentManager.SubscribeToMessageType(MT_RenderSubmit); } DEFAULT_COMPONENT_ALLOCATOR(TerritoryManager) static std::string GetSchema() { return ""; } u8 m_ImpassableCost; float m_BorderThickness; float m_BorderSeparation; // Player ID in bits 0-4 (TERRITORY_PLAYER_MASK) // connected flag in bit 5 (TERRITORY_CONNECTED_MASK) // blinking flag in bit 6 (TERRITORY_BLINKING_MASK) // processed flag in bit 7 (TERRITORY_PROCESSED_MASK) Grid* m_Territories; std::vector m_TerritoryCellCounts; u16 m_TerritoryTotalPassableCellCount; // Saves the cost per tile (to stop territory on impassable tiles) Grid* m_CostGrid; // Set to true when territories change; will send a TerritoriesChanged message // during the Update phase bool m_TriggerEvent; struct SBoundaryLine { bool blinking; player_id_t owner; CColor color; SOverlayTexturedLine overlay; }; std::vector m_BoundaryLines; bool m_BoundaryLinesDirty; double m_AnimTime; // time since start of rendering, in seconds TerritoryOverlay* m_DebugOverlay; bool m_EnableLineDebugOverlays; ///< Enable node debugging overlays for boundary lines? std::vector m_DebugBoundaryLineNodes; void Init(const CParamNode& UNUSED(paramNode)) override { m_Territories = NULL; m_CostGrid = NULL; m_DebugOverlay = NULL; // m_DebugOverlay = new TerritoryOverlay(*this); m_BoundaryLinesDirty = true; m_TriggerEvent = true; m_EnableLineDebugOverlays = false; m_DirtyID = 1; m_DirtyBlinkingID = 1; m_Visible = true; m_ColorChanged = false; m_AnimTime = 0.0; m_TerritoryTotalPassableCellCount = 0; // Register Relax NG validator CXeromyces::AddValidator(g_VFS, "territorymanager", "simulation/data/territorymanager.rng"); CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/territorymanager.xml", "territorymanager"); int impassableCost = externalParamNode.GetChild("TerritoryManager").GetChild("ImpassableCost").ToInt(); ENSURE(0 <= impassableCost && impassableCost <= 255); m_ImpassableCost = (u8)impassableCost; m_BorderThickness = externalParamNode.GetChild("TerritoryManager").GetChild("BorderThickness").ToFixed().ToFloat(); m_BorderSeparation = externalParamNode.GetChild("TerritoryManager").GetChild("BorderSeparation").ToFixed().ToFloat(); } void Deinit() override { SAFE_DELETE(m_Territories); SAFE_DELETE(m_CostGrid); SAFE_DELETE(m_DebugOverlay); } void Serialize(ISerializer& serialize) override { // Territory state can be recomputed as required, so we don't need to serialize any of it. serialize.Bool("trigger event", m_TriggerEvent); } void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override { Init(paramNode); deserialize.Bool("trigger event", m_TriggerEvent); } void HandleMessage(const CMessage& msg, bool UNUSED(global)) override { switch (msg.GetType()) { case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); MakeDirtyIfRelevantEntity(msgData.entity); break; } case MT_PlayerColorChanged: { MakeDirty(); break; } case MT_PositionChanged: { const CMessagePositionChanged& msgData = static_cast (msg); MakeDirtyIfRelevantEntity(msgData.entity); break; } case MT_ValueModification: { const CMessageValueModification& msgData = static_cast (msg); if (msgData.component == L"TerritoryInfluence") MakeDirty(); break; } case MT_ObstructionMapShapeChanged: case MT_TerrainChanged: case MT_WaterChanged: { // also recalculate the cost grid to support atlas changes SAFE_DELETE(m_CostGrid); MakeDirty(); break; } case MT_Update: { if (m_TriggerEvent) { m_TriggerEvent = false; GetSimContext().GetComponentManager().BroadcastMessage(CMessageTerritoriesChanged()); } break; } case MT_Interpolate: { const CMessageInterpolate& msgData = static_cast (msg); Interpolate(msgData.deltaSimTime, msgData.offset); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector, msgData.frustum, msgData.culling); break; } } } // Check whether the entity is either a settlement or territory influence; // ignore any others void MakeDirtyIfRelevantEntity(entity_id_t ent) { CmpPtr cmpTerritoryInfluence(GetSimContext(), ent); if (cmpTerritoryInfluence) MakeDirty(); } const Grid& GetTerritoryGrid() override { CalculateTerritories(); ENSURE(m_Territories); return *m_Territories; } player_id_t GetOwner(entity_pos_t x, entity_pos_t z) override; std::vector GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected) override; bool IsConnected(entity_pos_t x, entity_pos_t z) override; void SetTerritoryBlinking(entity_pos_t x, entity_pos_t z, bool enable) override; bool IsTerritoryBlinking(entity_pos_t x, entity_pos_t z) override; // To support lazy updates of territory render data, // we maintain a DirtyID here and increment it whenever territories change; // if a caller has a lower DirtyID then it needs to be updated. // We also do the same thing for blinking updates using DirtyBlinkingID. size_t m_DirtyID; size_t m_DirtyBlinkingID; bool m_ColorChanged; void MakeDirty() { SAFE_DELETE(m_Territories); ++m_DirtyID; m_BoundaryLinesDirty = true; m_TriggerEvent = true; } bool NeedUpdateTexture(size_t* dirtyID) override { if (*dirtyID == m_DirtyID && !m_ColorChanged) return false; *dirtyID = m_DirtyID; m_ColorChanged = false; return true; } bool NeedUpdateAI(size_t* dirtyID, size_t* dirtyBlinkingID) const override { if (*dirtyID == m_DirtyID && *dirtyBlinkingID == m_DirtyBlinkingID) return false; *dirtyID = m_DirtyID; *dirtyBlinkingID = m_DirtyBlinkingID; return true; } void CalculateCostGrid(); void CalculateTerritories(); u8 GetTerritoryPercentage(player_id_t player) override; std::vector ComputeBoundaries(); void UpdateBoundaryLines(); void Interpolate(float frameTime, float frameOffset); void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); void SetVisibility(bool visible) override { m_Visible = visible; } void UpdateColors() override; private: bool m_Visible; }; REGISTER_COMPONENT_TYPE(TerritoryManager) // Tile data type, for easier accessing of coordinates struct Tile { Tile(u16 i, u16 j) : x(i), z(j) { } u16 x, z; }; // Floodfill templates that expand neighbours from a certain source onwards // (posX, posZ) are the coordinates of the currently expanded tile // (nx, nz) are the coordinates of the current neighbour handled // The user of this floodfill should use "continue" on every neighbour that // shouldn't be expanded on its own. (without continue, an infinite loop will happen) # define FLOODFILL(i, j, code)\ do {\ const int NUM_NEIGHBOURS = 8;\ const int NEIGHBOURS_X[NUM_NEIGHBOURS] = {1,-1, 0, 0, 1,-1, 1,-1};\ const int NEIGHBOURS_Z[NUM_NEIGHBOURS] = {0, 0, 1,-1, 1,-1,-1, 1};\ std::queue openTiles;\ openTiles.emplace(i, j);\ while (!openTiles.empty())\ {\ u16 posX = openTiles.front().x;\ u16 posZ = openTiles.front().z;\ openTiles.pop();\ for (int n = 0; n < NUM_NEIGHBOURS; ++n)\ {\ u16 nx = posX + NEIGHBOURS_X[n];\ u16 nz = posZ + NEIGHBOURS_Z[n];\ /* Check the bounds, underflow will cause the values to be big again */\ if (nx >= tilesW || nz >= tilesH)\ continue;\ code\ openTiles.emplace(nx, nz);\ }\ }\ }\ while (false) /** * Compute the tile indexes on the grid nearest to a given point */ static void NearestTerritoryTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h) { entity_pos_t scale = Pathfinding::NAVCELL_SIZE * ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE; i = Clamp((x / scale).ToInt_RoundToNegInfinity(), 0, w - 1); j = Clamp((z / scale).ToInt_RoundToNegInfinity(), 0, h - 1); } void CCmpTerritoryManager::CalculateCostGrid() { if (m_CostGrid) return; CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; pass_class_t passClassTerritory = cmpPathfinder->GetPassabilityClass("default-terrain-only"); pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted"); const Grid& passGrid = cmpPathfinder->GetPassabilityGrid(); int tilesW = passGrid.m_W / NAVCELLS_PER_TERRITORY_TILE; int tilesH = passGrid.m_H / NAVCELLS_PER_TERRITORY_TILE; m_CostGrid = new Grid(tilesW, tilesH); m_TerritoryTotalPassableCellCount = 0; for (int i = 0; i < tilesW; ++i) { for (int j = 0; j < tilesH; ++j) { NavcellData c = 0; for (u16 di = 0; di < NAVCELLS_PER_TERRITORY_TILE; ++di) for (u16 dj = 0; dj < NAVCELLS_PER_TERRITORY_TILE; ++dj) c |= passGrid.get( i * NAVCELLS_PER_TERRITORY_TILE + di, j * NAVCELLS_PER_TERRITORY_TILE + dj); if (!IS_PASSABLE(c, passClassTerritory)) m_CostGrid->set(i, j, m_ImpassableCost); else if (!IS_PASSABLE(c, passClassUnrestricted)) m_CostGrid->set(i, j, 255); // off the world; use maximum cost else { m_CostGrid->set(i, j, 1); ++m_TerritoryTotalPassableCellCount; } } } } void CCmpTerritoryManager::CalculateTerritories() { if (m_Territories) return; PROFILE("CalculateTerritories"); // If the pathfinder hasn't been loaded (e.g. this is called during map initialisation), // abort the computation (and assume callers can cope with m_Territories == NULL) CalculateCostGrid(); if (!m_CostGrid) return; const u16 tilesW = m_CostGrid->m_W; const u16 tilesH = m_CostGrid->m_H; m_Territories = new Grid(tilesW, tilesH); // Reset territory counts for all players CmpPtr cmpPlayerManager(GetSystemEntity()); if (cmpPlayerManager && (size_t)cmpPlayerManager->GetNumPlayers() != m_TerritoryCellCounts.size()) m_TerritoryCellCounts.resize(cmpPlayerManager->GetNumPlayers()); for (u16& count : m_TerritoryCellCounts) count = 0; // Find all territory influence entities CComponentManager::InterfaceList influences = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_TerritoryInfluence); // Split influence entities into per-player lists, ignoring any with invalid properties std::map > influenceEntities; for (const CComponentManager::InterfacePair& pair : influences) { entity_id_t ent = pair.first; CmpPtr cmpOwnership(GetSimContext(), ent); if (!cmpOwnership) continue; // Ignore Gaia and unassigned or players we can't represent player_id_t owner = cmpOwnership->GetOwner(); if (owner <= 0 || owner > TERRITORY_PLAYER_MASK) continue; influenceEntities[owner].push_back(ent); } // Store the overall best weight for comparison Grid bestWeightGrid(tilesW, tilesH); // store the root influences to mark territory as connected std::vector rootInfluenceEntities; for (const std::pair>& pair : influenceEntities) { // entityGrid stores the weight for a single entity, and is reset per entity Grid entityGrid(tilesW, tilesH); // playerGrid stores the combined weight of all entities for this player Grid playerGrid(tilesW, tilesH); u8 owner = static_cast(pair.first); const std::vector& ents = pair.second; // With 2^16 entities, we're safe against overflows as the weight is also limited to 2^16 ENSURE(ents.size() < 1 << 16); // Compute the influence map of the current entity, then add it to the player grid for (entity_id_t ent : ents) { CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition || !cmpPosition->IsInWorld()) continue; CmpPtr cmpTerritoryInfluence(GetSimContext(), ent); u32 weight = cmpTerritoryInfluence->GetWeight(); u32 radius = cmpTerritoryInfluence->GetRadius(); if (weight == 0 || radius == 0) continue; u32 falloff = weight * (Pathfinding::NAVCELL_SIZE * NAVCELLS_PER_TERRITORY_TILE).ToInt_RoundToNegInfinity() / radius; CFixedVector2D pos = cmpPosition->GetPosition2D(); u16 i, j; NearestTerritoryTile(pos.X, pos.Y, i, j, tilesW, tilesH); if (cmpTerritoryInfluence->IsRoot()) rootInfluenceEntities.push_back(ent); // Initialise the tile under the entity entityGrid.set(i, j, weight); if (weight > bestWeightGrid.get(i, j)) { bestWeightGrid.set(i, j, weight); m_Territories->set(i, j, owner); } // Expand influences outwards FLOODFILL(i, j, u32 dg = falloff * m_CostGrid->get(nx, nz); // diagonal neighbour -> multiply with approx sqrt(2) if (nx != posX && nz != posZ) dg = (dg * 362) / 256; // Don't expand if new cost is not better than previous value for that tile // (arranged to avoid underflow if entityGrid.get(x, z) < dg) if (entityGrid.get(posX, posZ) <= entityGrid.get(nx, nz) + dg) continue; // weight of this tile = weight of predecessor - falloff from predecessor u32 newWeight = entityGrid.get(posX, posZ) - dg; u32 totalWeight = playerGrid.get(nx, nz) - entityGrid.get(nx, nz) + newWeight; playerGrid.set(nx, nz, totalWeight); entityGrid.set(nx, nz, newWeight); // if this weight is better than the best thus far, set the owner if (totalWeight > bestWeightGrid.get(nx, nz)) { bestWeightGrid.set(nx, nz, totalWeight); m_Territories->set(nx, nz, owner); } ); entityGrid.reset(); } } // Detect territories connected to a 'root' influence (typically a civ center) // belonging to their player, and mark them with the connected flag for (entity_id_t ent : rootInfluenceEntities) { // (These components must be valid else the entities wouldn't be added to this list) CmpPtr cmpOwnership(GetSimContext(), ent); CmpPtr cmpPosition(GetSimContext(), ent); CFixedVector2D pos = cmpPosition->GetPosition2D(); u16 i, j; NearestTerritoryTile(pos.X, pos.Y, i, j, tilesW, tilesH); u8 owner = (u8)cmpOwnership->GetOwner(); if (m_Territories->get(i, j) != owner) continue; m_Territories->set(i, j, owner | TERRITORY_CONNECTED_MASK); + if (m_CostGrid->get(i, j) < m_ImpassableCost) + ++m_TerritoryCellCounts[owner]; + FLOODFILL(i, j, // Don't expand non-owner tiles, or tiles that already have a connected mask if (m_Territories->get(nx, nz) != owner) continue; m_Territories->set(nx, nz, owner | TERRITORY_CONNECTED_MASK); if (m_CostGrid->get(nx, nz) < m_ImpassableCost) ++m_TerritoryCellCounts[owner]; ); } // Then recomputes the blinking tiles CmpPtr cmpTerritoryDecayManager(GetSystemEntity()); if (cmpTerritoryDecayManager) { size_t dirtyBlinkingID = m_DirtyBlinkingID; cmpTerritoryDecayManager->SetBlinkingEntities(); m_DirtyBlinkingID = dirtyBlinkingID; } } std::vector CCmpTerritoryManager::ComputeBoundaries() { PROFILE("ComputeBoundaries"); CalculateTerritories(); ENSURE(m_Territories); return CTerritoryBoundaryCalculator::ComputeBoundaries(m_Territories); } u8 CCmpTerritoryManager::GetTerritoryPercentage(player_id_t player) { - if (player <= 0 || static_cast(player) >= m_TerritoryCellCounts.size()) + if (player <= 0 || (m_Territories && static_cast(player) >= m_TerritoryCellCounts.size())) return 0; CalculateTerritories(); // Territories may have been recalculated, check whether player is still there. if (m_TerritoryTotalPassableCellCount == 0 || static_cast(player) >= m_TerritoryCellCounts.size()) return 0; u8 percentage = (m_TerritoryCellCounts[player] * 100) / m_TerritoryTotalPassableCellCount; ENSURE(percentage <= 100); return percentage; } void CCmpTerritoryManager::UpdateBoundaryLines() { PROFILE("update boundary lines"); m_BoundaryLines.clear(); m_DebugBoundaryLineNodes.clear(); if (!CRenderer::IsInitialised()) return; std::vector boundaries = ComputeBoundaries(); CTextureProperties texturePropsBase("art/textures/misc/territory_border.png"); texturePropsBase.SetAddressMode( Renderer::Backend::Sampler::AddressMode::CLAMP_TO_BORDER, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE); texturePropsBase.SetAnisotropicFilter(true); CTexturePtr textureBase = g_Renderer.GetTextureManager().CreateTexture(texturePropsBase); CTextureProperties texturePropsMask("art/textures/misc/territory_border_mask.png"); texturePropsMask.SetAddressMode( Renderer::Backend::Sampler::AddressMode::CLAMP_TO_BORDER, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE); texturePropsMask.SetAnisotropicFilter(true); CTexturePtr textureMask = g_Renderer.GetTextureManager().CreateTexture(texturePropsMask); CmpPtr cmpPlayerManager(GetSystemEntity()); if (!cmpPlayerManager) return; for (size_t i = 0; i < boundaries.size(); ++i) { if (boundaries[i].points.empty()) continue; CColor color(1, 0, 1, 1); CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(boundaries[i].owner)); if (cmpPlayer) color = cmpPlayer->GetDisplayedColor(); m_BoundaryLines.push_back(SBoundaryLine()); m_BoundaryLines.back().blinking = boundaries[i].blinking; m_BoundaryLines.back().owner = boundaries[i].owner; m_BoundaryLines.back().color = color; m_BoundaryLines.back().overlay.m_SimContext = &GetSimContext(); m_BoundaryLines.back().overlay.m_TextureBase = textureBase; m_BoundaryLines.back().overlay.m_TextureMask = textureMask; m_BoundaryLines.back().overlay.m_Color = color; m_BoundaryLines.back().overlay.m_Thickness = m_BorderThickness; m_BoundaryLines.back().overlay.m_Closed = true; SimRender::SmoothPointsAverage(boundaries[i].points, m_BoundaryLines.back().overlay.m_Closed); SimRender::InterpolatePointsRNS(boundaries[i].points, m_BoundaryLines.back().overlay.m_Closed, m_BorderSeparation); std::vector& points = m_BoundaryLines.back().overlay.m_Coords; for (size_t j = 0; j < boundaries[i].points.size(); ++j) { points.push_back(boundaries[i].points[j]); if (m_EnableLineDebugOverlays) { const size_t numHighlightNodes = 7; // highlight the X last nodes on either end to see where they meet (if closed) SOverlayLine overlayNode; if (j > boundaries[i].points.size() - 1 - numHighlightNodes) overlayNode.m_Color = CColor(1.f, 0.f, 0.f, 1.f); else if (j < numHighlightNodes) overlayNode.m_Color = CColor(0.f, 1.f, 0.f, 1.f); else overlayNode.m_Color = CColor(1.0f, 1.0f, 1.0f, 1.0f); overlayNode.m_Thickness = 0.1f; SimRender::ConstructCircleOnGround(GetSimContext(), boundaries[i].points[j].X, boundaries[i].points[j].Y, 0.1f, overlayNode, true); m_DebugBoundaryLineNodes.push_back(overlayNode); } } } } void CCmpTerritoryManager::Interpolate(float frameTime, float UNUSED(frameOffset)) { m_AnimTime += frameTime; if (m_BoundaryLinesDirty) { UpdateBoundaryLines(); m_BoundaryLinesDirty = false; } for (size_t i = 0; i < m_BoundaryLines.size(); ++i) { if (m_BoundaryLines[i].blinking) { CColor c = m_BoundaryLines[i].color; c.a *= 0.2f + 0.8f * fabsf((float)cos(m_AnimTime * M_PI)); // TODO: should let artists tweak this m_BoundaryLines[i].overlay.m_Color = c; } } } void CCmpTerritoryManager::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { if (!m_Visible) return; for (size_t i = 0; i < m_BoundaryLines.size(); ++i) { if (culling && !m_BoundaryLines[i].overlay.IsVisibleInFrustum(frustum)) continue; collector.Submit(&m_BoundaryLines[i].overlay); } for (size_t i = 0; i < m_DebugBoundaryLineNodes.size(); ++i) collector.Submit(&m_DebugBoundaryLineNodes[i]); } player_id_t CCmpTerritoryManager::GetOwner(entity_pos_t x, entity_pos_t z) { u16 i, j; if (!m_Territories) { CalculateTerritories(); if (!m_Territories) return 0; } NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); return m_Territories->get(i, j) & TERRITORY_PLAYER_MASK; } std::vector CCmpTerritoryManager::GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected) { CmpPtr cmpPlayerManager(GetSystemEntity()); if (!cmpPlayerManager) return std::vector(); std::vector ret(cmpPlayerManager->GetNumPlayers(), 0); CalculateTerritories(); if (!m_Territories) return ret; u16 i, j; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); // calculate the neighbours player_id_t thisOwner = m_Territories->get(i, j) & TERRITORY_PLAYER_MASK; u16 tilesW = m_Territories->m_W; u16 tilesH = m_Territories->m_H; // use a flood-fill algorithm that fills up to the borders and remembers the owners Grid markerGrid(tilesW, tilesH); markerGrid.set(i, j, true); FLOODFILL(i, j, if (markerGrid.get(nx, nz)) continue; // mark the tile as visited in any case markerGrid.set(nx, nz, true); int owner = m_Territories->get(nx, nz) & TERRITORY_PLAYER_MASK; if (owner != thisOwner) { if (owner == 0 || !filterConnected || (m_Territories->get(nx, nz) & TERRITORY_CONNECTED_MASK) != 0) ret[owner]++; // add player to the neighbour list when requested continue; // don't expand non-owner tiles further } ); return ret; } bool CCmpTerritoryManager::IsConnected(entity_pos_t x, entity_pos_t z) { u16 i, j; CalculateTerritories(); if (!m_Territories) return false; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); return (m_Territories->get(i, j) & TERRITORY_CONNECTED_MASK) != 0; } void CCmpTerritoryManager::SetTerritoryBlinking(entity_pos_t x, entity_pos_t z, bool enable) { CalculateTerritories(); if (!m_Territories) return; u16 i, j; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); u16 tilesW = m_Territories->m_W; u16 tilesH = m_Territories->m_H; player_id_t thisOwner = m_Territories->get(i, j) & TERRITORY_PLAYER_MASK; + u8 bitmask = m_Territories->get(i, j); + u8 blinking = bitmask & TERRITORY_BLINKING_MASK; + if (enable && !blinking) + m_Territories->set(i, j, bitmask | TERRITORY_BLINKING_MASK); + else if (!enable && blinking) + m_Territories->set(i, j, bitmask & ~TERRITORY_BLINKING_MASK); + FLOODFILL(i, j, - u8 bitmask = m_Territories->get(nx, nz); + bitmask = m_Territories->get(nx, nz); if ((bitmask & TERRITORY_PLAYER_MASK) != thisOwner) continue; - u8 blinking = bitmask & TERRITORY_BLINKING_MASK; + blinking = bitmask & TERRITORY_BLINKING_MASK; if (enable && !blinking) m_Territories->set(nx, nz, bitmask | TERRITORY_BLINKING_MASK); else if (!enable && blinking) m_Territories->set(nx, nz, bitmask & ~TERRITORY_BLINKING_MASK); else continue; ); ++m_DirtyBlinkingID; m_BoundaryLinesDirty = true; } bool CCmpTerritoryManager::IsTerritoryBlinking(entity_pos_t x, entity_pos_t z) { CalculateTerritories(); if (!m_Territories) return false; u16 i, j; NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); return (m_Territories->get(i, j) & TERRITORY_BLINKING_MASK) != 0; } void CCmpTerritoryManager::UpdateColors() { m_ColorChanged = true; CmpPtr cmpPlayerManager(GetSystemEntity()); if (!cmpPlayerManager) return; for (SBoundaryLine& boundaryLine : m_BoundaryLines) { CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(boundaryLine.owner)); if (!cmpPlayer) continue; boundaryLine.color = cmpPlayer->GetDisplayedColor(); boundaryLine.overlay.m_Color = boundaryLine.color; } } TerritoryOverlay::TerritoryOverlay(CCmpTerritoryManager& manager) : TerrainTextureOverlay((float)Pathfinding::NAVCELLS_PER_TERRAIN_TILE / ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE), m_TerritoryManager(manager) { } void TerritoryOverlay::BuildTextureRGBA(u8* data, size_t w, size_t h) { for (size_t j = 0; j < h; ++j) { for (size_t i = 0; i < w; ++i) { SColor4ub color; u8 id = (m_TerritoryManager.m_Territories->get((int)i, (int)j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK); color = GetColor(id, 64); *data++ = color.R; *data++ = color.G; *data++ = color.B; *data++ = color.A; } } } #undef FLOODFILL Index: ps/trunk/source/simulation2/components/tests/test_TerritoryManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_TerritoryManager.h (revision 27927) +++ ps/trunk/source/simulation2/components/tests/test_TerritoryManager.h (revision 27928) @@ -1,291 +1,453 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "simulation2/system/ComponentTest.h" +#include "maths/Matrix3D.h" #include "ps/CStr.h" #include "graphics/Terrain.h" #include "graphics/TerritoryBoundary.h" #include "simulation2/helpers/Grid.h" #include "simulation2/components/ICmpTerritoryManager.h" +#include "simulation2/components/ICmpPlayerManager.h" +#include "simulation2/components/ICmpTerritoryInfluence.h" +#include "simulation2/components/ICmpOwnership.h" + +class MockPathfinderTerrMan : public ICmpPathfinder +{ +public: + DEFAULT_MOCK_COMPONENT() + + // Test data + Grid m_PassabilityGrid; + + virtual pass_class_t GetPassabilityClass(const std::string&) const override { return 0; } + virtual const Grid& GetPassabilityGrid() override { return m_PassabilityGrid; } + + // Irrelevant part of the mock. + virtual void GetPassabilityClasses(std::map&) const override {} + virtual void GetPassabilityClasses(std::map&, std::map&) const override {} + virtual entity_pos_t GetClearance(pass_class_t) const override { return entity_pos_t::FromInt(1); } + virtual entity_pos_t GetMaximumClearance() const override { return entity_pos_t::FromInt(1); } + virtual const GridUpdateInformation& GetAIPathfinderDirtinessInformation() const override { static GridUpdateInformation gridInfo; return gridInfo; } + virtual void FlushAIPathfinderDirtinessInformation() override {} + virtual Grid ComputeShoreGrid(bool = false) override { return Grid {}; } + virtual u32 ComputePathAsync(entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t, entity_id_t) override { return 1; } + virtual void ComputePathImmediate(entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t, WaypointPath&) const override {} + virtual u32 ComputeShortPathAsync(entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t, bool, entity_id_t, entity_id_t) override { return 1; } + virtual WaypointPath ComputeShortPathImmediate(const ShortPathRequest&) const override { return WaypointPath(); } + virtual void SetDebugPath(entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t) override {} + virtual bool IsGoalReachable(entity_pos_t, entity_pos_t, const PathGoal&, pass_class_t) override { return false; } + virtual bool CheckMovement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, pass_class_t) const override { return false; } + virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, pass_class_t, bool = false) const override { return ICmpObstruction::FOUNDATION_CHECK_SUCCESS; } + virtual ICmpObstruction::EFoundationCheck CheckBuildingPlacement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_id_t, pass_class_t) const override { return ICmpObstruction::FOUNDATION_CHECK_SUCCESS; } + virtual ICmpObstruction::EFoundationCheck CheckBuildingPlacement(const IObstructionTestFilter&, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t, entity_id_t, pass_class_t, bool) const override { return ICmpObstruction::FOUNDATION_CHECK_SUCCESS; } + virtual void SetDebugOverlay(bool) override {} + virtual void SetHierDebugOverlay(bool) override {} + virtual void SendRequestedPaths() override {} + virtual void StartProcessingMoves(bool) override {} + virtual void UpdateGrid() override {} + virtual void GetDebugData(u32&, double&, Grid&) const override {} + virtual void SetAtlasOverlay(bool, pass_class_t = 0) override {} +}; + +class MockPlayerMgrTerrMan : public ICmpPlayerManager +{ +public: + DEFAULT_MOCK_COMPONENT() + + int32_t GetNumPlayers() override { return 2; } + entity_id_t GetPlayerByID(int32_t id) override { return id + 1; } +}; + +class MockTerrInfTerrMan : public ICmpTerritoryInfluence +{ +public: + DEFAULT_MOCK_COMPONENT() + + bool IsRoot() const override { return true; }; + u16 GetWeight() const override { return 10; }; + u32 GetRadius() const override { return m_Radius; }; + + u32 m_Radius = 0; +}; + +class MockOwnershipTerrMan : public ICmpOwnership +{ +public: + DEFAULT_MOCK_COMPONENT() + + player_id_t GetOwner() const override { return 1; }; + void SetOwner(player_id_t) override {}; + void SetOwnerQuiet(player_id_t) override {}; +}; + +class MockPositionTerrMan : public ICmpPosition +{ +public: + DEFAULT_MOCK_COMPONENT() + + void SetTurretParent(entity_id_t, const CFixedVector3D&) override {} + entity_id_t GetTurretParent() const override { return INVALID_ENTITY; } + void UpdateTurretPosition() override {} + std::set* GetTurrets() override { return nullptr; } + bool IsInWorld() const override { return true; } + void MoveOutOfWorld() override {} + void MoveTo(entity_pos_t, entity_pos_t) override {} + void MoveAndTurnTo(entity_pos_t, entity_pos_t, entity_angle_t) override {} + void JumpTo(entity_pos_t, entity_pos_t) override {} + void SetHeightOffset(entity_pos_t) override {} + entity_pos_t GetHeightOffset() const override { return entity_pos_t::Zero(); } + void SetHeightFixed(entity_pos_t) override {} + entity_pos_t GetHeightFixed() const override { return entity_pos_t::Zero(); } + entity_pos_t GetHeightAtFixed(entity_pos_t, entity_pos_t) const override { return entity_pos_t::Zero(); } + bool IsHeightRelative() const override { return true; } + void SetHeightRelative(bool) override {} + bool CanFloat() const override { return false; } + void SetFloating(bool) override {} + void SetActorFloating(bool) override {} + void SetConstructionProgress(fixed) override {} + CFixedVector3D GetPosition() const override { return m_Pos; } + CFixedVector2D GetPosition2D() const override { return CFixedVector2D(m_Pos.X, m_Pos.Z); } + CFixedVector3D GetPreviousPosition() const override { return CFixedVector3D(); } + CFixedVector2D GetPreviousPosition2D() const override { return CFixedVector2D(); } + fixed GetTurnRate() const override { return fixed::Zero(); } + void TurnTo(entity_angle_t) override {} + void SetYRotation(entity_angle_t) override {} + void SetXZRotation(entity_angle_t, entity_angle_t) override {} + CFixedVector3D GetRotation() const override { return CFixedVector3D(); } + fixed GetDistanceTravelled() const override { return fixed::Zero(); } + void GetInterpolatedPosition2D(float, float&, float&, float&) const override {} + CMatrix3D GetInterpolatedTransform(float) const override { return CMatrix3D(); } + + CFixedVector3D m_Pos; +}; + class TestCmpTerritoryManager : public CxxTest::TestSuite { public: void setUp() { CxxTest::setAbortTestOnFail(true); + g_VFS = CreateVfs(); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.sim" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); + CXeromyces::Startup(); } void tearDown() { + CXeromyces::Terminate(); + g_VFS.reset(); + DeleteDirectory(DataDir()/"_testcache"); + } + + // Regression test for D5181 / fix for rP27673 issue + void test_calculate_territories_uninitialised() + { + ComponentTestHelper test(g_ScriptContext); + ICmpTerritoryManager* cmp = test.Add(CID_TerritoryManager, "", SYSTEM_ENTITY); + + MockPathfinderTerrMan pathfinder; + test.AddMock(SYSTEM_ENTITY, IID_Pathfinder, pathfinder); + + MockPlayerMgrTerrMan playerMan; + test.AddMock(SYSTEM_ENTITY, IID_PlayerManager, playerMan); + pathfinder.m_PassabilityGrid.resize(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE * 5, ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE * 5); + + MockTerrInfTerrMan terrInf; + test.AddMock(5, IID_TerritoryInfluence, terrInf); + MockOwnershipTerrMan ownership; + test.AddMock(5, IID_Ownership, ownership); + MockPositionTerrMan position; + test.AddMock(5, IID_Position, position); + + position.m_Pos = CFixedVector3D( + entity_pos_t::FromInt(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE * 5 + ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE / 2), + entity_pos_t::FromInt(1), + entity_pos_t::FromInt(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE * 5 + ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE / 2) + ); + terrInf.m_Radius = 1; + + TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(0), 0); + TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(1), 4); // 5*5 = 25 -> 1 tile out of 25 is 4% + TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(2), 0); + + terrInf.m_Radius = ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE * 10; + + test.HandleMessage(cmp, CMessageTerrainChanged(0, 0, 0, 0), true); + + TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(0), 0); + TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(1), 100); + TS_ASSERT_EQUALS(cmp->GetTerritoryPercentage(2), 0); } void test_boundaries() { Grid grid = GetGrid("--------" "777777--" "777777--" "777777--" "--------", 8, 5); std::vector boundaries = CTerritoryBoundaryCalculator::ComputeBoundaries(&grid); TS_ASSERT_EQUALS(1U, boundaries.size()); TS_ASSERT_EQUALS(18U, boundaries[0].points.size()); // 2x6 + 2x3 TS_ASSERT_EQUALS((player_id_t)7, boundaries[0].owner); TS_ASSERT_EQUALS(false, boundaries[0].blinking); // high bits aren't set by GetGrid // assumes NAVCELLS_PER_TERRITORY_TILE is 8; dealt with in TestBoundaryPointsEqual int expectedPoints[][2] = {{ 4, 8}, {12, 8}, {20, 8}, {28, 8}, {36, 8}, {44, 8}, {48,12}, {48,20}, {48,28}, {44,32}, {36,32}, {28,32}, {20,32}, {12,32}, { 4,32}, { 0,28}, { 0,20}, { 0,12}}; TestBoundaryPointsEqual(boundaries[0].points, expectedPoints); } void test_nested_boundaries1() { // test case from ticket #918; contains single-tile territories with double borders Grid grid1 = GetGrid("--------" "-111111-" "-1-1213-" "-111111-" "--------", 8, 5); std::vector boundaries = CTerritoryBoundaryCalculator::ComputeBoundaries(&grid1); size_t expectedNumBoundaries = 5; TS_ASSERT_EQUALS(expectedNumBoundaries, boundaries.size()); STerritoryBoundary* onesOuter = NULL; STerritoryBoundary* onesInner0 = NULL; // inner border around the neutral tile STerritoryBoundary* onesInner2 = NULL; // inner border around the '2' tile STerritoryBoundary* twosOuter = NULL; STerritoryBoundary* threesOuter = NULL; // expected number of points (!) in the inner boundaries for terrain 1 (there are two with the same size) size_t onesInnerNumExpectedPoints = 4; for (size_t i=0; ipoints.size(), 20U); TS_ASSERT_EQUALS(onesInner0->points.size(), 4U); TS_ASSERT_EQUALS(onesInner2->points.size(), 4U); TS_ASSERT_EQUALS(twosOuter->points.size(), 4U); TS_ASSERT_EQUALS(threesOuter->points.size(), 4U); int onesOuterExpectedPoints[][2] = {{12, 8}, {20, 8}, {28, 8}, {36, 8}, {44, 8}, {52, 8}, {56,12}, {52,16}, {48,20}, {52,24}, {56,28}, {52,32}, {44,32}, {36,32}, {28,32}, {20,32}, {12,32}, { 8,28}, { 8,20}, { 8,12}}; int onesInner0ExpectedPoints[][2] = {{20,24}, {24,20}, {20,16}, {16,20}}; int onesInner2ExpectedPoints[][2] = {{36,24}, {40,20}, {36,16}, {32,20}}; int twosOuterExpectedPoints[][2] = {{36,16}, {40,20}, {36,24}, {32,20}}; int threesOuterExpectedPoints[][2] = {{52,16}, {56,20}, {52,24}, {48,20}}; TestBoundaryPointsEqual(onesOuter->points, onesOuterExpectedPoints); TestBoundaryPointsEqual(onesInner0->points, onesInner0ExpectedPoints); TestBoundaryPointsEqual(onesInner2->points, onesInner2ExpectedPoints); TestBoundaryPointsEqual(twosOuter->points, twosOuterExpectedPoints); TestBoundaryPointsEqual(threesOuter->points, threesOuterExpectedPoints); } void test_nested_boundaries2() { Grid grid1 = GetGrid("-22222-" "-2---2-" "-2-1123" "-2-1123" "-2-2223" "-222333", 7, 6); std::vector boundaries = CTerritoryBoundaryCalculator::ComputeBoundaries(&grid1); // There should be two boundaries found for the territory of 2's (one outer and one inner edge), plus two regular // outer edges of the territories of 1's and 3's. The order in which they're returned doesn't matter though, so // we should first detect which one is which. size_t expectedNumBoundaries = 4; TS_ASSERT_EQUALS(expectedNumBoundaries, boundaries.size()); STerritoryBoundary* onesOuter = NULL; STerritoryBoundary* twosOuter = NULL; STerritoryBoundary* twosInner = NULL; STerritoryBoundary* threesOuter = NULL; for (size_t i=0; i < expectedNumBoundaries; i++) { STerritoryBoundary& boundary = boundaries[i]; switch (boundary.owner) { case 1: TSM_ASSERT_EQUALS("Too many boundaries for territory owned by player 1", onesOuter, (STerritoryBoundary*) NULL); onesOuter = &boundary; break; case 3: TSM_ASSERT_EQUALS("Too many boundaries for territory owned by player 3", threesOuter, (STerritoryBoundary*) NULL); threesOuter = &boundary; break; case 2: // assign twosOuter first, then twosInner last; we'll swap them afterwards if needed if (twosOuter == NULL) twosOuter = &boundary; else if (twosInner == NULL) twosInner = &boundary; else TS_FAIL("Too many boundaries for territory owned by player 2"); break; default: TS_FAIL("Unexpected tile owner"); break; } } TS_ASSERT_DIFFERS(onesOuter, (STerritoryBoundary*) NULL); TS_ASSERT_DIFFERS(twosOuter, (STerritoryBoundary*) NULL); TS_ASSERT_DIFFERS(twosInner, (STerritoryBoundary*) NULL); TS_ASSERT_DIFFERS(threesOuter, (STerritoryBoundary*) NULL); TS_ASSERT_EQUALS(onesOuter->points.size(), 8U); TS_ASSERT_EQUALS(twosOuter->points.size(), 22U); TS_ASSERT_EQUALS(twosInner->points.size(), 14U); TS_ASSERT_EQUALS(threesOuter->points.size(), 14U); // See if we need to swap the outer and inner edges of the twos territories (uses the extremely simplistic // heuristic of comparing the amount of points to determine which one is the outer one and which one the inner // one (which does happen to work in this case though). if (twosOuter->points.size() < twosInner->points.size()) { STerritoryBoundary* tmp = twosOuter; twosOuter = twosInner; twosInner = tmp; } int onesOuterExpectedPoints[][2] = {{28,16}, {36,16}, {40,20}, {40,28}, {36,32}, {28,32}, {24,28}, {24,20}}; int twosOuterExpectedPoints[][2] = {{12, 0}, {20, 0}, {28, 0}, {32, 4}, {36, 8}, {44, 8}, {48,12}, {48,20}, {48,28}, {48,36}, {48,44}, {44,48}, {36,48}, {28,48}, {20,48}, {12,48}, { 8,44}, { 8,36}, { 8,28}, { 8,20}, { 8,12}, { 8, 4}}; int twosInnerExpectedPoints[][2] = {{20,40}, {28,40}, {36,40}, {40,36}, {40,28}, {40,20}, {36,16}, {28,16}, {24,12}, {20, 8}, {16,12}, {16,20}, {16,28}, {16,36}}; int threesOuterExpectedPoints[][2] = {{36, 0}, {44, 0}, {52, 0}, {56, 4}, {56,12}, {56,20}, {56,28}, {52,32}, {48,28}, {48,20}, {48,12}, {44, 8}, {36, 8}, {32, 4}}; TestBoundaryPointsEqual(onesOuter->points, onesOuterExpectedPoints); TestBoundaryPointsEqual(twosOuter->points, twosOuterExpectedPoints); TestBoundaryPointsEqual(twosInner->points, twosInnerExpectedPoints); TestBoundaryPointsEqual(threesOuter->points, threesOuterExpectedPoints); } private: /// Parses a string representation of a grid into an actual Grid structure, such that the (i,j) axes are located in the bottom /// left hand side of the map. Note: leaves all custom bits in the grid values at zero (anything outside /// ICmpTerritoryManager::TERRITORY_PLAYER_MASK). Grid GetGrid(const std::string& def, u16 w, u16 h) { Grid grid(w, h); const char* chars = def.c_str(); for (u16 y=0; y& points, int expectedPoints[][2]) { // TODO: currently relies on an exact point match, i.e. expectedPoints must be specified going CCW or CW (depending on // whether we're testing an inner or an outer edge) starting from the exact same point that the algorithm happened to // decide to start the run from. This is an algorithmic detail and is not considered to be part of the specification // of the return value. Hence, this method should also accept 'expectedPoints' to be a cyclically shifted // version of 'points', so that the starting position doesn't need to match exactly. for (size_t i = 0; i < points.size(); i++) { // the input numbers in expectedPoints are defined under the assumption that NAVCELLS_PER_TERRITORY_TILE is 8, so let's include // a scaling factor to protect against that should NAVCELLS_PER_TERRITORY_TILE ever change TS_ASSERT_DELTA(points[i].X, float(expectedPoints[i][0]) * 8.f / ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE, 1e-7); TS_ASSERT_DELTA(points[i].Y, float(expectedPoints[i][1]) * 8.f / ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE, 1e-7); } } };