Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "simulation2/system/Component.h"
21 : #include "ICmpTerritoryManager.h"
22 :
23 : #include "graphics/Overlay.h"
24 : #include "graphics/Terrain.h"
25 : #include "graphics/TextureManager.h"
26 : #include "graphics/TerritoryBoundary.h"
27 : #include "maths/MathUtil.h"
28 : #include "ps/Profile.h"
29 : #include "ps/XML/Xeromyces.h"
30 : #include "renderer/Renderer.h"
31 : #include "renderer/Scene.h"
32 : #include "renderer/TerrainOverlay.h"
33 : #include "simulation2/MessageTypes.h"
34 : #include "simulation2/components/ICmpOwnership.h"
35 : #include "simulation2/components/ICmpPathfinder.h"
36 : #include "simulation2/components/ICmpPlayer.h"
37 : #include "simulation2/components/ICmpPlayerManager.h"
38 : #include "simulation2/components/ICmpPosition.h"
39 : #include "simulation2/components/ICmpTerritoryDecayManager.h"
40 : #include "simulation2/components/ICmpTerritoryInfluence.h"
41 : #include "simulation2/helpers/Grid.h"
42 : #include "simulation2/helpers/Render.h"
43 :
44 : #include <queue>
45 :
46 : class CCmpTerritoryManager;
47 :
48 0 : class TerritoryOverlay final : public TerrainTextureOverlay
49 : {
50 : NONCOPYABLE(TerritoryOverlay);
51 : public:
52 : CCmpTerritoryManager& m_TerritoryManager;
53 :
54 : TerritoryOverlay(CCmpTerritoryManager& manager);
55 : void BuildTextureRGBA(u8* data, size_t w, size_t h) override;
56 : };
57 :
58 9 : class CCmpTerritoryManager : public ICmpTerritoryManager
59 : {
60 : public:
61 116 : static void ClassInit(CComponentManager& componentManager)
62 : {
63 116 : componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged);
64 116 : componentManager.SubscribeGloballyToMessageType(MT_PlayerColorChanged);
65 116 : componentManager.SubscribeGloballyToMessageType(MT_PositionChanged);
66 116 : componentManager.SubscribeGloballyToMessageType(MT_ValueModification);
67 116 : componentManager.SubscribeToMessageType(MT_ObstructionMapShapeChanged);
68 116 : componentManager.SubscribeToMessageType(MT_TerrainChanged);
69 116 : componentManager.SubscribeToMessageType(MT_WaterChanged);
70 116 : componentManager.SubscribeToMessageType(MT_Update);
71 116 : componentManager.SubscribeToMessageType(MT_Interpolate);
72 116 : componentManager.SubscribeToMessageType(MT_RenderSubmit);
73 116 : }
74 :
75 6 : DEFAULT_COMPONENT_ALLOCATOR(TerritoryManager)
76 :
77 116 : static std::string GetSchema()
78 : {
79 116 : return "<a:component type='system'/><empty/>";
80 : }
81 :
82 : u8 m_ImpassableCost;
83 : float m_BorderThickness;
84 : float m_BorderSeparation;
85 :
86 : // Player ID in bits 0-4 (TERRITORY_PLAYER_MASK)
87 : // connected flag in bit 5 (TERRITORY_CONNECTED_MASK)
88 : // blinking flag in bit 6 (TERRITORY_BLINKING_MASK)
89 : // processed flag in bit 7 (TERRITORY_PROCESSED_MASK)
90 : Grid<u8>* m_Territories;
91 :
92 : std::vector<u16> m_TerritoryCellCounts;
93 : u16 m_TerritoryTotalPassableCellCount;
94 :
95 : // Saves the cost per tile (to stop territory on impassable tiles)
96 : Grid<u8>* m_CostGrid;
97 :
98 : // Set to true when territories change; will send a TerritoriesChanged message
99 : // during the Update phase
100 : bool m_TriggerEvent;
101 :
102 0 : struct SBoundaryLine
103 : {
104 : bool blinking;
105 : player_id_t owner;
106 : CColor color;
107 : SOverlayTexturedLine overlay;
108 : };
109 :
110 : std::vector<SBoundaryLine> m_BoundaryLines;
111 : bool m_BoundaryLinesDirty;
112 :
113 : double m_AnimTime; // time since start of rendering, in seconds
114 :
115 : TerritoryOverlay* m_DebugOverlay;
116 :
117 : bool m_EnableLineDebugOverlays; ///< Enable node debugging overlays for boundary lines?
118 : std::vector<SOverlayLine> m_DebugBoundaryLineNodes;
119 :
120 3 : void Init(const CParamNode& UNUSED(paramNode)) override
121 : {
122 3 : m_Territories = NULL;
123 3 : m_CostGrid = NULL;
124 3 : m_DebugOverlay = NULL;
125 : // m_DebugOverlay = new TerritoryOverlay(*this);
126 3 : m_BoundaryLinesDirty = true;
127 3 : m_TriggerEvent = true;
128 3 : m_EnableLineDebugOverlays = false;
129 3 : m_DirtyID = 1;
130 3 : m_DirtyBlinkingID = 1;
131 3 : m_Visible = true;
132 3 : m_ColorChanged = false;
133 :
134 3 : m_AnimTime = 0.0;
135 :
136 3 : m_TerritoryTotalPassableCellCount = 0;
137 :
138 : // Register Relax NG validator
139 3 : CXeromyces::AddValidator(g_VFS, "territorymanager", "simulation/data/territorymanager.rng");
140 :
141 6 : CParamNode externalParamNode;
142 3 : CParamNode::LoadXML(externalParamNode, L"simulation/data/territorymanager.xml", "territorymanager");
143 :
144 3 : int impassableCost = externalParamNode.GetChild("TerritoryManager").GetChild("ImpassableCost").ToInt();
145 3 : ENSURE(0 <= impassableCost && impassableCost <= 255);
146 3 : m_ImpassableCost = (u8)impassableCost;
147 3 : m_BorderThickness = externalParamNode.GetChild("TerritoryManager").GetChild("BorderThickness").ToFixed().ToFloat();
148 3 : m_BorderSeparation = externalParamNode.GetChild("TerritoryManager").GetChild("BorderSeparation").ToFixed().ToFloat();
149 3 : }
150 :
151 3 : void Deinit() override
152 : {
153 3 : SAFE_DELETE(m_Territories);
154 3 : SAFE_DELETE(m_CostGrid);
155 3 : SAFE_DELETE(m_DebugOverlay);
156 3 : }
157 :
158 0 : void Serialize(ISerializer& serialize) override
159 : {
160 : // Territory state can be recomputed as required, so we don't need to serialize any of it.
161 0 : serialize.Bool("trigger event", m_TriggerEvent);
162 0 : }
163 :
164 0 : void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override
165 : {
166 0 : Init(paramNode);
167 0 : deserialize.Bool("trigger event", m_TriggerEvent);
168 0 : }
169 :
170 0 : void HandleMessage(const CMessage& msg, bool UNUSED(global)) override
171 : {
172 0 : switch (msg.GetType())
173 : {
174 0 : case MT_OwnershipChanged:
175 : {
176 0 : const CMessageOwnershipChanged& msgData = static_cast<const CMessageOwnershipChanged&> (msg);
177 0 : MakeDirtyIfRelevantEntity(msgData.entity);
178 0 : break;
179 : }
180 0 : case MT_PlayerColorChanged:
181 : {
182 0 : MakeDirty();
183 0 : break;
184 : }
185 0 : case MT_PositionChanged:
186 : {
187 0 : const CMessagePositionChanged& msgData = static_cast<const CMessagePositionChanged&> (msg);
188 0 : MakeDirtyIfRelevantEntity(msgData.entity);
189 0 : break;
190 : }
191 0 : case MT_ValueModification:
192 : {
193 0 : const CMessageValueModification& msgData = static_cast<const CMessageValueModification&> (msg);
194 0 : if (msgData.component == L"TerritoryInfluence")
195 0 : MakeDirty();
196 0 : break;
197 : }
198 0 : case MT_ObstructionMapShapeChanged:
199 : case MT_TerrainChanged:
200 : case MT_WaterChanged:
201 : {
202 : // also recalculate the cost grid to support atlas changes
203 0 : SAFE_DELETE(m_CostGrid);
204 0 : MakeDirty();
205 0 : break;
206 : }
207 0 : case MT_Update:
208 : {
209 0 : if (m_TriggerEvent)
210 : {
211 0 : m_TriggerEvent = false;
212 0 : GetSimContext().GetComponentManager().BroadcastMessage(CMessageTerritoriesChanged());
213 : }
214 0 : break;
215 : }
216 0 : case MT_Interpolate:
217 : {
218 0 : const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg);
219 0 : Interpolate(msgData.deltaSimTime, msgData.offset);
220 0 : break;
221 : }
222 0 : case MT_RenderSubmit:
223 : {
224 0 : const CMessageRenderSubmit& msgData = static_cast<const CMessageRenderSubmit&> (msg);
225 0 : RenderSubmit(msgData.collector, msgData.frustum, msgData.culling);
226 0 : break;
227 : }
228 : }
229 0 : }
230 :
231 : // Check whether the entity is either a settlement or territory influence;
232 : // ignore any others
233 0 : void MakeDirtyIfRelevantEntity(entity_id_t ent)
234 : {
235 0 : CmpPtr<ICmpTerritoryInfluence> cmpTerritoryInfluence(GetSimContext(), ent);
236 0 : if (cmpTerritoryInfluence)
237 0 : MakeDirty();
238 0 : }
239 :
240 0 : const Grid<u8>& GetTerritoryGrid() override
241 : {
242 0 : CalculateTerritories();
243 0 : ENSURE(m_Territories);
244 0 : return *m_Territories;
245 : }
246 :
247 : player_id_t GetOwner(entity_pos_t x, entity_pos_t z) override;
248 : std::vector<u32> GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected) override;
249 : bool IsConnected(entity_pos_t x, entity_pos_t z) override;
250 :
251 : void SetTerritoryBlinking(entity_pos_t x, entity_pos_t z, bool enable) override;
252 : bool IsTerritoryBlinking(entity_pos_t x, entity_pos_t z) override;
253 :
254 : // To support lazy updates of territory render data,
255 : // we maintain a DirtyID here and increment it whenever territories change;
256 : // if a caller has a lower DirtyID then it needs to be updated.
257 : // We also do the same thing for blinking updates using DirtyBlinkingID.
258 :
259 : size_t m_DirtyID;
260 : size_t m_DirtyBlinkingID;
261 :
262 : bool m_ColorChanged;
263 :
264 0 : void MakeDirty()
265 : {
266 0 : SAFE_DELETE(m_Territories);
267 0 : ++m_DirtyID;
268 0 : m_BoundaryLinesDirty = true;
269 0 : m_TriggerEvent = true;
270 0 : }
271 :
272 0 : bool NeedUpdateTexture(size_t* dirtyID) override
273 : {
274 0 : if (*dirtyID == m_DirtyID && !m_ColorChanged)
275 0 : return false;
276 :
277 0 : *dirtyID = m_DirtyID;
278 0 : m_ColorChanged = false;
279 0 : return true;
280 : }
281 :
282 0 : bool NeedUpdateAI(size_t* dirtyID, size_t* dirtyBlinkingID) const override
283 : {
284 0 : if (*dirtyID == m_DirtyID && *dirtyBlinkingID == m_DirtyBlinkingID)
285 0 : return false;
286 :
287 0 : *dirtyID = m_DirtyID;
288 0 : *dirtyBlinkingID = m_DirtyBlinkingID;
289 0 : return true;
290 : }
291 :
292 : void CalculateCostGrid();
293 :
294 : void CalculateTerritories();
295 :
296 : u8 GetTerritoryPercentage(player_id_t player) override;
297 :
298 : std::vector<STerritoryBoundary> ComputeBoundaries();
299 :
300 : void UpdateBoundaryLines();
301 :
302 : void Interpolate(float frameTime, float frameOffset);
303 :
304 : void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling);
305 :
306 0 : void SetVisibility(bool visible) override
307 : {
308 0 : m_Visible = visible;
309 0 : }
310 :
311 : void UpdateColors() override;
312 :
313 : private:
314 :
315 : bool m_Visible;
316 : };
317 :
318 116 : REGISTER_COMPONENT_TYPE(TerritoryManager)
319 :
320 : // Tile data type, for easier accessing of coordinates
321 : struct Tile
322 : {
323 0 : Tile(u16 i, u16 j) : x(i), z(j) { }
324 : u16 x, z;
325 : };
326 :
327 : // Floodfill templates that expand neighbours from a certain source onwards
328 : // (posX, posZ) are the coordinates of the currently expanded tile
329 : // (nx, nz) are the coordinates of the current neighbour handled
330 : // The user of this floodfill should use "continue" on every neighbour that
331 : // shouldn't be expanded on its own. (without continue, an infinite loop will happen)
332 : # define FLOODFILL(i, j, code)\
333 : do {\
334 : const int NUM_NEIGHBOURS = 8;\
335 : const int NEIGHBOURS_X[NUM_NEIGHBOURS] = {1,-1, 0, 0, 1,-1, 1,-1};\
336 : const int NEIGHBOURS_Z[NUM_NEIGHBOURS] = {0, 0, 1,-1, 1,-1,-1, 1};\
337 : std::queue<Tile> openTiles;\
338 : openTiles.emplace(i, j);\
339 : while (!openTiles.empty())\
340 : {\
341 : u16 posX = openTiles.front().x;\
342 : u16 posZ = openTiles.front().z;\
343 : openTiles.pop();\
344 : for (int n = 0; n < NUM_NEIGHBOURS; ++n)\
345 : {\
346 : u16 nx = posX + NEIGHBOURS_X[n];\
347 : u16 nz = posZ + NEIGHBOURS_Z[n];\
348 : /* Check the bounds, underflow will cause the values to be big again */\
349 : if (nx >= tilesW || nz >= tilesH)\
350 : continue;\
351 : code\
352 : openTiles.emplace(nx, nz);\
353 : }\
354 : }\
355 : }\
356 : while (false)
357 :
358 : /**
359 : * Compute the tile indexes on the grid nearest to a given point
360 : */
361 0 : static void NearestTerritoryTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h)
362 : {
363 0 : entity_pos_t scale = Pathfinding::NAVCELL_SIZE * ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE;
364 0 : i = Clamp((x / scale).ToInt_RoundToNegInfinity(), 0, w - 1);
365 0 : j = Clamp((z / scale).ToInt_RoundToNegInfinity(), 0, h - 1);
366 0 : }
367 :
368 0 : void CCmpTerritoryManager::CalculateCostGrid()
369 : {
370 0 : if (m_CostGrid)
371 0 : return;
372 :
373 0 : CmpPtr<ICmpPathfinder> cmpPathfinder(GetSystemEntity());
374 0 : if (!cmpPathfinder)
375 0 : return;
376 :
377 0 : pass_class_t passClassTerritory = cmpPathfinder->GetPassabilityClass("default-terrain-only");
378 0 : pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted");
379 :
380 0 : const Grid<NavcellData>& passGrid = cmpPathfinder->GetPassabilityGrid();
381 :
382 0 : int tilesW = passGrid.m_W / NAVCELLS_PER_TERRITORY_TILE;
383 0 : int tilesH = passGrid.m_H / NAVCELLS_PER_TERRITORY_TILE;
384 :
385 0 : m_CostGrid = new Grid<u8>(tilesW, tilesH);
386 0 : m_TerritoryTotalPassableCellCount = 0;
387 :
388 0 : for (int i = 0; i < tilesW; ++i)
389 : {
390 0 : for (int j = 0; j < tilesH; ++j)
391 : {
392 0 : NavcellData c = 0;
393 0 : for (u16 di = 0; di < NAVCELLS_PER_TERRITORY_TILE; ++di)
394 0 : for (u16 dj = 0; dj < NAVCELLS_PER_TERRITORY_TILE; ++dj)
395 0 : c |= passGrid.get(
396 0 : i * NAVCELLS_PER_TERRITORY_TILE + di,
397 0 : j * NAVCELLS_PER_TERRITORY_TILE + dj);
398 0 : if (!IS_PASSABLE(c, passClassTerritory))
399 0 : m_CostGrid->set(i, j, m_ImpassableCost);
400 0 : else if (!IS_PASSABLE(c, passClassUnrestricted))
401 0 : m_CostGrid->set(i, j, 255); // off the world; use maximum cost
402 : else
403 : {
404 0 : m_CostGrid->set(i, j, 1);
405 0 : ++m_TerritoryTotalPassableCellCount;
406 : }
407 : }
408 : }
409 : }
410 :
411 0 : void CCmpTerritoryManager::CalculateTerritories()
412 : {
413 0 : if (m_Territories)
414 0 : return;
415 :
416 0 : PROFILE("CalculateTerritories");
417 :
418 : // If the pathfinder hasn't been loaded (e.g. this is called during map initialisation),
419 : // abort the computation (and assume callers can cope with m_Territories == NULL)
420 0 : CalculateCostGrid();
421 0 : if (!m_CostGrid)
422 0 : return;
423 :
424 0 : const u16 tilesW = m_CostGrid->m_W;
425 0 : const u16 tilesH = m_CostGrid->m_H;
426 :
427 0 : m_Territories = new Grid<u8>(tilesW, tilesH);
428 :
429 : // Reset territory counts for all players
430 0 : CmpPtr<ICmpPlayerManager> cmpPlayerManager(GetSystemEntity());
431 0 : if (cmpPlayerManager && (size_t)cmpPlayerManager->GetNumPlayers() != m_TerritoryCellCounts.size())
432 0 : m_TerritoryCellCounts.resize(cmpPlayerManager->GetNumPlayers());
433 0 : for (u16& count : m_TerritoryCellCounts)
434 0 : count = 0;
435 :
436 : // Find all territory influence entities
437 0 : CComponentManager::InterfaceList influences = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_TerritoryInfluence);
438 :
439 : // Split influence entities into per-player lists, ignoring any with invalid properties
440 0 : std::map<player_id_t, std::vector<entity_id_t> > influenceEntities;
441 0 : for (const CComponentManager::InterfacePair& pair : influences)
442 : {
443 0 : entity_id_t ent = pair.first;
444 :
445 0 : CmpPtr<ICmpOwnership> cmpOwnership(GetSimContext(), ent);
446 0 : if (!cmpOwnership)
447 0 : continue;
448 :
449 : // Ignore Gaia and unassigned or players we can't represent
450 0 : player_id_t owner = cmpOwnership->GetOwner();
451 0 : if (owner <= 0 || owner > TERRITORY_PLAYER_MASK)
452 0 : continue;
453 :
454 0 : influenceEntities[owner].push_back(ent);
455 : }
456 :
457 : // Store the overall best weight for comparison
458 0 : Grid<u32> bestWeightGrid(tilesW, tilesH);
459 : // store the root influences to mark territory as connected
460 0 : std::vector<entity_id_t> rootInfluenceEntities;
461 :
462 0 : for (const std::pair<const player_id_t, std::vector<entity_id_t>>& pair : influenceEntities)
463 : {
464 : // entityGrid stores the weight for a single entity, and is reset per entity
465 0 : Grid<u32> entityGrid(tilesW, tilesH);
466 : // playerGrid stores the combined weight of all entities for this player
467 0 : Grid<u32> playerGrid(tilesW, tilesH);
468 :
469 0 : u8 owner = static_cast<u8>(pair.first);
470 0 : const std::vector<entity_id_t>& ents = pair.second;
471 : // With 2^16 entities, we're safe against overflows as the weight is also limited to 2^16
472 0 : ENSURE(ents.size() < 1 << 16);
473 : // Compute the influence map of the current entity, then add it to the player grid
474 0 : for (entity_id_t ent : ents)
475 : {
476 0 : CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent);
477 0 : if (!cmpPosition || !cmpPosition->IsInWorld())
478 0 : continue;
479 :
480 0 : CmpPtr<ICmpTerritoryInfluence> cmpTerritoryInfluence(GetSimContext(), ent);
481 0 : u32 weight = cmpTerritoryInfluence->GetWeight();
482 0 : u32 radius = cmpTerritoryInfluence->GetRadius();
483 0 : if (weight == 0 || radius == 0)
484 0 : continue;
485 0 : u32 falloff = weight * (Pathfinding::NAVCELL_SIZE * NAVCELLS_PER_TERRITORY_TILE).ToInt_RoundToNegInfinity() / radius;
486 :
487 0 : CFixedVector2D pos = cmpPosition->GetPosition2D();
488 : u16 i, j;
489 0 : NearestTerritoryTile(pos.X, pos.Y, i, j, tilesW, tilesH);
490 :
491 0 : if (cmpTerritoryInfluence->IsRoot())
492 0 : rootInfluenceEntities.push_back(ent);
493 :
494 : // Initialise the tile under the entity
495 0 : entityGrid.set(i, j, weight);
496 0 : if (weight > bestWeightGrid.get(i, j))
497 : {
498 0 : bestWeightGrid.set(i, j, weight);
499 0 : m_Territories->set(i, j, owner);
500 : }
501 :
502 : // Expand influences outwards
503 0 : FLOODFILL(i, j,
504 : u32 dg = falloff * m_CostGrid->get(nx, nz);
505 :
506 : // diagonal neighbour -> multiply with approx sqrt(2)
507 : if (nx != posX && nz != posZ)
508 : dg = (dg * 362) / 256;
509 :
510 : // Don't expand if new cost is not better than previous value for that tile
511 : // (arranged to avoid underflow if entityGrid.get(x, z) < dg)
512 : if (entityGrid.get(posX, posZ) <= entityGrid.get(nx, nz) + dg)
513 : continue;
514 :
515 : // weight of this tile = weight of predecessor - falloff from predecessor
516 : u32 newWeight = entityGrid.get(posX, posZ) - dg;
517 : u32 totalWeight = playerGrid.get(nx, nz) - entityGrid.get(nx, nz) + newWeight;
518 : playerGrid.set(nx, nz, totalWeight);
519 : entityGrid.set(nx, nz, newWeight);
520 : // if this weight is better than the best thus far, set the owner
521 : if (totalWeight > bestWeightGrid.get(nx, nz))
522 : {
523 : bestWeightGrid.set(nx, nz, totalWeight);
524 : m_Territories->set(nx, nz, owner);
525 : }
526 : );
527 :
528 0 : entityGrid.reset();
529 : }
530 : }
531 :
532 : // Detect territories connected to a 'root' influence (typically a civ center)
533 : // belonging to their player, and mark them with the connected flag
534 0 : for (entity_id_t ent : rootInfluenceEntities)
535 : {
536 : // (These components must be valid else the entities wouldn't be added to this list)
537 0 : CmpPtr<ICmpOwnership> cmpOwnership(GetSimContext(), ent);
538 0 : CmpPtr<ICmpPosition> cmpPosition(GetSimContext(), ent);
539 :
540 0 : CFixedVector2D pos = cmpPosition->GetPosition2D();
541 : u16 i, j;
542 0 : NearestTerritoryTile(pos.X, pos.Y, i, j, tilesW, tilesH);
543 :
544 0 : u8 owner = (u8)cmpOwnership->GetOwner();
545 :
546 0 : if (m_Territories->get(i, j) != owner)
547 0 : continue;
548 :
549 0 : m_Territories->set(i, j, owner | TERRITORY_CONNECTED_MASK);
550 :
551 0 : FLOODFILL(i, j,
552 : // Don't expand non-owner tiles, or tiles that already have a connected mask
553 : if (m_Territories->get(nx, nz) != owner)
554 : continue;
555 : m_Territories->set(nx, nz, owner | TERRITORY_CONNECTED_MASK);
556 : if (m_CostGrid->get(nx, nz) < m_ImpassableCost)
557 : ++m_TerritoryCellCounts[owner];
558 : );
559 : }
560 :
561 : // Then recomputes the blinking tiles
562 0 : CmpPtr<ICmpTerritoryDecayManager> cmpTerritoryDecayManager(GetSystemEntity());
563 0 : if (cmpTerritoryDecayManager)
564 : {
565 0 : size_t dirtyBlinkingID = m_DirtyBlinkingID;
566 0 : cmpTerritoryDecayManager->SetBlinkingEntities();
567 0 : m_DirtyBlinkingID = dirtyBlinkingID;
568 : }
569 : }
570 :
571 0 : std::vector<STerritoryBoundary> CCmpTerritoryManager::ComputeBoundaries()
572 : {
573 0 : PROFILE("ComputeBoundaries");
574 :
575 0 : CalculateTerritories();
576 0 : ENSURE(m_Territories);
577 :
578 0 : return CTerritoryBoundaryCalculator::ComputeBoundaries(m_Territories);
579 : }
580 :
581 0 : u8 CCmpTerritoryManager::GetTerritoryPercentage(player_id_t player)
582 : {
583 0 : if (player <= 0 || static_cast<size_t>(player) >= m_TerritoryCellCounts.size())
584 0 : return 0;
585 :
586 0 : CalculateTerritories();
587 :
588 : // Territories may have been recalculated, check whether player is still there.
589 0 : if (m_TerritoryTotalPassableCellCount == 0 || static_cast<size_t>(player) >= m_TerritoryCellCounts.size())
590 0 : return 0;
591 :
592 0 : u8 percentage = (m_TerritoryCellCounts[player] * 100) / m_TerritoryTotalPassableCellCount;
593 0 : ENSURE(percentage <= 100);
594 0 : return percentage;
595 : }
596 :
597 0 : void CCmpTerritoryManager::UpdateBoundaryLines()
598 : {
599 0 : PROFILE("update boundary lines");
600 :
601 0 : m_BoundaryLines.clear();
602 0 : m_DebugBoundaryLineNodes.clear();
603 :
604 0 : if (!CRenderer::IsInitialised())
605 0 : return;
606 :
607 0 : std::vector<STerritoryBoundary> boundaries = ComputeBoundaries();
608 :
609 0 : CTextureProperties texturePropsBase("art/textures/misc/territory_border.png");
610 0 : texturePropsBase.SetAddressMode(
611 : Renderer::Backend::Sampler::AddressMode::CLAMP_TO_BORDER,
612 : Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE);
613 0 : texturePropsBase.SetAnisotropicFilter(true);
614 0 : CTexturePtr textureBase = g_Renderer.GetTextureManager().CreateTexture(texturePropsBase);
615 :
616 0 : CTextureProperties texturePropsMask("art/textures/misc/territory_border_mask.png");
617 0 : texturePropsMask.SetAddressMode(
618 : Renderer::Backend::Sampler::AddressMode::CLAMP_TO_BORDER,
619 : Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE);
620 0 : texturePropsMask.SetAnisotropicFilter(true);
621 0 : CTexturePtr textureMask = g_Renderer.GetTextureManager().CreateTexture(texturePropsMask);
622 :
623 0 : CmpPtr<ICmpPlayerManager> cmpPlayerManager(GetSystemEntity());
624 0 : if (!cmpPlayerManager)
625 0 : return;
626 :
627 0 : for (size_t i = 0; i < boundaries.size(); ++i)
628 : {
629 0 : if (boundaries[i].points.empty())
630 0 : continue;
631 :
632 0 : CColor color(1, 0, 1, 1);
633 0 : CmpPtr<ICmpPlayer> cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(boundaries[i].owner));
634 0 : if (cmpPlayer)
635 0 : color = cmpPlayer->GetDisplayedColor();
636 :
637 0 : m_BoundaryLines.push_back(SBoundaryLine());
638 0 : m_BoundaryLines.back().blinking = boundaries[i].blinking;
639 0 : m_BoundaryLines.back().owner = boundaries[i].owner;
640 0 : m_BoundaryLines.back().color = color;
641 0 : m_BoundaryLines.back().overlay.m_SimContext = &GetSimContext();
642 0 : m_BoundaryLines.back().overlay.m_TextureBase = textureBase;
643 0 : m_BoundaryLines.back().overlay.m_TextureMask = textureMask;
644 0 : m_BoundaryLines.back().overlay.m_Color = color;
645 0 : m_BoundaryLines.back().overlay.m_Thickness = m_BorderThickness;
646 0 : m_BoundaryLines.back().overlay.m_Closed = true;
647 :
648 0 : SimRender::SmoothPointsAverage(boundaries[i].points, m_BoundaryLines.back().overlay.m_Closed);
649 0 : SimRender::InterpolatePointsRNS(boundaries[i].points, m_BoundaryLines.back().overlay.m_Closed, m_BorderSeparation);
650 :
651 0 : std::vector<CVector2D>& points = m_BoundaryLines.back().overlay.m_Coords;
652 0 : for (size_t j = 0; j < boundaries[i].points.size(); ++j)
653 : {
654 0 : points.push_back(boundaries[i].points[j]);
655 :
656 0 : if (m_EnableLineDebugOverlays)
657 : {
658 0 : const size_t numHighlightNodes = 7; // highlight the X last nodes on either end to see where they meet (if closed)
659 0 : SOverlayLine overlayNode;
660 0 : if (j > boundaries[i].points.size() - 1 - numHighlightNodes)
661 0 : overlayNode.m_Color = CColor(1.f, 0.f, 0.f, 1.f);
662 0 : else if (j < numHighlightNodes)
663 0 : overlayNode.m_Color = CColor(0.f, 1.f, 0.f, 1.f);
664 : else
665 0 : overlayNode.m_Color = CColor(1.0f, 1.0f, 1.0f, 1.0f);
666 :
667 0 : overlayNode.m_Thickness = 0.1f;
668 0 : SimRender::ConstructCircleOnGround(GetSimContext(), boundaries[i].points[j].X, boundaries[i].points[j].Y, 0.1f, overlayNode, true);
669 0 : m_DebugBoundaryLineNodes.push_back(overlayNode);
670 : }
671 : }
672 :
673 : }
674 : }
675 :
676 0 : void CCmpTerritoryManager::Interpolate(float frameTime, float UNUSED(frameOffset))
677 : {
678 0 : m_AnimTime += frameTime;
679 :
680 0 : if (m_BoundaryLinesDirty)
681 : {
682 0 : UpdateBoundaryLines();
683 0 : m_BoundaryLinesDirty = false;
684 : }
685 :
686 0 : for (size_t i = 0; i < m_BoundaryLines.size(); ++i)
687 : {
688 0 : if (m_BoundaryLines[i].blinking)
689 : {
690 0 : CColor c = m_BoundaryLines[i].color;
691 0 : c.a *= 0.2f + 0.8f * fabsf((float)cos(m_AnimTime * M_PI)); // TODO: should let artists tweak this
692 0 : m_BoundaryLines[i].overlay.m_Color = c;
693 : }
694 : }
695 0 : }
696 :
697 0 : void CCmpTerritoryManager::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling)
698 : {
699 0 : if (!m_Visible)
700 0 : return;
701 :
702 0 : for (size_t i = 0; i < m_BoundaryLines.size(); ++i)
703 : {
704 0 : if (culling && !m_BoundaryLines[i].overlay.IsVisibleInFrustum(frustum))
705 0 : continue;
706 0 : collector.Submit(&m_BoundaryLines[i].overlay);
707 : }
708 :
709 0 : for (size_t i = 0; i < m_DebugBoundaryLineNodes.size(); ++i)
710 0 : collector.Submit(&m_DebugBoundaryLineNodes[i]);
711 :
712 : }
713 :
714 0 : player_id_t CCmpTerritoryManager::GetOwner(entity_pos_t x, entity_pos_t z)
715 : {
716 : u16 i, j;
717 0 : if (!m_Territories)
718 : {
719 0 : CalculateTerritories();
720 0 : if (!m_Territories)
721 0 : return 0;
722 : }
723 :
724 0 : NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
725 0 : return m_Territories->get(i, j) & TERRITORY_PLAYER_MASK;
726 : }
727 :
728 0 : std::vector<u32> CCmpTerritoryManager::GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected)
729 : {
730 0 : CmpPtr<ICmpPlayerManager> cmpPlayerManager(GetSystemEntity());
731 0 : if (!cmpPlayerManager)
732 0 : return std::vector<u32>();
733 :
734 0 : std::vector<u32> ret(cmpPlayerManager->GetNumPlayers(), 0);
735 0 : CalculateTerritories();
736 0 : if (!m_Territories)
737 0 : return ret;
738 :
739 : u16 i, j;
740 0 : NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
741 :
742 : // calculate the neighbours
743 0 : player_id_t thisOwner = m_Territories->get(i, j) & TERRITORY_PLAYER_MASK;
744 :
745 0 : u16 tilesW = m_Territories->m_W;
746 0 : u16 tilesH = m_Territories->m_H;
747 :
748 : // use a flood-fill algorithm that fills up to the borders and remembers the owners
749 0 : Grid<bool> markerGrid(tilesW, tilesH);
750 0 : markerGrid.set(i, j, true);
751 :
752 0 : FLOODFILL(i, j,
753 : if (markerGrid.get(nx, nz))
754 : continue;
755 : // mark the tile as visited in any case
756 : markerGrid.set(nx, nz, true);
757 : int owner = m_Territories->get(nx, nz) & TERRITORY_PLAYER_MASK;
758 : if (owner != thisOwner)
759 : {
760 : if (owner == 0 || !filterConnected || (m_Territories->get(nx, nz) & TERRITORY_CONNECTED_MASK) != 0)
761 : ret[owner]++; // add player to the neighbour list when requested
762 : continue; // don't expand non-owner tiles further
763 : }
764 : );
765 :
766 0 : return ret;
767 : }
768 :
769 0 : bool CCmpTerritoryManager::IsConnected(entity_pos_t x, entity_pos_t z)
770 : {
771 : u16 i, j;
772 0 : CalculateTerritories();
773 0 : if (!m_Territories)
774 0 : return false;
775 :
776 0 : NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
777 0 : return (m_Territories->get(i, j) & TERRITORY_CONNECTED_MASK) != 0;
778 : }
779 :
780 0 : void CCmpTerritoryManager::SetTerritoryBlinking(entity_pos_t x, entity_pos_t z, bool enable)
781 : {
782 0 : CalculateTerritories();
783 0 : if (!m_Territories)
784 0 : return;
785 :
786 : u16 i, j;
787 0 : NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
788 :
789 0 : u16 tilesW = m_Territories->m_W;
790 0 : u16 tilesH = m_Territories->m_H;
791 :
792 0 : player_id_t thisOwner = m_Territories->get(i, j) & TERRITORY_PLAYER_MASK;
793 :
794 0 : FLOODFILL(i, j,
795 : u8 bitmask = m_Territories->get(nx, nz);
796 : if ((bitmask & TERRITORY_PLAYER_MASK) != thisOwner)
797 : continue;
798 : u8 blinking = bitmask & TERRITORY_BLINKING_MASK;
799 : if (enable && !blinking)
800 : m_Territories->set(nx, nz, bitmask | TERRITORY_BLINKING_MASK);
801 : else if (!enable && blinking)
802 : m_Territories->set(nx, nz, bitmask & ~TERRITORY_BLINKING_MASK);
803 : else
804 : continue;
805 : );
806 0 : ++m_DirtyBlinkingID;
807 0 : m_BoundaryLinesDirty = true;
808 : }
809 :
810 0 : bool CCmpTerritoryManager::IsTerritoryBlinking(entity_pos_t x, entity_pos_t z)
811 : {
812 0 : CalculateTerritories();
813 0 : if (!m_Territories)
814 0 : return false;
815 :
816 : u16 i, j;
817 0 : NearestTerritoryTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H);
818 0 : return (m_Territories->get(i, j) & TERRITORY_BLINKING_MASK) != 0;
819 : }
820 :
821 0 : void CCmpTerritoryManager::UpdateColors()
822 : {
823 0 : m_ColorChanged = true;
824 :
825 0 : CmpPtr<ICmpPlayerManager> cmpPlayerManager(GetSystemEntity());
826 0 : if (!cmpPlayerManager)
827 0 : return;
828 :
829 0 : for (SBoundaryLine& boundaryLine : m_BoundaryLines)
830 : {
831 0 : CmpPtr<ICmpPlayer> cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(boundaryLine.owner));
832 0 : if (!cmpPlayer)
833 0 : continue;
834 :
835 0 : boundaryLine.color = cmpPlayer->GetDisplayedColor();
836 0 : boundaryLine.overlay.m_Color = boundaryLine.color;
837 : }
838 : }
839 :
840 0 : TerritoryOverlay::TerritoryOverlay(CCmpTerritoryManager& manager) :
841 : TerrainTextureOverlay((float)Pathfinding::NAVCELLS_PER_TERRAIN_TILE / ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE),
842 0 : m_TerritoryManager(manager)
843 0 : { }
844 :
845 0 : void TerritoryOverlay::BuildTextureRGBA(u8* data, size_t w, size_t h)
846 : {
847 0 : for (size_t j = 0; j < h; ++j)
848 : {
849 0 : for (size_t i = 0; i < w; ++i)
850 : {
851 0 : SColor4ub color;
852 0 : u8 id = (m_TerritoryManager.m_Territories->get((int)i, (int)j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK);
853 0 : color = GetColor(id, 64);
854 0 : *data++ = color.R;
855 0 : *data++ = color.G;
856 0 : *data++ = color.B;
857 0 : *data++ = color.A;
858 : }
859 : }
860 3 : }
861 :
862 : #undef FLOODFILL
|