diff --git a/binaries/data/mods/public/simulation/templates/special/territory_block.xml b/binaries/data/mods/public/simulation/templates/special/territory_block.xml index de25a9cc24..45d3ce1b1a 100644 --- a/binaries/data/mods/public/simulation/templates/special/territory_block.xml +++ b/binaries/data/mods/public/simulation/templates/special/territory_block.xml @@ -28,6 +28,8 @@ structures/hellenes/wall_med.xml - 100 + 64 + 0 + 0 diff --git a/binaries/data/mods/public/simulation/templates/special/territory_pull.xml b/binaries/data/mods/public/simulation/templates/special/territory_pull.xml index 0db6599291..206c2451e9 100644 --- a/binaries/data/mods/public/simulation/templates/special/territory_pull.xml +++ b/binaries/data/mods/public/simulation/templates/special/territory_pull.xml @@ -29,5 +29,7 @@ 0 + 0 + 0 diff --git a/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml b/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml index ae05252c30..a7f54d5daf 100644 --- a/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml +++ b/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml @@ -67,4 +67,8 @@ units/{civ}_support_female_citizen + + 256 + 65536 + diff --git a/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml b/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml index fd28e004f7..494213339e 100644 --- a/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml +++ b/binaries/data/mods/public/simulation/templates/template_structure_civic_house.xml @@ -54,4 +54,8 @@ units/{civ}_support_female_citizen + + 64 + 65536 + diff --git a/source/simulation2/components/CCmpTerritoryInfluence.cpp b/source/simulation2/components/CCmpTerritoryInfluence.cpp index ae1904a5d4..dd7ec0a9de 100644 --- a/source/simulation2/components/CCmpTerritoryInfluence.cpp +++ b/source/simulation2/components/CCmpTerritoryInfluence.cpp @@ -29,19 +29,37 @@ public: DEFAULT_COMPONENT_ALLOCATOR(TerritoryInfluence) - u8 m_Cost; + int m_Cost; + u32 m_Weight; + u32 m_Radius; static std::string GetSchema() { return - "" + "" + "" + "" + "255" + "" + "" + "" + "" + "" + "" + "" "" ""; } virtual void Init(const CParamNode& paramNode) { - m_Cost = paramNode.GetChild("OverrideCost").ToInt(); + if (paramNode.GetChild("OverrideCost").IsOk()) + m_Cost = paramNode.GetChild("OverrideCost").ToInt(); + else + m_Cost = -1; + + m_Weight = paramNode.GetChild("Weight").ToInt(); + m_Radius = paramNode.GetChild("Radius").ToInt(); } virtual void Deinit() @@ -57,11 +75,20 @@ public: Init(paramNode); } - virtual u8 GetCost() + virtual int GetCost() { return m_Cost; } + virtual u32 GetWeight() + { + return m_Weight; + } + + virtual u32 GetRadius() + { + return m_Radius; + } }; REGISTER_COMPONENT_TYPE(TerritoryInfluence) diff --git a/source/simulation2/components/CCmpTerritoryManager.cpp b/source/simulation2/components/CCmpTerritoryManager.cpp index a167d079ab..18b0f92187 100644 --- a/source/simulation2/components/CCmpTerritoryManager.cpp +++ b/source/simulation2/components/CCmpTerritoryManager.cpp @@ -27,6 +27,7 @@ #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/ICmpPosition.h" #include "simulation2/components/ICmpSettlement.h" @@ -167,48 +168,72 @@ public: * 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(Grid& grid); + void RasteriseInfluences(CComponentManager::InterfaceList& infls, Grid& grid); }; REGISTER_COMPONENT_TYPE(TerritoryManager) /* - -We compute territory influences with a kind of best-first search: - 1) Initialise an 'open' list with tiles that contain settlements (annotated with - territory ID) with initial cost 0 - 2) Pick the lowest cost tile from 'item' - 3) For each neighbour which has not already been assigned to a territory, - assign it to this territory and compute its new cost (effectively the - distance from the associated settlement) and add to 'open' - 4) Go to 2 until 'open' is empty - +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> OpenQueue; +typedef PriorityQueueHeap, u32, std::greater > OpenQueue; -static void ProcessNeighbour(u8 pid, u16 i, u16 j, u32 pg, bool diagonal, - Grid& grid, OpenQueue& queue, const Grid& influenceGrid) +static void ProcessNeighbour(u32 falloff, u16 i, u16 j, u32 pg, bool diagonal, + Grid& grid, OpenQueue& queue, const Grid& costGrid) { - // Ignore tiles that are already claimed - u8 id = grid.get(i, j); - if (id) + 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; - // Base cost for moving onto this tile - u32 dg = diagonal ? 362 : 256; + u32 g = pg - dg; // cost to this tile = cost to predecessor - falloff from predecessor - // Adjust cost based on this tile's influences - dg *= influenceGrid.get(i, j); - - u32 g = pg + dg; // cost to this tile = cost to predecessor + delta from predecessor - - grid.set(i, j, pid); + 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) +{ + u32 tilesW = grid.m_W; + u32 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, x-1, z, tile.rank, false, grid, openTiles, costGrid); + if (x < tilesW-1) + ProcessNeighbour(falloff, x+1, z, tile.rank, false, grid, openTiles, costGrid); + if (z > 0) + ProcessNeighbour(falloff, x, z-1, tile.rank, false, grid, openTiles, costGrid); + if (z < tilesH-1) + ProcessNeighbour(falloff, x, z+1, tile.rank, false, grid, openTiles, costGrid); + if (x > 0 && z > 0) + ProcessNeighbour(falloff, x-1, z-1, tile.rank, true, grid, openTiles, costGrid); + if (x > 0 && z < tilesH-1) + ProcessNeighbour(falloff, x-1, z+1, tile.rank, true, grid, openTiles, costGrid); + if (x < tilesW-1 && z > 0) + ProcessNeighbour(falloff, x+1, z-1, tile.rank, true, grid, openTiles, costGrid); + if (x < tilesW-1 && z < tilesH-1) + ProcessNeighbour(falloff, x+1, z+1, tile.rank, true, grid, openTiles, costGrid); + } +} + void CCmpTerritoryManager::CalculateTerritories() { PROFILE("CalculateTerritories"); @@ -220,98 +245,124 @@ void CCmpTerritoryManager::CalculateTerritories() uint32_t tilesW = cmpTerrain->GetVerticesPerSide() - 1; uint32_t tilesH = cmpTerrain->GetVerticesPerSide() - 1; - Grid influenceGrid(tilesW, tilesH); - - RasteriseInfluences(influenceGrid); - SAFE_DELETE(m_Territories); m_Territories = new Grid(tilesW, tilesH); + // Compute terrain-passability-dependent costs per tile + Grid influenceGrid(tilesW, tilesH); + CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); ICmpPathfinder::pass_class_t passClass = cmpPathfinder->GetPassabilityClass("default"); const Grid& passGrid = cmpPathfinder->GetPassabilityGrid(); - - // Adjust influenceGrid so it contains terrain-passability-dependent costs, - // unless overridden by existing values in influenceGrid for (u32 j = 0; j < tilesH; ++j) { for (u32 i = 0; i < tilesW; ++i) { u8 cost; - u8 inflCost = influenceGrid.get(i, j); - if (inflCost) - { - cost = inflCost-1; // undo RasteriseInfluences's offset - } + if (passGrid.get(i, j) & passClass) + cost = 4; // TODO: should come from some XML file else - { - if (passGrid.get(i, j) & passClass) - cost = 100; - else - cost = 1; - } + cost = 1; influenceGrid.set(i, j, cost); } } - OpenQueue openTiles; + // Find all territory influence entities + CComponentManager::InterfaceList influences = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_TerritoryInfluence); - // Initialise open list with all settlements + // Allow influence entities to override the terrain costs + RasteriseInfluences(influences, influenceGrid); - CComponentManager::InterfaceList settlements = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_Settlement); - u8 id = 1; - for (CComponentManager::InterfaceList::iterator it = settlements.begin(); it != settlements.end(); ++it) + // Split influence entities into per-player lists, ignoring any with invalid properties + std::map > influenceEntities; + for (CComponentManager::InterfaceList::iterator it = influences.begin(); it != influences.end(); ++it) { - entity_id_t settlement = it->first; - CmpPtr cmpPosition(GetSimContext(), settlement); + // 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; + + CmpPtr cmpOwnership(GetSimContext(), it->first); + if (cmpOwnership.null()) + continue; + + // Ignore Gaia and unassigned + player_id_t owner = cmpOwnership->GetOwner(); + if (owner <= 0) + continue; + + // Ignore if invalid position + CmpPtr cmpPosition(GetSimContext(), it->first); if (cmpPosition.null() || !cmpPosition->IsInWorld()) continue; - // TODO: maybe we need to ignore settlements with owner -1, - // since they're probably destroyed - - CFixedVector2D pos = cmpPosition->GetPosition2D(); - int i = clamp((pos.X / (int)CELL_SIZE).ToInt_RoundToNegInfinity(), 0, (int)tilesW-1); - int j = clamp((pos.Y / (int)CELL_SIZE).ToInt_RoundToNegInfinity(), 0, (int)tilesH-1); - - // Must avoid duplicates in the priority queue; ignore the settlement - // if there's already one on that tile - if (!m_Territories->get(i, j)) - { - m_Territories->set(i, j, id); - OpenQueue::Item tile = { std::make_pair((u16)i, (i16)j), 0 }; - openTiles.push(tile); - } - - id += 1; + influenceEntities[owner].push_back(it->first); } - while (!openTiles.empty()) + // 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 + + for (std::map >::iterator it = influenceEntities.begin(); it != influenceEntities.end(); ++it) { - OpenQueue::Item tile = openTiles.pop(); + Grid playerGrid(tilesW, tilesH); - // Get current tile's territory ID - u8 tid = m_Territories->get(tile.id.first, tile.id.second); + 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 - // 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(tid, x-1, z, tile.rank, false, *m_Territories, openTiles, influenceGrid); - if (x < tilesW-1) - ProcessNeighbour(tid, x+1, z, tile.rank, false, *m_Territories, openTiles, influenceGrid); - if (z > 0) - ProcessNeighbour(tid, x, z-1, tile.rank, false, *m_Territories, openTiles, influenceGrid); - if (z < tilesH-1) - ProcessNeighbour(tid, x, z+1, tile.rank, false, *m_Territories, openTiles, influenceGrid); - if (x > 0 && z > 0) - ProcessNeighbour(tid, x-1, z-1, tile.rank, true, *m_Territories, openTiles, influenceGrid); - if (x > 0 && z < tilesH-1) - ProcessNeighbour(tid, x-1, z+1, tile.rank, true, *m_Territories, openTiles, influenceGrid); - if (x < tilesW-1 && z > 0) - ProcessNeighbour(tid, x+1, z-1, tile.rank, true, *m_Territories, openTiles, influenceGrid); - if (x < tilesW-1 && z < tilesH-1) - ProcessNeighbour(tid, x+1, z+1, tile.rank, true, *m_Territories, openTiles, influenceGrid); + Grid entityGrid(tilesW, tilesH); + + CmpPtr cmpPosition(GetSimContext(), *eit); + CFixedVector2D pos = cmpPosition->GetPosition2D(); + int i = clamp((pos.X / (int)CELL_SIZE).ToInt_RoundToNegInfinity(), 0, (int)tilesW-1); + int j = clamp((pos.Y / (int)CELL_SIZE).ToInt_RoundToNegInfinity(), 0, (int)tilesH-1); + + CmpPtr cmpTerritoryInfluence(GetSimContext(), *eit); + u32 weight = cmpTerritoryInfluence->GetWeight(); + u32 radius = cmpTerritoryInfluence->GetRadius() / CELL_SIZE; + u32 falloff = weight / radius; // earlier check for GetRadius() == 0 prevents divide-by-zero + + // TODO: we should have some maximum value on weight, to avoid overflow + // when doing all the sums + + // 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); + + // Expand influences outwards + FloodFill(entityGrid, influenceGrid, openTiles, falloff); + + // TODO: we should do a sparse grid and only add the non-zero regions, for performance + for (u16 j = 0; j < entityGrid.m_H; ++j) + for (u16 i = 0; i < entityGrid.m_W; ++i) + playerGrid.set(i, j, playerGrid.get(i, j) + entityGrid.get(i, j)); + } + + 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) + { + player_id_t id = playerGrids[k].first; + m_Territories->set(i, j, (u8)id); + bestWeight = w; + } + } + } } } @@ -336,13 +387,16 @@ static void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z) // TODO: would be nice not to duplicate those two functions from CCmpObstructionManager.cpp -void CCmpTerritoryManager::RasteriseInfluences(Grid& grid) +void CCmpTerritoryManager::RasteriseInfluences(CComponentManager::InterfaceList& infls, Grid& grid) { - CComponentManager::InterfaceList infls = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_TerritoryInfluence); for (CComponentManager::InterfaceList::iterator it = infls.begin(); it != infls.end(); ++it) { ICmpTerritoryInfluence* cmpTerritoryInfluence = static_cast(it->second); + int cost = cmpTerritoryInfluence->GetCost(); + if (cost == -1) + continue; + CmpPtr cmpObstruction(GetSimContext(), it->first); if (cmpObstruction.null()) continue; @@ -351,8 +405,6 @@ void CCmpTerritoryManager::RasteriseInfluences(Grid& grid) if (!cmpObstruction->GetObstructionSquare(square)) continue; - u8 cost = cmpTerritoryInfluence->GetCost(); - CFixedVector2D halfSize(square.hw, square.hh); CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(square.u, square.v, halfSize); @@ -366,7 +418,7 @@ void CCmpTerritoryManager::RasteriseInfluences(Grid& grid) 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, cost+1); + grid.set(i, j, cost); } } diff --git a/source/simulation2/components/ICmpTerritoryInfluence.h b/source/simulation2/components/ICmpTerritoryInfluence.h index e760988a7f..7c6ab827cb 100644 --- a/source/simulation2/components/ICmpTerritoryInfluence.h +++ b/source/simulation2/components/ICmpTerritoryInfluence.h @@ -23,7 +23,16 @@ class ICmpTerritoryInfluence : public IComponent { public: - virtual u8 GetCost() = 0; + /** + * 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 int GetCost() = 0; + + virtual u32 GetWeight() = 0; + + virtual u32 GetRadius() = 0; DECLARE_INTERFACE_TYPE(TerritoryInfluence) }; diff --git a/source/simulation2/helpers/PriorityQueue.h b/source/simulation2/helpers/PriorityQueue.h index 5ca2a171f6..9e7c8e37ee 100644 --- a/source/simulation2/helpers/PriorityQueue.h +++ b/source/simulation2/helpers/PriorityQueue.h @@ -29,14 +29,14 @@ #define PRIORITYQUEUE_DEBUG 1 #endif -template +template struct QueueItemPriority { bool operator()(const Item& a, const Item& b) { - if (a.rank > b.rank) // higher costs are lower priority + if (CMP()(b.rank, a.rank)) // higher costs are lower priority return true; - if (a.rank < b.rank) + if (CMP()(a.rank, b.rank)) return false; // Need to tie-break to get a consistent ordering // TODO: Should probably tie-break on g or h or something, but don't bother for now @@ -57,7 +57,7 @@ struct QueueItemPriority * This is quite dreadfully slow in MSVC's debug STL implementation, * so we shouldn't use it unless we reimplement the heap functions more efficiently. */ -template +template > class PriorityQueueHeap { public: @@ -70,7 +70,7 @@ public: void push(const Item& item) { m_Heap.push_back(item); - push_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority()); + push_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority()); } Item* find(ID id) @@ -93,7 +93,7 @@ public: ENSURE(m_Heap[n].rank > newrank); #endif m_Heap[n].rank = newrank; - push_heap(m_Heap.begin(), m_Heap.begin()+n+1, QueueItemPriority()); + push_heap(m_Heap.begin(), m_Heap.begin()+n+1, QueueItemPriority()); return; } } @@ -105,7 +105,7 @@ public: ENSURE(m_Heap.size()); #endif Item r = m_Heap.front(); - pop_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority()); + pop_heap(m_Heap.begin(), m_Heap.end(), QueueItemPriority()); m_Heap.pop_back(); return r; } @@ -130,7 +130,7 @@ public: * It seems fractionally slower than a binary heap in optimised builds, but is * much simpler and less susceptible to MSVC's painfully slow debug STL. */ -template +template > class PriorityQueueList { public: @@ -171,7 +171,7 @@ public: size_t bestidx = m_List.size()-1; for (ssize_t i = (ssize_t)bestidx-1; i >= 0; --i) { - if (QueueItemPriority()(best, m_List[i])) + if (QueueItemPriority()(best, m_List[i])) { bestidx = i; best = m_List[i];