Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/territory_block.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/territory_block.xml (revision 16675) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/territory_block.xml (nonexistent) @@ -1,45 +0,0 @@ - - - - - 9.0 - - - - true - false - false - false - false - false - false - - - - 0 - upright - false - 6.0 - - - - - - circle/128x128.png - circle/128x128_mask.png - - - - - 64 - false - 0 - 0 - - - false - true - structures/hellenes/wall_medium.xml - true - - Property changes on: ps/trunk/binaries/data/mods/public/simulation/templates/special/territory_block.xml ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Deleted: svn:mime-type ## -1 +0,0 ## -text/xml \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 16675) +++ ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 16676) @@ -1,243 +1,269 @@ function Capturable() {} Capturable.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + ""; Capturable.prototype.Init = function() { // Cache this value this.maxCp = +this.template.CapturePoints; this.cp = []; - this.StartRegenTimer(); }; //// Interface functions //// /** * Returns the current capture points array */ Capturable.prototype.GetCapturePoints = function() { return this.cp; }; Capturable.prototype.GetMaxCapturePoints = function() { return this.maxCp; }; Capturable.prototype.GetGarrisonRegenRate = function() { return ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", +this.template.GarrisonRegenRate, this.entity); }; /** * Set the new capture points, used for cloning entities * The caller should assure that the sum of capture points * matches the max. */ Capturable.prototype.SetCapturePoints = function(capturePointsArray) { this.cp = capturePointsArray; }; /** * Reduces the amount of capture points of an entity, * in favour of the player of the source * Returns the number of capture points actually taken */ Capturable.prototype.Reduce = function(amount, playerID) { + if (amount <= 0) + return 0; + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == -1) return 0; var cmpPlayerSource = QueryPlayerIDInterface(playerID); if (!cmpPlayerSource) return 0; // Before changing the value, activate Fogging if necessary to hide changes var cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); - var enemiesFilter = function(v, i) { return v > 0 && cmpPlayerSource.IsEnemy(i); }; - var numberOfEnemies = this.cp.filter(enemiesFilter).length; + var numberOfEnemies = this.cp.filter((v, i) => v > 0 && cmpPlayerSource.IsEnemy(i)).length; if (numberOfEnemies == 0) return 0; // distribute the capture points over all enemies var distributedAmount = amount / numberOfEnemies; for (let i in this.cp) { if (!cmpPlayerSource.IsEnemy(i)) continue; if (this.cp[i] > distributedAmount) this.cp[i] -= distributedAmount; else this.cp[i] = 0; } // give all cp taken to the player - var takenCp = this.maxCp - this.cp.reduce(function(a, b) { return a + b; }); + var takenCp = this.maxCp - this.cp.reduce((a, b) => a + b); this.cp[playerID] += takenCp; - this.StartRegenTimer(); - - Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp }) - - if (this.cp[cmpOwnership.GetOwner()] > 0) - return takenCp; - - // if all cp has been taken from the owner, convert it to the best player - var bestPlayer = 0; - for (let i in this.cp) - if (this.cp[i] >= this.cp[bestPlayer]) - bestPlayer = +i; - - cmpOwnership.SetOwner(bestPlayer); - + this.CheckTimer(); + this.RegisterCapturePointsChanged(); return takenCp; }; /** * Check if the source can (re)capture points from this building */ Capturable.prototype.CanCapture = function(playerID) { var cmpPlayerSource = QueryPlayerIDInterface(playerID); if (!cmpPlayerSource) warn(playerID + " has no player component defined on its id"); var cp = this.GetCapturePoints() var sourceEnemyCp = 0; for (let i in this.GetCapturePoints()) if (cmpPlayerSource.IsEnemy(i)) sourceEnemyCp += cp[i]; return sourceEnemyCp > 0; }; //// Private functions //// +/** + * this has to be called whenever the capture points are changed. + * It notifies other components of the change, and switches ownership when needed + */ +Capturable.prototype.RegisterCapturePointsChanged = function() +{ + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership) + return; + + Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp }) + + var owner = cmpOwnership.GetOwner(); + if (owner == -1 || this.cp[owner] > 0) + return; + + // if all cp has been taken from the owner, convert it to the best player + var bestPlayer = 0; + for (let i in this.cp) + if (this.cp[i] >= this.cp[bestPlayer]) + bestPlayer = +i; + + cmpOwnership.SetOwner(bestPlayer); +}; + Capturable.prototype.GetRegenRate = function() { var regenRate = +this.template.RegenRate; regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", regenRate, this.entity); - var cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); - if (cmpTerritoryDecay && cmpTerritoryDecay.IsDecaying()) - var territoryDecayRate = cmpTerritoryDecay.GetDecayRate(); - else - var territoryDecayRate = 0; - var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (cmpGarrisonHolder) var garrisonRegenRate = this.GetGarrisonRegenRate() * cmpGarrisonHolder.GetEntities().length; else var garrisonRegenRate = 0; - return regenRate + garrisonRegenRate - territoryDecayRate; + return regenRate + garrisonRegenRate; }; -Capturable.prototype.RegenCapturePoints = function() +Capturable.prototype.TimerTick = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == -1) return; + var owner = cmpOwnership.GetOwner(); + var modifiedCp = 0; + + // special handle for the territory decay + // reduce cp from the owner in favour of all neighbours (also allies) + var cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); + if (cmpTerritoryDecay && cmpTerritoryDecay.IsDecaying()) + { + var neighbours = cmpTerritoryDecay.GetConnectedNeighbours(); + var totalNeighbours = neighbours.reduce((a, b) => a + b); + var decay = Math.min(cmpTerritoryDecay.GetDecayRate(), this.cp[owner]); + this.cp[owner] -= decay; + modifiedCp += decay; + for (let p in neighbours) + this.cp[p] += decay * neighbours[p] / totalNeighbours; + this.RegisterCapturePointsChanged(); + } + var regenRate = this.GetRegenRate(); if (regenRate < 0) - var takenCp = this.Reduce(-regenRate, 0); - else - var takenCp = this.Reduce(regenRate, cmpOwnership.GetOwner()) + modifiedCp += this.Reduce(-regenRate, 0) + else if (regenRate > 0) + modifiedCp += this.Reduce(regenRate, owner) - if (takenCp > 0) + if (modifiedCp) return; - // no capture points taken, stop the timer + // nothing changed, stop the timer var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.regenTimer); - this.regenTimer = 0; - Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, {"regenerating": false, "rate": 0}); + cmpTimer.CancelTimer(this.timer); + this.timer = 0; + Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, {"regenerating": false, "regenRate": 0, "territoryDecay": 0}); }; /** - * Start the regeneration timer when no timer exists - * When nothing can be regenerated (f.e. because the - * rate is 0, or because it is fully regenerated), - * the timer stops automatically after one execution. + * Start the regeneration timer when no timer exists. + * When nothing can be modified (f.e. because it is fully regenerated), the + * timer stops automatically after one execution. */ -Capturable.prototype.StartRegenTimer = function() +Capturable.prototype.CheckTimer = function() { - if (this.regenTimer) + if (this.timer) return; - var rate = this.GetRegenRate(); - if (rate == 0) + var regenRate = this.GetRegenRate(); + var cmpDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); + var decay = cmpDecay && cmpDecay.IsDecaying() ? cmpDecay.GetDecayRate() : 0; + if (regenRate == 0 && decay == 0) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.regenTimer = cmpTimer.SetInterval(this.entity, IID_Capturable, "RegenCapturePoints", 1000, 1000, null); - Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, {"regenerating": true, "rate": rate}); + this.timer = cmpTimer.SetInterval(this.entity, IID_Capturable, "TimerTick", 1000, 1000, null); + Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, {"ticking": true, "regenRate": regenRate, "territoryDecay": decay}); }; //// Message Listeners //// Capturable.prototype.OnValueModification = function(msg) { if (msg.component != "Capturable") return; var oldMaxCp = this.GetMaxCapturePoints(); this.maxCp = ApplyValueModificationsToEntity("Capturable/Max", +this.template.Max, this.entity); if (oldMaxCp == this.maxCp) return; var scale = this.maxCp / oldMaxCp; for (let i in this.cp) this.cp[i] *= scale; Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp }); - this.StartRegenTimer(); + this.CheckTimer(); }; Capturable.prototype.OnGarrisonedUnitsChanged = function(msg) { - this.StartRegenTimer(); + this.CheckTimer(); }; Capturable.prototype.OnTerritoryDecayChanged = function(msg) { if (msg.to) - this.StartRegenTimer(); + this.CheckTimer(); }; Capturable.prototype.OnOwnershipChanged = function(msg) { - this.StartRegenTimer(); + this.CheckTimer(); // if the new owner has no capture points, it means that either // * it's being initialised now, or // * it changed ownership for a different reason (defeat, atlas, ...) if (this.cp[msg.to]) return; // initialise the capture points when created this.cp = []; var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); for (let i = 0; i < cmpPlayerManager.GetNumPlayers(); ++i) if (i == msg.to) this.cp[i] = this.maxCp; else this.cp[i] = 0; }; Engine.RegisterComponentType(IID_Capturable, "Capturable", Capturable); Index: ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js (revision 16675) +++ ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js (revision 16676) @@ -1,79 +1,97 @@ function TerritoryDecay() {} TerritoryDecay.prototype.Schema = "" + "" + ""; TerritoryDecay.prototype.Init = function() { this.decaying = false; + this.connectedNeighbours = []; }; TerritoryDecay.prototype.IsConnected = function() { + var numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + this.connectedNeighbours.fill(0, 0, numPlayers); + var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return false; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) - return false; + return true; // something without ownership can't decay - // Prevent special gaia buildings from decaying (e.g. fences, ruins) - if (cmpOwnership.GetOwner() == 0) - return true; - var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); - if (!cmpTerritoryManager) - return false; - var pos = cmpPosition.GetPosition2D(); var tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y); if (tileOwner != cmpOwnership.GetOwner()) + { + this.connectedNeighbours[tileOwner] = 1; return false; + } + + if (tileOwner == 0) + return true; // Gaia building on gaia ground -> don't decay - return cmpTerritoryManager.IsConnected(pos.x, pos.y); + if (cmpTerritoryManager.IsConnected(pos.x, pos.y)) + return true; + + this.connectedNeighbours = cmpTerritoryManager.GetNeighbours(pos.x, pos.y, true); + return false; }; TerritoryDecay.prototype.IsDecaying = function() { return this.decaying; }; TerritoryDecay.prototype.GetDecayRate = function() { return ApplyValueModificationsToEntity( "TerritoryDecay/DecayRate", +this.template.DecayRate, this.entity); }; +/** + * Get the number of connected bordering tiles to this region + * Only valid when this.IsDecaying() + */ +TerritoryDecay.prototype.GetConnectedNeighbours = function() +{ + return this.connectedNeighbours; +}; + TerritoryDecay.prototype.UpdateDecayState = function() { if (this.IsConnected()) var decaying = false; else var decaying = this.GetDecayRate() > 0; if (decaying === this.decaying) return; this.decaying = decaying; - Engine.PostMessage(this.entity, MT_TerritoryDecayChanged, { "entity": this.entity, "to": decaying }); + Engine.PostMessage(this.entity, MT_TerritoryDecayChanged, { "entity": this.entity, "to": decaying, "rate": this.GetDecayRate() }); }; TerritoryDecay.prototype.OnTerritoriesChanged = function(msg) { this.UpdateDecayState(); }; TerritoryDecay.prototype.OnTerritoryPositionChanged = function(msg) { this.UpdateDecayState(); }; TerritoryDecay.prototype.OnOwnershipChanged = function(msg) { - this.UpdateDecayState(); + // if it influences the territory, wait until we get a TerritoriesChanged message + if (!Engine.QueryInterface(this.entity, IID_TerritoryInfluence)) + this.UpdateDecayState(); }; Engine.RegisterComponentType(IID_TerritoryDecay, "TerritoryDecay", TerritoryDecay); Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/territory_pull.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/territory_pull.xml (revision 16675) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/territory_pull.xml (revision 16676) @@ -1,45 +1,38 @@ - - - 9.0 - - - - true - false - false - false - false - false - false - + + 5 + 0 + 0 + 0 upright false 6.0 circle/128x128.png circle/128x128_mask.png + + 5 + - 0 false - 0 - 0 + 32 + 30000 false true - structures/hellenes/wall_medium.xml + props/special/common/waypoint_flag.xml true Index: ps/trunk/source/simulation2/components/CCmpTerritoryInfluence.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTerritoryInfluence.cpp (revision 16675) +++ ps/trunk/source/simulation2/components/CCmpTerritoryInfluence.cpp (revision 16676) @@ -1,113 +1,97 @@ /* Copyright (C) 2015 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 "ICmpTerritoryInfluence.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpValueModificationManager.h" class CCmpTerritoryInfluence : public ICmpTerritoryInfluence { public: static void ClassInit(CComponentManager& UNUSED(componentManager)) { } DEFAULT_COMPONENT_ALLOCATOR(TerritoryInfluence) - i32 m_Cost; bool m_Root; - u32 m_Weight; + u16 m_Weight; u32 m_Radius; static std::string GetSchema() { return - "" - "" - "" - "255" - "" - "" - "" "" "" "" "" - "" + "" + "65536" // Max value 2^16 + "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode) { - if (paramNode.GetChild("OverrideCost").IsOk()) - m_Cost = paramNode.GetChild("OverrideCost").ToInt(); - else - m_Cost = -1; - m_Root = paramNode.GetChild("Root").ToBool(); - m_Weight = paramNode.GetChild("Weight").ToInt(); + m_Weight = (u16)paramNode.GetChild("Weight").ToInt(); m_Radius = paramNode.GetChild("Radius").ToInt(); } virtual void Deinit() { } virtual void Serialize(ISerializer& UNUSED(serialize)) { } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(paramNode); } - virtual i32 GetCost() - { - return m_Cost; - } - virtual bool IsRoot() { return m_Root; } - virtual u32 GetWeight() + virtual u16 GetWeight() { return m_Weight; } virtual u32 GetRadius() { CmpPtr cmpValueModificationManager(GetSystemEntity()); if (!cmpValueModificationManager) return m_Radius; u32 newRadius = cmpValueModificationManager->ApplyModifications(L"TerritoryInfluence/Radius", m_Radius, GetEntityId()); return newRadius; } }; REGISTER_COMPONENT_TYPE(TerritoryInfluence) Index: ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp (revision 16675) +++ ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp (revision 16676) @@ -1,764 +1,672 @@ -/* Copyright (C) 2012 Wildfire Games. +/* Copyright (C) 2015 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 "maths/Vector2D.h" #include "renderer/Renderer.h" #include "renderer/Scene.h" #include "renderer/TerrainOverlay.h" #include "simulation2/MessageTypes.h" -#include "simulation2/components/ICmpObstruction.h" -#include "simulation2/components/ICmpObstructionManager.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/ICmpSettlement.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpTerritoryInfluence.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Grid.h" -#include "simulation2/helpers/PriorityQueue.h" #include "simulation2/helpers/Render.h" class CCmpTerritoryManager; class TerritoryOverlay : public TerrainOverlay { NONCOPYABLE(TerritoryOverlay); public: CCmpTerritoryManager& m_TerritoryManager; TerritoryOverlay(CCmpTerritoryManager& manager); virtual void StartRender(); virtual void ProcessTile(ssize_t i, ssize_t j); }; class CCmpTerritoryManager : public ICmpTerritoryManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); componentManager.SubscribeGloballyToMessageType(MT_ValueModification); 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-5 (TERRITORY_PLAYER_MASK); // connected flag in bit 6 (TERRITORY_CONNECTED_MASK); // processed flag in bit 7 (TERRITORY_PROCESSED_MASK) Grid* m_Territories; // Set to true when territories change; will send a TerritoriesChanged message // during the Update phase bool m_TriggerEvent; struct SBoundaryLine { bool connected; 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; virtual void Init(const CParamNode& UNUSED(paramNode)) { m_Territories = NULL; m_DebugOverlay = NULL; // m_DebugOverlay = new TerritoryOverlay(*this); m_BoundaryLinesDirty = true; m_TriggerEvent = true; m_EnableLineDebugOverlays = false; m_DirtyID = 1; m_AnimTime = 0.0; CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/territorymanager.xml"); 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(); } virtual void Deinit() { SAFE_DELETE(m_Territories); SAFE_DELETE(m_DebugOverlay); } virtual void Serialize(ISerializer& UNUSED(serialize)) { // Territory state can be recomputed as required, so we don't need to serialize any of it. // TODO: do we ever need to serialize m_TriggerEvent to prevent lost messages? } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(paramNode); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); MakeDirtyIfRelevantEntity(msgData.entity); 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_TerrainChanged: case MT_WaterChanged: { MakeDirty(); break; } case MT_Update: { if (m_TriggerEvent) { m_TriggerEvent = false; CMessageTerritoriesChanged msg; GetSimContext().GetComponentManager().BroadcastMessage(msg); } 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); break; } } } // Check whether the entity is either a settlement or territory influence; // ignore any others void MakeDirtyIfRelevantEntity(entity_id_t ent) { - CmpPtr cmpSettlement(GetSimContext(), ent); - if (cmpSettlement) - MakeDirty(); - CmpPtr cmpTerritoryInfluence(GetSimContext(), ent); if (cmpTerritoryInfluence) MakeDirty(); } virtual const Grid& GetTerritoryGrid() { CalculateTerritories(); ENSURE(m_Territories); return *m_Territories; } virtual player_id_t GetOwner(entity_pos_t x, entity_pos_t z); + virtual std::vector GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected); virtual bool IsConnected(entity_pos_t x, entity_pos_t z); // 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. size_t m_DirtyID; void MakeDirty() { SAFE_DELETE(m_Territories); ++m_DirtyID; m_BoundaryLinesDirty = true; m_TriggerEvent = true; } virtual bool NeedUpdate(size_t* dirtyID) { if (*dirtyID != m_DirtyID) { *dirtyID = m_DirtyID; return true; } return false; } void CalculateTerritories(); - /** - * Updates @p grid based on the obstruction shapes of all entities with - * a TerritoryInfluence component. Grid cells are 0 if no influence, - * or 1+c if the influence have cost c (assumed between 0 and 254). - */ - void RasteriseInfluences(CComponentManager::InterfaceList& infls, Grid& grid); - std::vector ComputeBoundaries(); void UpdateBoundaryLines(); void Interpolate(float frameTime, float frameOffset); void RenderSubmit(SceneCollector& collector); }; REGISTER_COMPONENT_TYPE(TerritoryManager) -/* -We compute the territory influence of an entity with a kind of best-first search, -storing an 'open' list of tiles that have not yet been processed, -then taking the highest-weight tile (closest to origin) and updating the weight -of extending to each neighbour (based on radius-determining 'falloff' value, -adjusted by terrain movement cost), and repeating until all tiles are processed. -*/ - -typedef PriorityQueueHeap, u32, std::greater > OpenQueue; - -static void ProcessNeighbour(u32 falloff, u16 i, u16 j, u32 pg, bool diagonal, - Grid& grid, OpenQueue& queue, const Grid& costGrid) -{ - u32 dg = falloff * costGrid.get(i, j); - if (diagonal) - dg = (dg * 362) / 256; - - // Stop if new cost g=pg-dg is not better than previous value for that tile - // (arranged to avoid underflow if pg < dg) - if (pg <= grid.get(i, j) + dg) - return; +// Tile data type, for easier accessing of coordinates +struct Tile +{ + Tile(u16 i, u16 j) : x(i), z(j) { } + u16 x, z; +}; - u32 g = pg - dg; // cost to this tile = cost to predecessor - falloff from predecessor +// Floodfill templates that expand neighbours from a certain source onwards +// (x, z) 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 x = openTiles.front().x;\ + u16 z = openTiles.front().z;\ + openTiles.pop();\ + for (int n = 0; n < NUM_NEIGHBOURS; ++n)\ + {\ + u16 nx = x + NEIGHBOURS_X[n];\ + u16 nz = z + 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) - grid.set(i, j, g); - OpenQueue::Item tile = { std::make_pair(i, j), g }; - queue.push(tile); -} - -static void FloodFill(Grid& grid, Grid& costGrid, OpenQueue& openTiles, u32 falloff) -{ - u16 tilesW = grid.m_W; - u16 tilesH = grid.m_H; - - while (!openTiles.empty()) - { - OpenQueue::Item tile = openTiles.pop(); - - // Process neighbours (if they're not off the edge of the map) - u16 x = tile.id.first; - u16 z = tile.id.second; - if (x > 0) - ProcessNeighbour(falloff, (u16)(x-1), z, tile.rank, false, grid, openTiles, costGrid); - if (x < tilesW-1) - ProcessNeighbour(falloff, (u16)(x+1), z, tile.rank, false, grid, openTiles, costGrid); - if (z > 0) - ProcessNeighbour(falloff, x, (u16)(z-1), tile.rank, false, grid, openTiles, costGrid); - if (z < tilesH-1) - ProcessNeighbour(falloff, x, (u16)(z+1), tile.rank, false, grid, openTiles, costGrid); - if (x > 0 && z > 0) - ProcessNeighbour(falloff, (u16)(x-1), (u16)(z-1), tile.rank, true, grid, openTiles, costGrid); - if (x > 0 && z < tilesH-1) - ProcessNeighbour(falloff, (u16)(x-1), (u16)(z+1), tile.rank, true, grid, openTiles, costGrid); - if (x < tilesW-1 && z > 0) - ProcessNeighbour(falloff, (u16)(x+1), (u16)(z-1), tile.rank, true, grid, openTiles, costGrid); - if (x < tilesW-1 && z < tilesH-1) - ProcessNeighbour(falloff, (u16)(x+1), (u16)(z+1), tile.rank, true, grid, openTiles, costGrid); - } +/** + * Compute the tile indexes on the grid nearest to a given point + */ +static void NearestTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h) +{ + i = (u16)clamp((x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, w-1); + j = (u16)clamp((z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, h-1); } void CCmpTerritoryManager::CalculateTerritories() { if (m_Territories) return; PROFILE("CalculateTerritories"); CmpPtr cmpTerrain(GetSystemEntity()); // If the terrain hasn't been loaded (e.g. this is called during map initialisation), // abort the computation (and assume callers can cope with m_Territories == NULL) if (!cmpTerrain->IsLoaded()) return; - u16 tilesW = cmpTerrain->GetTilesPerSide(); - u16 tilesH = cmpTerrain->GetTilesPerSide(); + const u16 tilesW = cmpTerrain->GetTilesPerSide(); + const u16 tilesH = tilesW; m_Territories = new Grid(tilesW, tilesH); - // Compute terrain-passability-dependent costs per tile - Grid influenceGrid(tilesW, tilesH); - - CmpPtr cmpPathfinder(GetSystemEntity()); - ICmpPathfinder::pass_class_t passClassDefault = cmpPathfinder->GetPassabilityClass("default"); - ICmpPathfinder::pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted"); - - const Grid& passGrid = cmpPathfinder->GetPassabilityGrid(); - for (u16 j = 0; j < tilesH; ++j) - { - for (u16 i = 0; i < tilesW; ++i) - { - u16 g = passGrid.get(i, j); - u8 cost; - if (g & passClassUnrestricted) - cost = 255; // off the world; use maximum cost - else if (g & passClassDefault) - cost = m_ImpassableCost; - else - cost = 1; - influenceGrid.set(i, j, cost); - } - } - // Find all territory influence entities CComponentManager::InterfaceList influences = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_TerritoryInfluence); - // Allow influence entities to override the terrain costs - RasteriseInfluences(influences, influenceGrid); - // Split influence entities into per-player lists, ignoring any with invalid properties std::map > influenceEntities; - std::vector rootInfluenceEntities; - for (CComponentManager::InterfaceList::iterator it = influences.begin(); it != influences.end(); ++it) + for (const CComponentManager::InterfacePair& pair : influences) { - // Ignore any with no weight or radius (to avoid divide-by-zero later) - ICmpTerritoryInfluence* cmpTerritoryInfluence = static_cast(it->second); - if (cmpTerritoryInfluence->GetWeight() == 0 || cmpTerritoryInfluence->GetRadius() == 0) - continue; + entity_id_t ent = pair.first; - CmpPtr cmpOwnership(GetSimContext(), it->first); + CmpPtr cmpOwnership(GetSimContext(), ent); if (!cmpOwnership) continue; - // Ignore Gaia and unassigned + // Ignore Gaia and unassigned or players we can't represent player_id_t owner = cmpOwnership->GetOwner(); - if (owner <= 0) + if (owner <= 0 || owner > TERRITORY_PLAYER_MASK) continue; - // We only have so many bits to store tile ownership, so ignore unrepresentable players - if (owner > TERRITORY_PLAYER_MASK) - continue; + influenceEntities[owner].push_back(ent); + } - // Ignore if invalid position - CmpPtr cmpPosition(GetSimContext(), it->first); - if (!cmpPosition || !cmpPosition->IsInWorld()) - continue; + // Store the overall best weight for comparison + Grid bestWeightGrid(tilesW, tilesH); + // store the root influences to mark territory as connected + std::vector rootInfluenceEntities; - influenceEntities[owner].push_back(it->first); + CmpPtr cmpPathfinder(GetSystemEntity()); + if (!cmpPathfinder) + return; - if (cmpTerritoryInfluence->IsRoot()) - rootInfluenceEntities.push_back(it->first); - } + ICmpPathfinder::pass_class_t passClassDefault = cmpPathfinder->GetPassabilityClass("default"); + ICmpPathfinder::pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted"); - // For each player, store the sum of influences on each tile - std::vector > > playerGrids; - // TODO: this is a large waste of memory; we don't really need to store - // all the intermediate grids + const Grid& passibilityGrid = cmpPathfinder->GetPassabilityGrid(); - for (std::map >::iterator it = influenceEntities.begin(); it != influenceEntities.end(); ++it) + 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); - std::vector& ents = it->second; - for (std::vector::iterator eit = ents.begin(); eit != ents.end(); ++eit) - { - // Compute the influence map of the current entity, then add it to the player grid + u8 owner = (u8)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; - Grid entityGrid(tilesW, tilesH); - - CmpPtr cmpPosition(GetSimContext(), *eit); - CFixedVector2D pos = cmpPosition->GetPosition2D(); - u16 i = (u16)clamp((pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(), 0, tilesW-1); - u16 j = (u16)clamp((pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(), 0, tilesH-1); - - CmpPtr cmpTerritoryInfluence(GetSimContext(), *eit); + CmpPtr cmpTerritoryInfluence(GetSimContext(), ent); u32 weight = cmpTerritoryInfluence->GetWeight(); u32 radius = cmpTerritoryInfluence->GetRadius() / TERRAIN_TILE_SIZE; - u32 falloff = weight / radius; // earlier check for GetRadius() == 0 prevents divide-by-zero + if (weight == 0 || radius == 0) + continue; + u32 falloff = weight / radius; - // TODO: we should have some maximum value on weight, to avoid overflow - // when doing all the sums + CFixedVector2D pos = cmpPosition->GetPosition2D(); + u16 i, j; + NearestTile(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); - OpenQueue openTiles; - OpenQueue::Item tile = { std::make_pair((u16)i, (i16)j), weight }; - openTiles.push(tile); + if (weight > bestWeightGrid.get(i, j)) + { + bestWeightGrid.set(i, j, weight); + m_Territories->set(i, j, owner); + } // Expand influences outwards - FloodFill(entityGrid, influenceGrid, openTiles, falloff); - - // TODO: we should do a sparse grid and only add the non-zero regions, for performance - playerGrid.add(entityGrid); - } - - playerGrids.push_back(std::make_pair(it->first, playerGrid)); - } - - // Set m_Territories to the player ID with the highest influence for each tile - for (u16 j = 0; j < tilesH; ++j) - { - for (u16 i = 0; i < tilesW; ++i) - { - u32 bestWeight = 0; - for (size_t k = 0; k < playerGrids.size(); ++k) - { - u32 w = playerGrids[k].second.get(i, j); - if (w > bestWeight) + FLOODFILL(i, j, + u32 dg = falloff; + // enlarge the falloff for unpassable tiles + u16 g = passibilityGrid.get(nx, nz); + if (g & passClassUnrestricted) + dg *= 255; // off the world; use maximum cost + else if (g & passClassDefault) + dg *= m_ImpassableCost; + + // diagonal neighbour -> multiply with approx sqrt(2) + if (nx != x && nz != z) + 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(x, z) <= entityGrid.get(nx, nz) + dg) + continue; + + // weight of this tile = weight of predecessor - falloff from predecessor + u32 newWeight = entityGrid.get(x, z) - 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)) { - player_id_t id = playerGrids[k].first; - m_Territories->set(i, j, (u8)id); - bestWeight = w; + 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 (std::vector::iterator it = rootInfluenceEntities.begin(); it != rootInfluenceEntities.end(); ++it) + for (entity_id_t ent : rootInfluenceEntities) { // (These components must be valid else the entities wouldn't be added to this list) - CmpPtr cmpOwnership(GetSimContext(), *it); - CmpPtr cmpPosition(GetSimContext(), *it); + CmpPtr cmpOwnership(GetSimContext(), ent); + CmpPtr cmpPosition(GetSimContext(), ent); CFixedVector2D pos = cmpPosition->GetPosition2D(); - u16 i = (u16)clamp((pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(), 0, tilesW-1); - u16 j = (u16)clamp((pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(), 0, tilesH-1); + u16 i, j; + NearestTile(pos.X, pos.Y, i, j, tilesW, tilesH); u8 owner = (u8)cmpOwnership->GetOwner(); if (m_Territories->get(i, j) != owner) continue; - // TODO: would be nice to refactor some of the many flood fill - // algorithms in this component - - Grid& grid = *m_Territories; - - u16 maxi = (u16)(grid.m_W-1); - u16 maxj = (u16)(grid.m_H-1); - - std::vector > tileStack; - -#define MARK_AND_PUSH(i, j) STMT(grid.set(i, j, owner | TERRITORY_CONNECTED_MASK); tileStack.push_back(std::make_pair(i, j)); ) - - MARK_AND_PUSH(i, j); - while (!tileStack.empty()) - { - int ti = tileStack.back().first; - int tj = tileStack.back().second; - tileStack.pop_back(); - - if (ti > 0 && grid.get(ti-1, tj) == owner) - MARK_AND_PUSH(ti-1, tj); - if (ti < maxi && grid.get(ti+1, tj) == owner) - MARK_AND_PUSH(ti+1, tj); - if (tj > 0 && grid.get(ti, tj-1) == owner) - MARK_AND_PUSH(ti, tj-1); - if (tj < maxj && grid.get(ti, tj+1) == owner) - MARK_AND_PUSH(ti, tj+1); - - if (ti > 0 && tj > 0 && grid.get(ti-1, tj-1) == owner) - MARK_AND_PUSH(ti-1, tj-1); - if (ti > 0 && tj < maxj && grid.get(ti-1, tj+1) == owner) - MARK_AND_PUSH(ti-1, tj+1); - if (ti < maxi && tj > 0 && grid.get(ti+1, tj-1) == owner) - MARK_AND_PUSH(ti+1, tj-1); - if (ti < maxi && tj < maxj && grid.get(ti+1, tj+1) == owner) - MARK_AND_PUSH(ti+1, tj+1); - } - -#undef MARK_AND_PUSH - } -} - -/** - * Compute the tile indexes on the grid nearest to a given point - */ -static void NearestTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h) -{ - i = (u16)clamp((x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, w-1); - j = (u16)clamp((z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToZero(), 0, h-1); -} - -/** - * Returns the position of the center of the given tile - */ -static void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z) -{ - x = entity_pos_t::FromInt(i*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE/2); - z = entity_pos_t::FromInt(j*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE/2); -} - -// TODO: would be nice not to duplicate those two functions from CCmpObstructionManager.cpp - - -void CCmpTerritoryManager::RasteriseInfluences(CComponentManager::InterfaceList& infls, Grid& grid) -{ - for (CComponentManager::InterfaceList::iterator it = infls.begin(); it != infls.end(); ++it) - { - ICmpTerritoryInfluence* cmpTerritoryInfluence = static_cast(it->second); - - i32 cost = cmpTerritoryInfluence->GetCost(); - if (cost == -1) - continue; - - CmpPtr cmpObstruction(GetSimContext(), it->first); - if (!cmpObstruction) - continue; - - ICmpObstructionManager::ObstructionSquare square; - if (!cmpObstruction->GetObstructionSquare(square)) - continue; - - CFixedVector2D halfSize(square.hw, square.hh); - CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(square.u, square.v, halfSize); - - u16 i0, j0, i1, j1; - NearestTile(square.x - halfBound.X, square.z - halfBound.Y, i0, j0, grid.m_W, grid.m_H); - NearestTile(square.x + halfBound.X, square.z + halfBound.Y, i1, j1, grid.m_W, grid.m_H); - for (u16 j = j0; j <= j1; ++j) - { - for (u16 i = i0; i <= i1; ++i) - { - entity_pos_t x, z; - TileCenter(i, j, x, z); - if (Geometry::PointIsInSquare(CFixedVector2D(x - square.x, z - square.z), square.u, square.v, halfSize)) - grid.set(i, j, (u8)cost); - } - } + m_Territories->set(i, j, owner | TERRITORY_CONNECTED_MASK); + 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); + ); } } std::vector CCmpTerritoryManager::ComputeBoundaries() { PROFILE("ComputeBoundaries"); CalculateTerritories(); ENSURE(m_Territories); return CTerritoryBoundaryCalculator::ComputeBoundaries(m_Territories); } 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.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsBase.SetMaxAnisotropy(2.f); CTexturePtr textureBase = g_Renderer.GetTextureManager().CreateTexture(texturePropsBase); CTextureProperties texturePropsMask("art/textures/misc/territory_border_mask.png"); texturePropsMask.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsMask.SetMaxAnisotropy(2.f); 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->GetColor(); m_BoundaryLines.push_back(SBoundaryLine()); m_BoundaryLines.back().connected = boundaries[i].connected; 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].X); points.push_back(boundaries[i].points[j].Y); 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 = 1; 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].connected) { 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) { for (size_t i = 0; i < m_BoundaryLines.size(); ++i) 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; CalculateTerritories(); if (!m_Territories) return 0; NearestTile(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; + NearestTile(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; NearestTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); return (m_Territories->get(i, j) & TERRITORY_CONNECTED_MASK) != 0; } TerritoryOverlay::TerritoryOverlay(CCmpTerritoryManager& manager) : TerrainOverlay(manager.GetSimContext()), m_TerritoryManager(manager) { } void TerritoryOverlay::StartRender() { m_TerritoryManager.CalculateTerritories(); } void TerritoryOverlay::ProcessTile(ssize_t i, ssize_t j) { if (!m_TerritoryManager.m_Territories) return; u8 id = (m_TerritoryManager.m_Territories->get((int) i, (int) j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK); float a = 0.2f; switch (id) { case 0: break; case 1: RenderTile(CColor(1, 0, 0, a), false); break; case 2: RenderTile(CColor(0, 1, 0, a), false); break; case 3: RenderTile(CColor(0, 0, 1, a), false); break; case 4: RenderTile(CColor(1, 1, 0, a), false); break; case 5: RenderTile(CColor(0, 1, 1, a), false); break; case 6: RenderTile(CColor(1, 0, 1, a), false); break; default: RenderTile(CColor(1, 1, 1, a), false); break; } } + +#undef FOODFILL Index: ps/trunk/source/simulation2/components/ICmpTerritoryInfluence.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpTerritoryInfluence.h (revision 16675) +++ ps/trunk/source/simulation2/components/ICmpTerritoryInfluence.h (revision 16676) @@ -1,42 +1,35 @@ -/* Copyright (C) 2011 Wildfire Games. +/* Copyright (C) 2015 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 . */ #ifndef INCLUDED_ICMPTERRITORYINFLUENCE #define INCLUDED_ICMPTERRITORYINFLUENCE #include "simulation2/system/Interface.h" class ICmpTerritoryInfluence : public IComponent { public: - /** - * Returns either -1 to indicate no special terrain cost, or a value - * in [0, 255] to indicate overriding the normal cost of the terrain - * under the entity's obstruction. - */ - virtual i32 GetCost() = 0; - virtual bool IsRoot() = 0; - virtual u32 GetWeight() = 0; + virtual u16 GetWeight() = 0; virtual u32 GetRadius() = 0; DECLARE_INTERFACE_TYPE(TerritoryInfluence) }; #endif // INCLUDED_ICMPTERRITORYINFLUENCE Index: ps/trunk/source/simulation2/components/ICmpTerritoryManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpTerritoryManager.cpp (revision 16675) +++ ps/trunk/source/simulation2/components/ICmpTerritoryManager.cpp (revision 16676) @@ -1,27 +1,28 @@ -/* Copyright (C) 2011 Wildfire Games. +/* Copyright (C) 2015 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 "ICmpTerritoryManager.h" #include "simulation2/system/InterfaceScripted.h" BEGIN_INTERFACE_WRAPPER(TerritoryManager) DEFINE_INTERFACE_METHOD_2("GetOwner", player_id_t, ICmpTerritoryManager, GetOwner, entity_pos_t, entity_pos_t) +DEFINE_INTERFACE_METHOD_3("GetNeighbours", std::vector, ICmpTerritoryManager, GetNeighbours, entity_pos_t, entity_pos_t, bool) DEFINE_INTERFACE_METHOD_2("IsConnected", bool, ICmpTerritoryManager, IsConnected, entity_pos_t, entity_pos_t) END_INTERFACE_WRAPPER(TerritoryManager) Index: ps/trunk/source/simulation2/components/ICmpTerritoryManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpTerritoryManager.h (revision 16675) +++ ps/trunk/source/simulation2/components/ICmpTerritoryManager.h (revision 16676) @@ -1,58 +1,64 @@ -/* Copyright (C) 2011 Wildfire Games. +/* Copyright (C) 2015 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 . */ #ifndef INCLUDED_ICMPTERRITORYMANAGER #define INCLUDED_ICMPTERRITORYMANAGER #include "simulation2/system/Interface.h" #include "simulation2/helpers/Grid.h" #include "simulation2/helpers/Player.h" #include "simulation2/components/ICmpPosition.h" class ICmpTerritoryManager : public IComponent { public: virtual bool NeedUpdate(size_t* dirtyID) = 0; static const int TERRITORY_PLAYER_MASK = 0x3F; static const int TERRITORY_CONNECTED_MASK = 0x40; static const int TERRITORY_PROCESSED_MASK = 0x80; //< For internal use; marks a tile as processed. /** * For each tile, the TERRITORY_PLAYER_MASK bits are player ID; * TERRITORY_CONNECTED_MASK is set if the tile is connected to a root object * (civ center etc). */ virtual const Grid& GetTerritoryGrid() = 0; /** * Get owner of territory at given position. * @return player ID of owner; 0 if neutral territory */ virtual player_id_t GetOwner(entity_pos_t x, entity_pos_t z) = 0; /** + * get the number of neighbour tiles for per player for the selected position + * @return A list with the number of neighbour tiles per player + */ + virtual std::vector GetNeighbours(entity_pos_t x, entity_pos_t z, bool filterConnected) = 0; + + /** * Get whether territory at given position is connected to a root object * (civ center etc) owned by that territory's player. */ virtual bool IsConnected(entity_pos_t x, entity_pos_t z) = 0; DECLARE_INTERFACE_TYPE(TerritoryManager) }; #endif // INCLUDED_ICMPTERRITORYMANAGER Index: ps/trunk/source/simulation2/system/ComponentManager.h =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.h (revision 16675) +++ ps/trunk/source/simulation2/system/ComponentManager.h (revision 16676) @@ -1,384 +1,385 @@ /* Copyright (C) 2015 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 . */ #ifndef INCLUDED_COMPONENTMANAGER #define INCLUDED_COMPONENTMANAGER #include "Entity.h" #include "Components.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/helpers/Player.h" #include "ps/Filesystem.h" #include #include #include class IComponent; class CParamNode; class CMessage; class CSimContext; class CDynamicSubscription; class CComponentManager { NONCOPYABLE(CComponentManager); public: // We can't use EInterfaceId/etc directly, since scripts dynamically generate new IDs // and casting arbitrary ints to enums is undefined behaviour, so use 'int' typedefs typedef int InterfaceId; typedef int ComponentTypeId; typedef int MessageTypeId; private: // Component allocation types typedef IComponent* (*AllocFunc)(ScriptInterface& scriptInterface, JS::HandleValue ctor); typedef void (*DeallocFunc)(IComponent*); // ComponentTypes come in three types: // Native: normal C++ component // ScriptWrapper: C++ component that wraps a JS component implementation // Script: a ScriptWrapper linked to a specific JS component implementation enum EComponentTypeType { CT_Native, CT_ScriptWrapper, CT_Script }; // Representation of a component type, to be used when instantiating components struct ComponentType { EComponentTypeType type; InterfaceId iid; AllocFunc alloc; DeallocFunc dealloc; std::string name; std::string schema; // RelaxNG fragment DefPersistentRooted ctor; // only valid if type == CT_Script // TODO: Constructor, move assignment operator and move constructor only have to be // explicitly defined for Visual Studio. VS2013 is still behind on C++11 support // What's missing is what they call "Rvalue references v3.0", see // https://msdn.microsoft.com/en-us/library/hh567368.aspx#rvref ComponentType() {} ComponentType (EComponentTypeType type, InterfaceId iid, AllocFunc alloc, DeallocFunc dealloc, std::string name, std::string schema, DefPersistentRooted ctor) : type(type), iid(iid), alloc(alloc), dealloc(dealloc), name(name), schema(schema), ctor(std::move(ctor)) { } ComponentType& operator= (ComponentType&& other) { type = std::move(other.type); iid = std::move(other.iid); alloc = std::move(other.alloc); dealloc = std::move(other.dealloc); name = std::move(other.name); schema = std::move(other.schema); ctor = std::move(other.ctor); return *this; } ComponentType(ComponentType&& other) { type = std::move(other.type); iid = std::move(other.iid); alloc = std::move(other.alloc); dealloc = std::move(other.dealloc); name = std::move(other.name); schema = std::move(other.schema); ctor = std::move(other.ctor); } }; struct FindJSONFilesCallbackData { VfsPath path; std::vector templates; }; public: CComponentManager(CSimContext&, shared_ptr rt, bool skipScriptFunctions = false); ~CComponentManager(); void LoadComponentTypes(); /** * Load a script and execute it in a new function scope. * @param filename VFS path to load * @param hotload set to true if this script has been loaded before, and redefinitions of * existing components should not be considered errors */ bool LoadScript(const VfsPath& filename, bool hotload = false); void RegisterMessageType(MessageTypeId mtid, const char* name); void RegisterComponentType(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema); void RegisterComponentTypeScriptWrapper(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema); void MarkScriptedComponentForSystemEntity(CComponentManager::ComponentTypeId cid); /** * Subscribe the current component type to the given message type. * Each component's HandleMessage will be called on any BroadcastMessage of this message type, * or on any PostMessage of this type targeted at the component's entity. * Must only be called by a component type's ClassInit. */ void SubscribeToMessageType(MessageTypeId mtid); /** * Subscribe the current component type to all messages of the given message type. * Each component's HandleMessage will be called on any BroadcastMessage or PostMessage of this message type, * regardless of the entity. * Must only be called by a component type's ClassInit. */ void SubscribeGloballyToMessageType(MessageTypeId mtid); /** * Subscribe the given component instance to all messages of the given message type. * The component's HandleMessage will be called on any BroadcastMessage or PostMessage of * this message type, regardless of the entity. * * This can be called at any time (including inside the HandleMessage callback for this message type). * * The component type must not have statically subscribed to this message type in its ClassInit. * * The subscription status is not saved or network-synchronised. Components must remember to * resubscribe in their Deserialize methods if they still want the message. * * This is primarily intended for Interpolate and RenderSubmit messages, to avoid the cost of * sending the message to components that do not currently need to do any rendering. */ void DynamicSubscriptionNonsync(MessageTypeId mtid, IComponent* component, bool enabled); /** * @param cname Requested component type name (not including any "CID" or "CCmp" prefix) * @return The component type id, or CID__Invalid if not found */ ComponentTypeId LookupCID(const std::string& cname) const; /** * @return The name of the given component type, or "" if not found */ std::string LookupComponentTypeName(ComponentTypeId cid) const; /** * Set up an empty SYSTEM_ENTITY. Must be called after ResetState() and before GetSystemEntity(). */ void InitSystemEntity(); /** * Returns a CEntityHandle with id SYSTEM_ENTITY. */ CEntityHandle GetSystemEntity() { ASSERT(m_SystemEntity.GetId() == SYSTEM_ENTITY); return m_SystemEntity; } /** * Returns a CEntityHandle with id @p ent. * If @p allowCreate is true and there is no existing CEntityHandle, a new handle will be allocated. */ CEntityHandle LookupEntityHandle(entity_id_t ent, bool allowCreate = false); /** * Returns a new entity ID that has never been used before. * This affects the simulation state so it must only be called in network-synchronised ways. */ entity_id_t AllocateNewEntity(); /** * Returns a new local entity ID that has never been used before. * This entity will not be synchronised over the network, stored in saved games, etc. */ entity_id_t AllocateNewLocalEntity(); /** * Returns a new entity ID that has never been used before. * If possible, returns preferredId, and ensures this ID won't be allocated again. * This affects the simulation state so it must only be called in network-synchronised ways. */ entity_id_t AllocateNewEntity(entity_id_t preferredId); /** * Constructs a component of type 'cid', initialised with data 'paramNode', * and attaches it to entity 'ent'. * * @return true on success; false on failure, and logs an error message */ bool AddComponent(CEntityHandle ent, ComponentTypeId cid, const CParamNode& paramNode); /** * Add all system components to the system entity (skip the scripted components or the AI components on demand) */ void AddSystemComponents(bool skipScriptedComponents, bool skipAI); /** * Adds an externally-created component, so that it is returned by QueryInterface * but does not get destroyed and does not receive messages from the component manager. * (This is intended for unit tests that need to add mock objects the tested components * expect to exist.) */ void AddMockComponent(CEntityHandle ent, InterfaceId iid, IComponent& component); /** * Allocates a component object of type 'cid', and attaches it to entity 'ent'. * (The component's Init is not called here - either Init or Deserialize must be called * before using the returned object.) */ IComponent* ConstructComponent(CEntityHandle ent, ComponentTypeId cid); /** * Constructs an entity based on the given template, and adds it the world with * entity ID @p ent. There should not be any existing components with that entity ID. * @return ent, or INVALID_ENTITY on error */ entity_id_t AddEntity(const std::wstring& templateName, entity_id_t ent); /** * Destroys all the components belonging to the specified entity when FlushDestroyedComponents is called. * Has no effect if the entity does not exist, or has already been added to the destruction queue. */ void DestroyComponentsSoon(entity_id_t ent); /** * Does the actual destruction of components from DestroyComponentsSoon. * This must not be called if the component manager is on the call stack (since it * will break internal iterators). */ void FlushDestroyedComponents(); IComponent* QueryInterface(entity_id_t ent, InterfaceId iid) const; - typedef std::vector > InterfaceList; + typedef std::pair InterfacePair; + typedef std::vector InterfaceList; typedef boost::unordered_map InterfaceListUnordered; InterfaceList GetEntitiesWithInterface(InterfaceId iid) const; const InterfaceListUnordered& GetEntitiesWithInterfaceUnordered(InterfaceId iid) const; /** * Send a message, targeted at a particular entity. The message will be received by any * components of that entity which subscribed to the message type, and by any other components * that subscribed globally to the message type. */ void PostMessage(entity_id_t ent, const CMessage& msg); /** * Send a message, not targeted at any particular entity. The message will be received by any * components that subscribed (either globally or not) to the message type. */ void BroadcastMessage(const CMessage& msg); /** * Resets the dynamic simulation state (deletes all entities, resets entity ID counters; * doesn't unload/reload component scripts). */ void ResetState(); // Various state serialization functions: bool ComputeStateHash(std::string& outHash, bool quick); bool DumpDebugState(std::ostream& stream, bool includeDebugInfo); // FlushDestroyedComponents must be called before SerializeState (since the destruction queue // won't get serialized) bool SerializeState(std::ostream& stream); bool DeserializeState(std::istream& stream); std::string GenerateSchema(); ScriptInterface& GetScriptInterface() { return m_ScriptInterface; } private: // Implementations of functions exposed to scripts static void Script_RegisterComponentType_Common(ScriptInterface::CxPrivate* pCxPrivate, int iid, std::string cname, JS::HandleValue ctor, bool reRegister, bool systemComponent); static void Script_RegisterComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, std::string cname, JS::HandleValue ctor); static void Script_RegisterSystemComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, std::string cname, JS::HandleValue ctor); static void Script_ReRegisterComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, std::string cname, JS::HandleValue ctor); static void Script_RegisterInterface(ScriptInterface::CxPrivate* pCxPrivate, std::string name); static void Script_RegisterMessageType(ScriptInterface::CxPrivate* pCxPrivate, std::string name); static void Script_RegisterGlobal(ScriptInterface::CxPrivate* pCxPrivate, std::string name, JS::HandleValue value); static IComponent* Script_QueryInterface(ScriptInterface::CxPrivate* pCxPrivate, int ent, int iid); static std::vector Script_GetEntitiesWithInterface(ScriptInterface::CxPrivate* pCxPrivate, int iid); static std::vector Script_GetComponentsWithInterface(ScriptInterface::CxPrivate* pCxPrivate, int iid); static void Script_PostMessage(ScriptInterface::CxPrivate* pCxPrivate, int ent, int mtid, JS::HandleValue data); static void Script_BroadcastMessage(ScriptInterface::CxPrivate* pCxPrivate, int mtid, JS::HandleValue data); static int Script_AddEntity(ScriptInterface::CxPrivate* pCxPrivate, std::string templateName); static int Script_AddLocalEntity(ScriptInterface::CxPrivate* pCxPrivate, std::string templateName); static void Script_DestroyEntity(ScriptInterface::CxPrivate* pCxPrivate, int ent); static void Script_FlushDestroyedEntities(ScriptInterface::CxPrivate* pCxPrivate); static JS::Value Script_ReadJSONFile(ScriptInterface::CxPrivate* pCxPrivate, std::wstring fileName); static JS::Value Script_ReadCivJSONFile(ScriptInterface::CxPrivate* pCxPrivate, std::wstring fileName); static std::vector Script_FindJSONFiles(ScriptInterface::CxPrivate* pCxPrivate, std::wstring subPath, bool recursive); static JS::Value ReadJSONFile(ScriptInterface::CxPrivate* pCxPrivate, std::wstring filePath, std::wstring fileName); // callback function to handle recursively finding files in a directory static Status FindJSONFilesCallback(const VfsPath&, const CFileInfo&, const uintptr_t); CMessage* ConstructMessage(int mtid, JS::HandleValue data); void SendGlobalMessage(entity_id_t ent, const CMessage& msg); void FlattenDynamicSubscriptions(); void RemoveComponentDynamicSubscriptions(IComponent* component); ComponentTypeId GetScriptWrapper(InterfaceId iid); CEntityHandle AllocateEntityHandle(entity_id_t ent); ScriptInterface m_ScriptInterface; CSimContext& m_SimContext; CEntityHandle m_SystemEntity; ComponentTypeId m_CurrentComponent; // used when loading component types bool m_CurrentlyHotloading; // TODO: some of these should be vectors std::map m_ComponentTypesById; std::vector m_ScriptedSystemComponents; std::vector > m_ComponentsByInterface; // indexed by InterfaceId std::map > m_ComponentsByTypeId; std::map > m_LocalMessageSubscriptions; std::map > m_GlobalMessageSubscriptions; std::map m_ComponentTypeIdsByName; std::map m_MessageTypeIdsByName; std::map m_MessageTypeNamesById; std::map m_InterfaceIdsByName; std::map m_DynamicMessageSubscriptionsNonsync; std::map > m_DynamicMessageSubscriptionsNonsyncByComponent; std::map m_ComponentCaches; // TODO: maintaining both ComponentsBy* is nasty; can we get rid of one, // while keeping QueryInterface and PostMessage sufficiently efficient? std::vector m_DestructionQueue; ComponentTypeId m_NextScriptComponentTypeId; entity_id_t m_NextEntityId; entity_id_t m_NextLocalEntityId; boost::rand48 m_RNG; friend class TestComponentManager; }; #endif // INCLUDED_COMPONENTMANAGER