/* Copyright (C) 2024 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;
};
/**
* Queue based eight directional floodfill algorithm.
*
* @param origin Where to start the floodfill. In the first iteration it is
* passed as the second argument to the @see decider and the floodfill only
* continues when the invocation returns @c true.
* @param gridSize Tiles outside the boundary are never exteded. The
* @see decider isn't called with thous tiles.
* @param decider It is called with a tile wich was already added as the first
* argument and a neighbour as the second argument. The invocation shall
* return whether to extend the wavefront to the neighbour (if allways
* @c true is returned, an infinite loop will occur). In the first iteration
* the @see decider is invoked with a null pointer as the first argument and
* @see origin as the second argument.
*/
template
void Floodfill(const Tile& origin, const Tile& gridSize, Decider decider)
{
static_assert(std::is_invocable_r_v);
constexpr std::array, 8> neighbours{{{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1},
{-1, -1}, {1, -1}, {-1, 1}}};
std::queue openTiles;
const auto emplaceIfRequested = [decider = std::move(decider), &openTiles](
const Tile* currentTile, const Tile& neighbourTile)
{
if (decider(currentTile, neighbourTile))
openTiles.emplace(neighbourTile);
};
emplaceIfRequested(nullptr, origin);
while (!openTiles.empty())
{
const Tile currentTile{openTiles.front()};
openTiles.pop();
for (const std::array& neighbour : neighbours)
{
const Tile neighbourTile{static_cast(currentTile.x + std::get<0>(neighbour)),
static_cast(currentTile.z + std::get<1>(neighbour))};
// Check the bounds, underflow will cause the values to be big again.
if (neighbourTile.x < gridSize.x && neighbourTile.z < gridSize.z)
emplaceIfRequested(¤tTile, neighbourTile);
}
}
}
/**
* 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);
const u32 originWeight = cmpTerritoryInfluence->GetWeight();
u32 radius = cmpTerritoryInfluence->GetRadius();
if (originWeight == 0 || radius == 0)
continue;
const u32 relativeFalloff = originWeight *
(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);
// Expand influences outwards
Floodfill({i, j}, {tilesW, tilesH}, [&](const Tile* current, const Tile& neighbour)
{
const bool diagonalProgression{current && neighbour.x != current->x &&
neighbour.z != current->z};
const u32 falloffPerTile{relativeFalloff *
m_CostGrid->get(neighbour.x, neighbour.z)};
// diagonal neighbour -> multiply with approx sqrt(2)
const u32 falloff{diagonalProgression ? (falloffPerTile * 362) / 256 :
falloffPerTile};
// Don't expand if new cost is not better than previous value for that tile
// (arranged to avoid underflow if entityGrid.get(x, z) < falloff)
if (current &&
entityGrid.get(current->x, current->z) <=
entityGrid.get(neighbour.x, neighbour.z) + falloff)
{
return false;
}
// weight of this tile = weight of predecessor - falloff from predecessor
const u32 weight{current ? entityGrid.get(current->x, current->z) - falloff :
originWeight};
const u32 totalWeight{weight + (current ?
playerGrid.get(neighbour.x, neighbour.z) -
entityGrid.get(neighbour.x, neighbour.z) : 0)};
playerGrid.set(neighbour.x, neighbour.z, totalWeight);
entityGrid.set(neighbour.x, neighbour.z, weight);
// if this weight is better than the best thus far, set the owner
if (totalWeight > bestWeightGrid.get(neighbour.x, neighbour.z))
{
bestWeightGrid.set(neighbour.x, neighbour.z, totalWeight);
m_Territories->set(neighbour.x, neighbour.z, owner);
}
return true;
});
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();
Floodfill({i, j}, {tilesW, tilesH}, [&](const Tile*, const Tile& neighbour)
{
// Don't expand non-owner tiles, or tiles that already have a connected mask
if (m_Territories->get(neighbour.x, neighbour.z) != owner)
return false;
m_Territories->set(neighbour.x, neighbour.z, owner | TERRITORY_CONNECTED_MASK);
if (m_CostGrid->get(neighbour.x, neighbour.z) < m_ImpassableCost)
++m_TerritoryCellCounts[owner];
return true;
});
}
// 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 || (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);
Floodfill({i, j}, {tilesW, tilesH}, [&](const Tile*, const Tile& neighbour)
{
if (markerGrid.get(neighbour.x, neighbour.z))
return false;
// mark the tile as visited in any case
markerGrid.set(neighbour.x, neighbour.z, true);
int owner = m_Territories->get(neighbour.x, neighbour.z) & TERRITORY_PLAYER_MASK;
if (owner != thisOwner)
{
if (owner == 0 || !filterConnected || (m_Territories->get(neighbour.x, neighbour.z) & TERRITORY_CONNECTED_MASK) != 0)
ret[owner]++; // add player to the neighbour list when requested
return false; // don't expand non-owner tiles further
}
return true;
});
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;
Floodfill({i, j}, {tilesW, tilesH}, [&](const Tile*, const Tile& neighbour)
{
const u8 bitmask{m_Territories->get(neighbour.x, neighbour.z)};
if ((bitmask & TERRITORY_PLAYER_MASK) != thisOwner)
return false;
const bool blinking{(bitmask & TERRITORY_BLINKING_MASK) != 0};
if (enable != blinking)
{
m_Territories->set(neighbour.x, neighbour.z, enable ?
bitmask | TERRITORY_BLINKING_MASK : bitmask & ~TERRITORY_BLINKING_MASK);
return true;
}
return false;
});
++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;
}
}
}