Implemented nested territory boundaries. Fixes #918.

Fixed out of bounds memory access in Atlas due to always using global
terrain in TerrainOverlay
Fixed wrong player ID calculation in TerritoryOverlay

This was SVN commit r10929.
This commit is contained in:
vts 2012-01-18 21:22:58 +00:00
parent 86e6f378e3
commit 91652bdf6e
9 changed files with 614 additions and 170 deletions

View File

@ -0,0 +1,219 @@
/* Copyright (C) 2012 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 <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "TerritoryBoundary.h"
#include <algorithm> // for reverse
#include "graphics/Terrain.h"
#include "simulation2/components/ICmpTerritoryManager.h"
std::vector<STerritoryBoundary> CTerritoryBoundaryCalculator::ComputeBoundaries(const Grid<u8>* territory)
{
std::vector<STerritoryBoundary> boundaries;
// Copy the territories grid so we can mess with it
Grid<u8> grid(*territory);
// Some constants for the border walk
CVector2D edgeOffsets[] = {
CVector2D(0.5f, 0.0f),
CVector2D(1.0f, 0.5f),
CVector2D(0.5f, 1.0f),
CVector2D(0.0f, 0.5f)
};
// syntactic sugar
const u8 TILE_BOTTOM = 0;
const u8 TILE_RIGHT = 1;
const u8 TILE_TOP = 2;
const u8 TILE_LEFT = 3;
const int CURVE_CW = -1;
const int CURVE_CCW = 1;
// === Find territory boundaries ===
//
// The territory boundaries delineate areas of tiles that belong to the same player, and that all have the same
// connected-to-a-root-influence-entity status (see also STerritoryBoundary for a more wordy definition). Note that the grid
// values contain bit-packed information (i.e. not just the owning player ID), so we must be careful to only compare grid
// values using the player ID and connected flag bits. The joint mask to select these is referred to as the discriminator mask.
//
// The idea is to scan the (i,j)-grid going up row by row and look for tiles that have a different territory assignment from
// the one right underneath it (or, if it's a tile on the first row, they need only have a territory assignment). These tiles
// are necessarily edge tiles of a territory, and hence a territory boundary must pass through their bottom edge. Therefore,
// we start tracing the outline of the territory starting from said bottom edge, and go CCW around the territory boundary.
// Tracing continues until the starting point is reached, at which point the boundary is complete.
//
// While tracing a boundary, every tile in which the boundary passes through the bottom edge are marked as 'processed', so that
// we know not to start a new run from these tiles when scanning continues (when the boundary is complete). This information
// is maintained in the grid values themselves by means of the 'processed' bit mask (stressing the importance of using the
// discriminator mask to compare only player ID and connected flag).
//
// Thus, we can identify the following conditions for starting a trace from a tile (i,j). Let g(i,j) indicate the
// discriminator grid value at position (i,j); then the conditions are:
// - g(i,j) != 0; the tile must not be neutral
// - j=0 or g(i,j) != g(i,j-1); the tile directly underneath it must have a different owner and/or connected flag
// - the tile must not already be marked as 'processed'
//
// Additionally, there is one more point to be made; the algorithm initially assumes it's tracing CCW around the territory.
// If it's tracing an inner edge, however, this will actually cause it to trace in the CW direction (because inner edges curve
// 'backwards' compared to the outer edges when starting the trace in the same direction). This turns out to actually be
// exactly what the renderer needs to render two territory boundaries on the same edge back-to-back (instead of overlapping
// each other).
//
// In either case, we keep track of the way the outline curves while we're tracing to determine whether we're going CW or CCW.
// If at some point we ever need to revert the winding order or external code needs to know about it explicitly, then we can
// do this by looking at a curvature value which we define to start at 0, and which is incremented by 1 for every CCW turn and
// decremented by 1 for every CW turn. Hence, a negative multiple of 4 means a CW winding order, and a positive one means CCW.
const int TERRITORY_DISCR_MASK = (ICmpTerritoryManager::TERRITORY_CONNECTED_MASK | ICmpTerritoryManager::TERRITORY_PLAYER_MASK);
// Try to find an assigned tile
for (u16 j = 0; j < grid.m_H; ++j)
{
for (u16 i = 0; i < grid.m_W; ++i)
{
// saved tile state; from MSB to LSB:
// processed bit, connected bit, player ID
u8 tileState = grid.get(i, j);
u8 tileDiscr = (tileState & TERRITORY_DISCR_MASK);
// ignore neutral tiles (note that tiles without an owner should never have the connected bit set)
if (!tileDiscr)
continue;
bool tileProcessed = ((tileState & ICmpTerritoryManager::TERRITORY_PROCESSED_MASK) != 0);
bool tileEligible = (j == 0 || tileDiscr != (grid.get(i, j-1) & TERRITORY_DISCR_MASK));
if (tileProcessed || !tileEligible)
continue;
// Found the first tile (which must be the lowest j value of any non-zero tile);
// start at the bottom edge of it and chase anticlockwise around the border until
// we reach the starting point again
int curvature = 0; // +1 for every CCW 90 degree turn, -1 for every CW 90 degree turn; must be multiple of 4 at the end
boundaries.push_back(STerritoryBoundary());
boundaries.back().owner = (tileState & ICmpTerritoryManager::TERRITORY_PLAYER_MASK);
boundaries.back().connected = (tileState & ICmpTerritoryManager::TERRITORY_CONNECTED_MASK) != 0;
std::vector<CVector2D>& points = boundaries.back().points;
u8 dir = TILE_BOTTOM;
u8 cdir = dir;
u16 ci = i, cj = j;
u16 maxi = (u16)(grid.m_W-1);
u16 maxj = (u16)(grid.m_H-1);
while (true)
{
points.push_back((CVector2D(ci, cj) + edgeOffsets[cdir]) * TERRAIN_TILE_SIZE);
// Given that we're on an edge on a continuous boundary and aiming anticlockwise,
// we can either carry on straight or turn left or turn right, so examine each
// of the three possible cases (depending on initial direction):
switch (cdir)
{
case TILE_BOTTOM:
// mark tile as processed so we don't start a new run from it after this one is complete
ENSURE(!(grid.get(ci, cj) & ICmpTerritoryManager::TERRITORY_PROCESSED_MASK));
grid.set(ci, cj, grid.get(ci, cj) | ICmpTerritoryManager::TERRITORY_PROCESSED_MASK);
if (ci < maxi && cj > 0 && (grid.get(ci+1, cj-1) & TERRITORY_DISCR_MASK) == tileDiscr)
{
++ci;
--cj;
cdir = TILE_LEFT;
curvature += CURVE_CW;
}
else if (ci < maxi && (grid.get(ci+1, cj) & TERRITORY_DISCR_MASK) == tileDiscr)
++ci;
else
{
cdir = TILE_RIGHT;
curvature += CURVE_CCW;
}
break;
case TILE_RIGHT:
if (ci < maxi && cj < maxj && (grid.get(ci+1, cj+1) & TERRITORY_DISCR_MASK) == tileDiscr)
{
++ci;
++cj;
cdir = TILE_BOTTOM;
curvature += CURVE_CW;
}
else if (cj < maxj && (grid.get(ci, cj+1) & TERRITORY_DISCR_MASK) == tileDiscr)
++cj;
else
{
cdir = TILE_TOP;
curvature += CURVE_CCW;
}
break;
case TILE_TOP:
if (ci > 0 && cj < maxj && (grid.get(ci-1, cj+1) & TERRITORY_DISCR_MASK) == tileDiscr)
{
--ci;
++cj;
cdir = TILE_RIGHT;
curvature += CURVE_CW;
}
else if (ci > 0 && (grid.get(ci-1, cj) & TERRITORY_DISCR_MASK) == tileDiscr)
--ci;
else
{
cdir = TILE_LEFT;
curvature += CURVE_CCW;
}
break;
case TILE_LEFT:
if (ci > 0 && cj > 0 && (grid.get(ci-1, cj-1) & TERRITORY_DISCR_MASK) == tileDiscr)
{
--ci;
--cj;
cdir = TILE_TOP;
curvature += CURVE_CW;
}
else if (cj > 0 && (grid.get(ci, cj-1) & TERRITORY_DISCR_MASK) == tileDiscr)
--cj;
else
{
cdir = TILE_BOTTOM;
curvature += CURVE_CCW;
}
break;
}
// Stop when we've reached the starting point again
if (ci == i && cj == j && cdir == dir)
break;
}
ENSURE(curvature != 0 && abs(curvature) % 4 == 0);
}
}
return boundaries;
}

View File

@ -0,0 +1,66 @@
/* Copyright (C) 2012 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 <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_TERRITORYBOUNDARY
#define INCLUDED_TERRITORYBOUNDARY
#include <vector>
#include "maths/Vector2D.h"
#include "simulation2/helpers/Grid.h"
#include "simulation2/helpers/Player.h"
/**
* Describes an outline of a territory, where the latter are understood to mean the largest sets of mutually connected tiles
* ('connected' as in the mathematical sense from graph theory) that are either all reachable or all unreachable from a root
* influence entity.
*
* Note that the latter property is also called the 'connected' flag in the territory manager terminology, because for tiles
* to be reachable from a root influence entity they must in fact be mathematically connected. Hence, you should not confuse
* the 'connected' flag with the pure mathematical concept of connectedness, because in the former it is implicitly
* understood that the connection is to a root influence entity.
*/
struct STerritoryBoundary
{
/// Is the territory enclosed by this boundary mathematically connected to (i.e. reachable from) a root influence entity?
bool connected;
player_id_t owner;
/// The boundary points, in clockwise order for inner boundaries and counter-clockwise order for outer boundaries.
/// Note: if you need a way to explicitly find out which winding order these are in, you can have
/// CTerritoryBoundCalculator::ComputeBoundaries set it during computation -- see its implementation for details.
std::vector<CVector2D> points;
};
/**
* Responsible for calculating territory boundaries, given an input territory map. Factored out for testing.
*/
class CTerritoryBoundaryCalculator
{
private:
CTerritoryBoundaryCalculator(){} // private ctor
public:
/**
* Computes and returns all territory boundaries on the provided territory map (see STerritoryBoundary for a definition).
* The result includes both inner and outer territory boundaries. Outer boundaries have their points in CCW order, inner
* boundaries have them in CW order (because this matches the winding orders needed by the renderer to offset them
* inwards/outwards appropriately).
*/
static std::vector<STerritoryBoundary> ComputeBoundaries(const Grid<u8>* territories);
};
#endif

View File

@ -20,6 +20,7 @@
#include "TerrainOverlay.h"
#include "graphics/Terrain.h"
#include "simulation2/system/SimContext.h"
#include "lib/ogl.h"
#include "maths/MathUtil.h"
#include "ps/Game.h"
@ -68,7 +69,8 @@ struct render1st
static std::vector<std::pair<TerrainOverlay*, int> > g_TerrainOverlayList;
TerrainOverlay::TerrainOverlay(int priority)
TerrainOverlay::TerrainOverlay(const CSimContext& simContext, int priority /* = 100 */)
: m_Terrain(&simContext.GetTerrain())
{
// Add to global list of overlays
g_TerrainOverlayList.push_back(std::make_pair(this, priority));
@ -140,7 +142,8 @@ void TerrainOverlay::GetTileExtents(
void TerrainOverlay::Render()
{
m_Terrain = g_Game->GetWorld()->GetTerrain();
if (!m_Terrain)
return; // should never happen, but let's play it safe
StartRender();

View File

@ -25,6 +25,7 @@
struct CColor;
class CTerrain;
class CSimContext;
/**
* Base class for (relatively) simple drawing of
@ -45,6 +46,8 @@ class TerrainOverlay
{
public:
virtual ~TerrainOverlay();
private:
TerrainOverlay(){} // private default ctor (must be subclassed)
protected:
/**
@ -59,7 +62,7 @@ protected:
*
* @param priority controls the order of drawing
*/
TerrainOverlay(int priority = 100);
TerrainOverlay(const CSimContext& simContext, int priority = 100);
/**
* Override to perform processing at the start of the overlay rendering,

View File

@ -103,7 +103,8 @@ class PathfinderOverlay : public TerrainOverlay
public:
CCmpPathfinder& m_Pathfinder;
PathfinderOverlay(CCmpPathfinder& pathfinder) : m_Pathfinder(pathfinder)
PathfinderOverlay(CCmpPathfinder& pathfinder)
: TerrainOverlay(pathfinder.GetSimContext()), m_Pathfinder(pathfinder)
{
}

View File

@ -23,6 +23,7 @@
#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 "ps/Overlay.h"
@ -53,7 +54,7 @@ class TerritoryOverlay : public TerrainOverlay
public:
CCmpTerritoryManager& m_TerritoryManager;
TerritoryOverlay(CCmpTerritoryManager& manager) : m_TerritoryManager(manager) { }
TerritoryOverlay(CCmpTerritoryManager& manager);
virtual void StartRender();
virtual void ProcessTile(ssize_t i, ssize_t j);
};
@ -82,7 +83,7 @@ public:
float m_BorderThickness;
float m_BorderSeparation;
// Player ID in lower 7 bits; connected flag in high bit
// Player ID in lower 6 bits; connected flag in bit 7, processed flag in high bit
Grid<u8>* m_Territories;
// Set to true when territories change; will send a TerritoriesChanged message
@ -247,14 +248,7 @@ public:
*/
void RasteriseInfluences(CComponentManager::InterfaceList& infls, Grid<u8>& grid);
struct TerritoryBoundary
{
bool connected;
player_id_t owner;
std::vector<CVector2D> points;
};
std::vector<TerritoryBoundary> ComputeBoundaries();
std::vector<STerritoryBoundary> ComputeBoundaries();
void UpdateBoundaryLines();
@ -265,7 +259,6 @@ public:
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,
@ -349,8 +342,9 @@ void CCmpTerritoryManager::CalculateTerritories()
Grid<u8> influenceGrid(tilesW, tilesH);
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
ICmpPathfinder::pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted");
ICmpPathfinder::pass_class_t passClassDefault = cmpPathfinder->GetPassabilityClass("default");
ICmpPathfinder::pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted");
const Grid<u16>& passGrid = cmpPathfinder->GetPassabilityGrid();
for (u16 j = 0; j < tilesH; ++j)
{
@ -393,7 +387,7 @@ void CCmpTerritoryManager::CalculateTerritories()
if (owner <= 0)
continue;
// We only have 7 bits to store tile ownership, so ignore unrepresentable players
// We only have so many bits to store tile ownership, so ignore unrepresentable players
if (owner > TERRITORY_PLAYER_MASK)
continue;
@ -592,153 +586,14 @@ void CCmpTerritoryManager::RasteriseInfluences(CComponentManager::InterfaceList&
}
}
std::vector<CCmpTerritoryManager::TerritoryBoundary> CCmpTerritoryManager::ComputeBoundaries()
std::vector<STerritoryBoundary> CCmpTerritoryManager::ComputeBoundaries()
{
PROFILE("ComputeBoundaries");
std::vector<CCmpTerritoryManager::TerritoryBoundary> boundaries;
CalculateTerritories();
ENSURE(m_Territories);
// Copy the territories grid so we can mess with it
Grid<u8> grid (*m_Territories);
// Some constants for the border walk
CVector2D edgeOffsets[] = {
CVector2D(0.5f, 0.0f),
CVector2D(1.0f, 0.5f),
CVector2D(0.5f, 1.0f),
CVector2D(0.0f, 0.5f)
};
// Try to find an assigned tile
for (u16 j = 0; j < grid.m_H; ++j)
{
for (u16 i = 0; i < grid.m_W; ++i)
{
u8 owner = grid.get(i, j);
if (owner)
{
// Found the first tile (which must be the lowest j value of any non-zero tile);
// start at the bottom edge of it and chase anticlockwise around the border until
// we reach the starting point again
boundaries.push_back(TerritoryBoundary());
boundaries.back().connected = (owner & TERRITORY_CONNECTED_MASK) != 0;
boundaries.back().owner = (owner & TERRITORY_PLAYER_MASK);
std::vector<CVector2D>& points = boundaries.back().points;
u8 dir = 0; // 0 == bottom edge of tile, 1 == right, 2 == top, 3 == left
u8 cdir = dir;
u16 ci = i, cj = j;
u16 maxi = (u16)(grid.m_W-1);
u16 maxj = (u16)(grid.m_H-1);
while (true)
{
points.push_back((CVector2D(ci, cj) + edgeOffsets[cdir]) * TERRAIN_TILE_SIZE);
// Given that we're on an edge on a continuous boundary and aiming anticlockwise,
// we can either carry on straight or turn left or turn right, so examine each
// of the three possible cases (depending on initial direction):
switch (cdir)
{
case 0:
if (ci < maxi && cj > 0 && grid.get(ci+1, cj-1) == owner)
{
++ci;
--cj;
cdir = 3;
}
else if (ci < maxi && grid.get(ci+1, cj) == owner)
++ci;
else
cdir = 1;
break;
case 1:
if (ci < maxi && cj < maxj && grid.get(ci+1, cj+1) == owner)
{
++ci;
++cj;
cdir = 0;
}
else if (cj < maxj && grid.get(ci, cj+1) == owner)
++cj;
else
cdir = 2;
break;
case 2:
if (ci > 0 && cj < maxj && grid.get(ci-1, cj+1) == owner)
{
--ci;
++cj;
cdir = 1;
}
else if (ci > 0 && grid.get(ci-1, cj) == owner)
--ci;
else
cdir = 3;
break;
case 3:
if (ci > 0 && cj > 0 && grid.get(ci-1, cj-1) == owner)
{
--ci;
--cj;
cdir = 2;
}
else if (cj > 0 && grid.get(ci, cj-1) == owner)
--cj;
else
cdir = 0;
break;
}
// Stop when we've reached the starting point again
if (ci == i && cj == j && cdir == dir)
break;
}
// Zero out this whole territory with a simple flood fill, so we don't
// process it a second time
std::vector<std::pair<u16, u16> > tileStack;
#define MARK_AND_PUSH(i, j) STMT(grid.set(i, j, 0); 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
}
}
}
return boundaries;
return CTerritoryBoundaryCalculator::ComputeBoundaries(m_Territories);
}
void CCmpTerritoryManager::UpdateBoundaryLines()
@ -751,7 +606,7 @@ void CCmpTerritoryManager::UpdateBoundaryLines()
if (!CRenderer::IsInitialised())
return;
std::vector<CCmpTerritoryManager::TerritoryBoundary> boundaries = ComputeBoundaries();
std::vector<STerritoryBoundary> boundaries = ComputeBoundaries();
CTextureProperties texturePropsBase("art/textures/misc/territory_border.png");
texturePropsBase.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE);
@ -875,6 +730,10 @@ bool CCmpTerritoryManager::IsConnected(entity_pos_t x, entity_pos_t z)
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();
@ -885,18 +744,18 @@ 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);
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;
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;
}
}

View File

@ -29,8 +29,9 @@ class ICmpTerritoryManager : public IComponent
public:
virtual bool NeedUpdate(size_t* dirtyID) = 0;
static const int TERRITORY_PLAYER_MASK = 0x7F;
static const int TERRITORY_CONNECTED_MASK = 0x80;
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;

View File

@ -0,0 +1,290 @@
/* Copyright (C) 2012 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 <http://www.gnu.org/licenses/>.
*/
#include "simulation2/system/ComponentTest.h"
#include "ps/CStr.h"
#include "graphics/Terrain.h"
#include "graphics/TerritoryBoundary.h"
#include "simulation2/helpers/Grid.h"
class TestCmpTerritoryManager : public CxxTest::TestSuite
{
public:
void setUp()
{
CxxTest::setAbortTestOnFail(true);
}
void tearDown()
{
}
void test_boundaries()
{
Grid<u8> grid = GetGrid("--------"
"777777--"
"777777--"
"777777--"
"--------", 8, 5);
std::vector<STerritoryBoundary> boundaries = CTerritoryBoundaryCalculator::ComputeBoundaries(&grid);
TS_ASSERT_EQUALS(1, boundaries.size());
TS_ASSERT_EQUALS(18, boundaries[0].points.size()); // 2x6 + 2x3
TS_ASSERT_EQUALS(7, boundaries[0].owner);
TS_ASSERT_EQUALS(false, boundaries[0].connected); // high bits aren't set by GetGrid
// assumes CELL_SIZE is 4; dealt with in TestBoundaryPointsEqual
int expectedPoints[][2] = {{ 2, 4}, { 6, 4}, {10, 4}, {14, 4}, {18, 4}, {22, 4},
{24, 6}, {24,10}, {24,14},
{22,16}, {18,16}, {14,16}, {10,16}, { 6,16}, { 2,16},
{ 0,14}, { 0,10}, { 0, 6}};
TestBoundaryPointsEqual(boundaries[0].points, expectedPoints);
}
void test_nested_boundaries1()
{
// test case from ticket #918; contains single-tile territories with double borders
Grid<u8> grid1 = GetGrid("--------"
"-111111-"
"-1-1213-"
"-111111-"
"--------", 8, 5);
std::vector<STerritoryBoundary> boundaries = CTerritoryBoundaryCalculator::ComputeBoundaries(&grid1);
size_t expectedNumBoundaries = 5;
TS_ASSERT_EQUALS(expectedNumBoundaries, boundaries.size());
STerritoryBoundary* onesOuter = NULL;
STerritoryBoundary* onesInner0 = NULL; // inner border around the neutral tile
STerritoryBoundary* onesInner2 = NULL; // inner border around the '2' tile
STerritoryBoundary* twosOuter = NULL;
STerritoryBoundary* threesOuter = NULL;
// expected number of points (!) in the inner boundaries for terrain 1 (there are two with the same size)
size_t onesInnerNumExpectedPoints = 4;
for (size_t i=0; i<expectedNumBoundaries; i++)
{
STerritoryBoundary& boundary = boundaries[i];
switch (boundary.owner)
{
case 1:
// to figure out which 1-boundary is which, we can use the number of points to distinguish between outer and inner,
// and within the inners we can split them by their X value (onesInner0 is the leftmost one, onesInner1 the
// rightmost one).
if (boundary.points.size() != onesInnerNumExpectedPoints)
{
TSM_ASSERT_EQUALS("Found multiple outer boundaries for territory owned by player 1", onesOuter, (STerritoryBoundary*) NULL);
onesOuter = &boundary;
}
else
{
TS_ASSERT_EQUALS(onesInnerNumExpectedPoints, boundary.points.size()); // all inner boundaries are of size 4
if (boundary.points[0].X < 14.f)
{
// leftmost inner boundary, i.e. onesInner0
TSM_ASSERT_EQUALS("Found multiple leftmost inner boundaries for territory owned by player 1", onesInner0, (STerritoryBoundary*) NULL);
onesInner0 = &boundary;
}
else
{
TSM_ASSERT_EQUALS("Found multiple rightmost inner boundaries for territory owned by player 1", onesInner2, (STerritoryBoundary*) NULL);
onesInner2 = &boundary;
}
}
break;
case 2:
TSM_ASSERT_EQUALS("Too many boundaries for territory owned by player 2", twosOuter, (STerritoryBoundary*) NULL);
twosOuter = &boundary;
break;
case 3:
TSM_ASSERT_EQUALS("Too many boundaries for territory owned by player 3", threesOuter, (STerritoryBoundary*) NULL);
threesOuter = &boundary;
break;
default:
TS_FAIL("Unexpected tile owner");
break;
}
}
TS_ASSERT_DIFFERS(onesOuter, (STerritoryBoundary*) NULL);
TS_ASSERT_DIFFERS(onesInner0, (STerritoryBoundary*) NULL);
TS_ASSERT_DIFFERS(onesInner2, (STerritoryBoundary*) NULL);
TS_ASSERT_DIFFERS(twosOuter, (STerritoryBoundary*) NULL);
TS_ASSERT_DIFFERS(threesOuter, (STerritoryBoundary*) NULL);
TS_ASSERT_EQUALS(onesOuter->points.size(), 20);
TS_ASSERT_EQUALS(onesInner0->points.size(), 4);
TS_ASSERT_EQUALS(onesInner2->points.size(), 4);
TS_ASSERT_EQUALS(twosOuter->points.size(), 4);
TS_ASSERT_EQUALS(threesOuter->points.size(), 4);
int onesOuterExpectedPoints[][2] = {{6,4}, {10,4}, {14,4}, {18,4}, {22,4}, {26,4},
{28,6}, {26,8}, {24,10}, {26,12}, {28,14},
{26,16}, {22,16}, {18,16}, {14,16}, {10,16}, {6,16},
{4,14}, {4,10}, {4,6}};
int onesInner0ExpectedPoints[][2] = {{10,12}, {12,10}, {10,8}, {8,10}};
int onesInner2ExpectedPoints[][2] = {{18,12}, {20,10}, {18,8}, {16,10}};
int twosOuterExpectedPoints[][2] = {{18,8}, {20,10}, {18,12}, {16,10}};
int threesOuterExpectedPoints[][2] = {{26,8}, {28,10}, {26,12}, {24,10}};
TestBoundaryPointsEqual(onesOuter->points, onesOuterExpectedPoints);
TestBoundaryPointsEqual(onesInner0->points, onesInner0ExpectedPoints);
TestBoundaryPointsEqual(onesInner2->points, onesInner2ExpectedPoints);
TestBoundaryPointsEqual(twosOuter->points, twosOuterExpectedPoints);
TestBoundaryPointsEqual(threesOuter->points, threesOuterExpectedPoints);
}
void test_nested_boundaries2()
{
Grid<u8> grid1 = GetGrid("-22222-"
"-2---2-"
"-2-1123"
"-2-1123"
"-2-2223"
"-222333", 7, 6);
std::vector<STerritoryBoundary> boundaries = CTerritoryBoundaryCalculator::ComputeBoundaries(&grid1);
// There should be two boundaries found for the territory of 2's (one outer and one inner edge), plus two regular
// outer edges of the territories of 1's and 3's. The order in which they're returned doesn't matter though, so
// we should first detect which one is which.
size_t expectedNumBoundaries = 4;
TS_ASSERT_EQUALS(expectedNumBoundaries, boundaries.size());
STerritoryBoundary* onesOuter = NULL;
STerritoryBoundary* twosOuter = NULL;
STerritoryBoundary* twosInner = NULL;
STerritoryBoundary* threesOuter = NULL;
for (size_t i=0; i < expectedNumBoundaries; i++)
{
STerritoryBoundary& boundary = boundaries[i];
switch (boundary.owner)
{
case 1:
TSM_ASSERT_EQUALS("Too many boundaries for territory owned by player 1", onesOuter, (STerritoryBoundary*) NULL);
onesOuter = &boundary;
break;
case 3:
TSM_ASSERT_EQUALS("Too many boundaries for territory owned by player 3", threesOuter, (STerritoryBoundary*) NULL);
threesOuter = &boundary;
break;
case 2:
// assign twosOuter first, then twosInner last; we'll swap them afterwards if needed
if (twosOuter == NULL)
twosOuter = &boundary;
else if (twosInner == NULL)
twosInner = &boundary;
else
TS_FAIL("Too many boundaries for territory owned by player 2");
break;
default:
TS_FAIL("Unexpected tile owner");
break;
}
}
TS_ASSERT_DIFFERS(onesOuter, (STerritoryBoundary*) NULL);
TS_ASSERT_DIFFERS(twosOuter, (STerritoryBoundary*) NULL);
TS_ASSERT_DIFFERS(twosInner, (STerritoryBoundary*) NULL);
TS_ASSERT_DIFFERS(threesOuter, (STerritoryBoundary*) NULL);
TS_ASSERT_EQUALS(onesOuter->points.size(), 8);
TS_ASSERT_EQUALS(twosOuter->points.size(), 22);
TS_ASSERT_EQUALS(twosInner->points.size(), 14);
TS_ASSERT_EQUALS(threesOuter->points.size(), 14);
// See if we need to swap the outer and inner edges of the twos territories (uses the extremely simplistic
// heuristic of comparing the amount of points to determine which one is the outer one and which one the inner
// one (which does happen to work in this case though).
if (twosOuter->points.size() < twosInner->points.size())
{
STerritoryBoundary* tmp = twosOuter;
twosOuter = twosInner;
twosInner = tmp;
}
int onesOuterExpectedPoints[][2] = {{14, 8}, {18, 8}, {20,10}, {20,14}, {18,16}, {14,16}, {12,14}, {12,10}};
int twosOuterExpectedPoints[][2] = {{ 6, 0}, {10, 0}, {14, 0}, {16, 2}, {18, 4}, {22, 4},
{24, 6}, {24,10}, {24,14}, {24,18}, {24,22},
{22,24}, {18,24}, {14,24}, {10,24}, { 6,24},
{4, 22}, {4, 18}, {4, 14}, {4, 10}, { 4, 6}, { 4, 2}};
int twosInnerExpectedPoints[][2] = {{10,20}, {14,20}, {18,20}, {20,18}, {20,14}, {20,10}, {18, 8},
{14, 8}, {12, 6}, {10, 4}, { 8, 6}, { 8,10}, { 8,14}, { 8,18}};
int threesOuterExpectedPoints[][2] = {{18, 0}, {22, 0}, {26, 0}, {28, 2}, {28, 6}, {28,10}, {28,14}, {26,16},
{24,14}, {24,10}, {24, 6}, {22, 4}, {18, 4}, {16, 2}};
TestBoundaryPointsEqual(onesOuter->points, onesOuterExpectedPoints);
TestBoundaryPointsEqual(twosOuter->points, twosOuterExpectedPoints);
TestBoundaryPointsEqual(twosInner->points, twosInnerExpectedPoints);
TestBoundaryPointsEqual(threesOuter->points, threesOuterExpectedPoints);
}
private:
/// Parses a string representation of a grid into an actual Grid structure, such that the (i,j) axes are located in the bottom
/// left hand side of the map. Note: leaves all custom bits in the grid values at zero (anything outside
/// ICmpTerritoryManager::TERRITORY_PLAYER_MASK).
Grid<u8> GetGrid(std::string def, u16 w, u16 h)
{
Grid<u8> grid(w, h);
const char* chars = def.c_str();
for (u16 y=0; y<h; y++)
{
for (u16 x=0; x<w; x++)
{
char gridDefChar = chars[x+y*w];
if (gridDefChar == '-')
continue;
ENSURE('0' <= gridDefChar && gridDefChar <= '9');
u8 playerId = gridDefChar - '0';
grid.set(x, h-1-y, playerId);
}
}
return grid;
}
void TestBoundaryPointsEqual(std::vector<CVector2D> points, int expectedPoints[][2])
{
// TODO: currently relies on an exact point match, i.e. expectedPoints must be specified going CCW or CW (depending on
// whether we're testing an inner or an outer edge) starting from the exact same point that the algorithm happened to
// decide to start the run from. This is an algorithmic detail and is not considered to be part of the specification
// of the return value. Hence, this method should also accept 'expectedPoints' to be a cyclically shifted
// version of 'points', so that the starting position doesn't need to match exactly.
for (size_t i = 0; i < points.size(); i++)
{
// the input numbers in expectedPoints are defined under the assumption that CELL_SIZE is 4, so let's include
// a scaling factor to protect against that should CELL_SIZE ever change
TS_ASSERT_DELTA(points[i].X, float(expectedPoints[i][0]) * 4.f / TERRAIN_TILE_SIZE, 1e-7);
TS_ASSERT_DELTA(points[i].Y, float(expectedPoints[i][1]) * 4.f / TERRAIN_TILE_SIZE, 1e-7);
}
}
};

View File

@ -26,6 +26,8 @@
#include "lib/ogl.h"
#include "maths/MathUtil.h"
#include "renderer/TerrainOverlay.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/SimContext.h"
using namespace AtlasMessage;
@ -33,7 +35,7 @@ class BrushTerrainOverlay : public TerrainOverlay
{
public:
BrushTerrainOverlay(const Brush* brush)
: TerrainOverlay(300), m_Brush(brush)
: TerrainOverlay(g_Game->GetSimulation2()->GetSimContext(), 300), m_Brush(brush)
{
}