File: TerritoryBoundary.cpp

package info (click to toggle)
0ad 0.0.23.1-2
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 78,292 kB
  • sloc: cpp: 245,166; ansic: 200,249; python: 13,754; sh: 6,104; perl: 4,620; makefile: 977; xml: 810; java: 533; ruby: 229; erlang: 46; pascal: 30; sql: 21; tcl: 4
file content (223 lines) | stat: -rw-r--r-- 8,696 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/* 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/helpers/Pathfinding.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_BLINKING_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, blinking 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 blinking 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().blinking = (tileState & ICmpTerritoryManager::TERRITORY_BLINKING_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);

			// Size of a territory tile in metres
			float territoryTileSize = (Pathfinding::NAVCELL_SIZE * ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE).ToFloat();

			while (true)
			{
				points.push_back((CVector2D(ci, cj) + edgeOffsets[cdir]) * territoryTileSize);

				// 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;
}