warzone2100/src/map.cpp

1965 lines
55 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
This file is part of Warzone 2100.
Copyright (C) 1999-2004 Eidos Interactive
Copyright (C) 2005-2013 Warzone 2100 Project
Warzone 2100 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.
Warzone 2100 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 Warzone 2100; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* @file map.c
*
* Utility functions for the map data structure.
*
*/
#include <time.h>
#include "lib/framework/frame.h"
#include "lib/framework/endian_hack.h"
#include "lib/framework/file.h"
#include "lib/framework/physfs_ext.h"
#include "lib/ivis_opengl/tex.h"
#include "lib/netplay/netplay.h" // For syncDebug
#include "map.h"
#include "hci.h"
#include "projectile.h"
#include "display3d.h"
#include "game.h"
#include "texture.h"
#include "advvis.h"
#include "random.h"
#include "research.h"
#include "mission.h"
#include "gateway.h"
#include "wrappers.h"
#include "mapgrid.h"
#include "astar.h"
#include "fpath.h"
#include "levels.h"
#include "scriptfuncs.h"
#include "lib/framework/wzapp.h"
#define GAME_TICKS_FOR_DANGER (GAME_TICKS_PER_SEC * 2)
static WZ_THREAD *dangerThread = NULL;
static WZ_SEMAPHORE *dangerSemaphore = NULL;
static WZ_SEMAPHORE *dangerDoneSemaphore = NULL;
struct floodtile { uint8_t x; uint8_t y; };
static struct floodtile *floodbucket = NULL;
static int bucketcounter;
static UDWORD lastDangerUpdate = 0;
static int lastDangerPlayer = -1;
//scroll min and max values
SDWORD scrollMinX, scrollMaxX, scrollMinY, scrollMaxY;
/* Structure definitions for loading and saving map data */
struct MAP_SAVEHEADER // : public GAME_SAVEHEADER
{
char aFileType[4];
UDWORD version;
UDWORD width;
UDWORD height;
};
struct MAP_SAVETILE
{
UWORD texture;
UBYTE height;
};
struct GATEWAY_SAVEHEADER
{
UDWORD version;
UDWORD numGateways;
};
struct GATEWAY_SAVE
{
UBYTE x0,y0,x1,y1;
};
/* Sanity check definitions for the save struct file sizes */
#define SAVE_HEADER_SIZE 16
#define SAVE_TILE_SIZE 3
// Maximum expected return value from get height
#define MAX_HEIGHT (256 * ELEVATION_SCALE)
/* The size and contents of the map */
SDWORD mapWidth = 0, mapHeight = 0;
MAPTILE *psMapTiles = NULL;
uint8_t *psBlockMap[AUX_MAX];
uint8_t *psAuxMap[MAX_PLAYERS + AUX_MAX]; // yes, we waste one element... eyes wide open... makes API nicer
#define WATER_MIN_DEPTH 500
#define WATER_MAX_DEPTH (WATER_MIN_DEPTH + 400)
static void SetGroundForTile(const char *filename, const char *nametype);
static int getTextureType(const char *textureType);
static bool hasDecals(int i, int j);
static void SetDecals(const char *filename, const char *decal_type);
static void init_tileNames(int type);
/// The different ground types
GROUND_TYPE *psGroundTypes;
int numGroundTypes;
char *tilesetDir = NULL;
static int numTile_names;
static char *Tile_names = NULL;
#define ARIZONA 1
#define URBAN 2
#define ROCKIE 3
static int *map; // 3D array pointer that holds the texturetype
static bool *mapDecals; // array that tells us what tile is a decal
#define MAX_TERRAIN_TILES 0x0200 // max that we support (for now), see TILE_NUMMASK
/* Look up table that returns the terrain type of a given tile texture */
UBYTE terrainTypes[MAX_TILE_TEXTURES];
static void init_tileNames(int type)
{
char *pFileData = NULL;
char name[MAX_STR_LENGTH] = {'\0'};
int numlines = 0, i = 0, cnt = 0;
uint32_t fileSize = 0;
pFileData = fileLoadBuffer;
switch (type)
{
case ARIZONA:
{
if (!loadFileToBuffer("tileset/arizona_enum.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
{
debug(LOG_FATAL, "tileset/arizona_enum.txt not found. Aborting.");
abort();
}
sscanf(pFileData, "%255[^,'\r\n],%d%n", name, &numlines, &cnt);
pFileData += cnt;
if (strcmp("arizona_enum", name))
{
debug(LOG_FATAL, "%s found, but was expecting arizona_enum, aborting.", name);
abort();
}
break;
}
case URBAN:
{
if (!loadFileToBuffer("tileset/urban_enum.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
{
debug(LOG_FATAL, "tileset/urban_enum.txt not found. Aborting.");
abort();
}
sscanf(pFileData, "%255[^,'\r\n],%d%n", name, &numlines, &cnt);
pFileData += cnt;
if (strcmp("urban_enum", name))
{
debug(LOG_FATAL, "%s found, but was expecting urban_enum, aborting.", name);
abort();
}
break;
}
case ROCKIE:
{
if (!loadFileToBuffer("tileset/rockie_enum.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
{
debug(LOG_FATAL, "tileset/rockie_enum.txt not found. Aborting.");
abort();
}
sscanf(pFileData, "%255[^,'\r\n],%d%n", name, &numlines, &cnt);
pFileData += cnt;
if (strcmp("rockie_enum", name))
{
debug(LOG_FATAL, "%s found, but was expecting rockie_enum, aborting.", name);
abort();
}
break;
}
default:
debug(LOG_FATAL, "Unknown type (%d) given. Aborting.", type);
abort();
}
debug(LOG_TERRAIN, "name: %s, with %d entries", name, numlines);
if (numlines == 0 || numlines > MAX_TERRAIN_TILES)
{
debug(LOG_FATAL, "Rockie_enum parameter is out of range (%d). Aborting.", numlines);
abort();
}
numTile_names = numlines;
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
Tile_names = (char *)malloc(numlines * sizeof(char[MAX_STR_LENGTH]) );
memset(Tile_names, 0x0, (numlines * sizeof(char[MAX_STR_LENGTH])));
for (i=0; i < numlines; i++)
{
sscanf(pFileData, "%255[^,'\r\n]%n", &Tile_names[i*MAX_STR_LENGTH], &cnt);
pFileData += cnt;
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
}
}
// This is the main loading routine to get all the map's parameters set.
// Once it figures out what tileset we need, we then parse the files for that tileset.
// Currently, we only support 3 tilesets. Arizona, Urban, and Rockie
static bool mapLoadGroundTypes(void)
{
char *pFileData = NULL;
char tilename[MAX_STR_LENGTH] = {'\0'};
char textureName[MAX_STR_LENGTH] = {'\0'};
char textureType[MAX_STR_LENGTH] = {'\0'};
double textureSize = 0.f;
int numlines = 0;
int cnt = 0, i = 0;
uint32_t fileSize = 0;
pFileData = fileLoadBuffer;
debug(LOG_TERRAIN, "tileset: %s", tilesetDir);
// For Arizona
if (strcmp(tilesetDir, "texpages/tertilesc1hw") == 0)
{
fallback:
init_tileNames(ARIZONA);
if (!loadFileToBuffer("tileset/tertilesc1hwGtype.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
{
debug(LOG_FATAL, "tileset/tertilesc1hwGtype.txt not found, aborting.");
abort();
}
sscanf(pFileData, "%255[^,'\r\n],%d%n", tilename, &numlines, &cnt);
pFileData += cnt;
if (strcmp(tilename, "tertilesc1hw"))
{
debug(LOG_FATAL, "%s found, but was expecting tertilesc1hw! Aborting.", tilename);
abort();
}
debug(LOG_TERRAIN, "tilename: %s, with %d entries", tilename, numlines);
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
numGroundTypes = numlines;
psGroundTypes = (GROUND_TYPE *)malloc(sizeof(GROUND_TYPE)*numlines);
for (i=0; i < numlines; i++)
{
sscanf(pFileData, "%255[^,'\r\n],%255[^,'\r\n],%lf%n", textureType, textureName, &textureSize, &cnt);
pFileData += cnt;
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
psGroundTypes[getTextureType(textureType)].textureName = strdup(textureName);
psGroundTypes[getTextureType(textureType)].textureSize = textureSize ;
}
SetGroundForTile("tileset/arizonaground.txt", "arizona_ground");
SetDecals("tileset/arizonadecals.txt", "arizona_decals");
}
// for Urban
else if (strcmp(tilesetDir, "texpages/tertilesc2hw") == 0)
{
init_tileNames(URBAN);
if (!loadFileToBuffer("tileset/tertilesc2hwGtype.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
{
debug(LOG_POPUP, "tileset/tertilesc2hwGtype.txt not found, using default terrain ground types.");
goto fallback;
}
sscanf(pFileData, "%255[^,'\r\n],%d%n", tilename, &numlines, &cnt);
pFileData += cnt;
if (strcmp(tilename, "tertilesc2hw"))
{
debug(LOG_POPUP, "%s found, but was expecting tertilesc2hw!", tilename);
goto fallback;
}
debug(LOG_TERRAIN, "tilename: %s, with %d entries", tilename, numlines);
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
numGroundTypes = numlines;
psGroundTypes = (GROUND_TYPE *)malloc(sizeof(GROUND_TYPE)*numlines);
for (i=0; i < numlines; i++)
{
sscanf(pFileData, "%255[^,'\r\n],%255[^,'\r\n],%lf%n", textureType, textureName, &textureSize, &cnt);
pFileData += cnt;
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
psGroundTypes[getTextureType(textureType)].textureName = strdup(textureName);
psGroundTypes[getTextureType(textureType)].textureSize = textureSize;
}
SetGroundForTile("tileset/urbanground.txt", "urban_ground");
SetDecals("tileset/urbandecals.txt", "urban_decals");
}
// for Rockie
else if (strcmp(tilesetDir, "texpages/tertilesc3hw") == 0)
{
init_tileNames(ROCKIE);
if (!loadFileToBuffer("tileset/tertilesc3hwGtype.txt", pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
{
debug(LOG_POPUP, "tileset/tertilesc3hwGtype.txt not found, using default terrain ground types.");
goto fallback;
}
sscanf(pFileData, "%255[^,'\r\n],%d%n", tilename, &numlines, &cnt);
pFileData += cnt;
if (strcmp(tilename, "tertilesc3hw"))
{
debug(LOG_POPUP, "%s found, but was expecting tertilesc3hw!", tilename);
goto fallback;
}
debug(LOG_TERRAIN, "tilename: %s, with %d entries", tilename, numlines);
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
numGroundTypes = numlines;
psGroundTypes = (GROUND_TYPE *)malloc(sizeof(GROUND_TYPE)*numlines);
for (i=0; i < numlines; i++)
{
sscanf(pFileData, "%255[^,'\r\n],%255[^,'\r\n],%lf%n", textureType, textureName, &textureSize, &cnt);
pFileData += cnt;
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
psGroundTypes[getTextureType(textureType)].textureName = strdup(textureName);
psGroundTypes[getTextureType(textureType)].textureSize = textureSize;
}
SetGroundForTile("tileset/rockieground.txt", "rockie_ground");
SetDecals("tileset/rockiedecals.txt", "rockie_decals");
}
// When a map uses something other than the above, we fallback to Arizona
else
{
debug(LOG_ERROR, "unsupported tileset: %s", tilesetDir);
debug(LOG_POPUP, "This is a UNSUPPORTED map with a custom tileset.\nDefaulting to tertilesc1hw -- map may look strange!");
// HACK: / FIXME: For now, we just pretend this is a tertilesc1hw map.
goto fallback;
}
return true;
}
// Parse the file to set up the ground type
static void SetGroundForTile(const char *filename, const char *nametype)
{
char *pFileData = NULL;
char tilename[MAX_STR_LENGTH] = {'\0'};
char val1[MAX_STR_LENGTH], val2[MAX_STR_LENGTH], val3[MAX_STR_LENGTH], val4[MAX_STR_LENGTH];
int numlines = 0;
int cnt = 0, i = 0;
uint32_t fileSize = 0;
pFileData = fileLoadBuffer;
if (!loadFileToBuffer(filename, pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
{
debug(LOG_FATAL, "%s not found, aborting.", filename);
abort();
}
sscanf(pFileData, "%255[^,'\r\n],%d%n", tilename, &numlines, &cnt);
pFileData += cnt;
if (strcmp(tilename, nametype))
{
debug(LOG_FATAL, "%s found, but was expecting %s, aborting.", tilename, nametype);
abort();
}
debug(LOG_TERRAIN, "tilename: %s, with %d entries", tilename, numlines);
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
map = (int *)malloc(sizeof(int) * numlines * 2 * 2 ); // this is a 3D array map[numlines][2][2]
for (i=0; i < numlines; i++)
{
sscanf(pFileData, "%255[^,'\r\n],%255[^,'\r\n],%255[^,'\r\n],%255[^,'\r\n]%n", val1, val2, val3, val4, &cnt);
pFileData += cnt;
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
// inline int iA(int i, int j, int k){ return i*N2*N3 + j*N3 + k; }
// in case it isn't obvious, this is a 3D array, and using pointer math to access each element.
// so map[10][0][1] would be map[10*2*2 + 0 + 1] == map[41]
// map[10][1][0] == map[10*2*2 + 2 + 0] == map[42]
map[i*2*2+0*2+0] = getTextureType(val1);
map[i*2*2+0*2+1] = getTextureType(val2);
map[i*2*2+1*2+0] = getTextureType(val3);
map[i*2*2+1*2+1] = getTextureType(val4);
}
}
// getTextureType() -- just returns the value for that texture type.
static int getTextureType(const char *textureType)
{
int i = 0;
for (i=0; i < numTile_names; i++)
{
if (!strcmp(textureType, &Tile_names[i*MAX_STR_LENGTH]))
{
return i;
}
}
debug(LOG_FATAL, "unknown type [%s] found, aborting!", textureType);
abort();
}
// groundFromMapTile() just a simple lookup table, using pointers to access the 3D map array
// (quasi) pointer math is: map[num elements][2][2]
// so map[10][0][1] would be map[10*2*2 + 0*2 + 1] == map[41]
static int groundFromMapTile(int tile, int j, int k)
{
return map[TileNumber_tile(tile)* 2 * 2 + j * 2 + k];
}
static void rotFlip(int tile, int *i, int *j)
{
int texture = TileNumber_texture(tile);
int rot;
int map[2][2], invmap[4][2];
if (texture & TILE_XFLIP)
{
*i = 1 - *i;
}
if (texture & TILE_YFLIP)
{
*j = 1 - *j;
}
map[0][0] = 0; invmap[0][0] = 0; invmap[0][1] = 0;
map[1][0] = 1; invmap[1][0] = 1; invmap[1][1] = 0;
map[1][1] = 2; invmap[2][0] = 1; invmap[2][1] = 1;
map[0][1] = 3; invmap[3][0] = 0; invmap[3][1] = 1;
rot = map[*i][*j];
rot -= (texture & TILE_ROTMASK) >> TILE_ROTSHIFT;
while(rot < 0) rot += 4;
*i = invmap[rot][0];
*j = invmap[rot][1];
}
/// Tries to figure out what ground type a grid point is from the surrounding tiles
static int determineGroundType(int x, int y, const char *tileset)
{
int ground[2][2];
int votes[2][2];
int weight[2][2];
int i,j, tile;
int a,b, best;
MAPTILE *psTile;
if (x < 0 || y < 0 || x >= mapWidth || y >= mapHeight)
{
return 0; // just return the first ground type
}
// check what tiles surround this grid point
for(i=0;i<2;i++)
{
for(j=0;j<2;j++)
{
if (x+i-1 < 0 || y+j-1 < 0 || x+i-1 >= mapWidth || y+j-1 >= mapHeight)
{
psTile = NULL;
tile = 0;
}
else
{
psTile = mapTile(x+i-1, y+j-1);
tile = psTile->texture;
}
a = i;
b = j;
rotFlip(tile, &a, &b);
ground[i][j] = groundFromMapTile(tile, a, b);
votes[i][j] = 0;
// votes are weighted, some tiles have more weight than others
weight[i][j] = 10;
if (psTile)
{
// cliff tiles have higher priority, to be clearly visible
if (terrainType(psTile) == TER_CLIFFFACE)
weight[i][j] = 100;
// water bottom has lower priority, to stay inside water
if (terrainType(psTile) == TER_WATER)
weight[i][j] = 1;
}
}
}
// now vote, because some maps have seams
for(i=0;i<2;i++)
{
for(j=0;j<2;j++)
{
for(a=0;a<2;a++)
{
for(b=0;b<2;b++)
{
if (ground[i][j] == ground[a][b])
{
votes[i][j] += weight[a][b];
}
}
}
}
}
// and determine the winner
best = -1;
a = 0;
b = 0;
for(i=0;i<2;i++)
{
for(j=0;j<2;j++)
{
if (votes[i][j] > best || (votes[i][j] == best && ground[i][j]<ground[a][b]))
{
best = votes[i][j];
a = i;
b = j;
}
}
}
return ground[a][b];
}
// SetDecals()
// reads in the decal array for the requested tileset.
static void SetDecals(const char *filename, const char *decal_type)
{
char decalname[MAX_STR_LENGTH], *pFileData;
int numlines, cnt, i, tiledecal;
uint32_t fileSize;
pFileData = fileLoadBuffer;
if (!loadFileToBuffer(filename, pFileData, FILE_LOAD_BUFFER_SIZE, &fileSize))
{
debug(LOG_POPUP, "%s not found, aborting.", filename);
abort();
}
sscanf(pFileData, "%255[^,'\r\n],%d%n", decalname, &numlines, &cnt);
pFileData += cnt;
if (strcmp(decalname, decal_type))
{
debug(LOG_POPUP, "%s found, but was expecting %s, aborting.", decalname, decal_type);
abort();
}
debug(LOG_TERRAIN, "reading: %s, with %d entries", filename, numlines);
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
mapDecals = new bool[MAX_TERRAIN_TILES];
std::fill_n(mapDecals, MAX_TERRAIN_TILES, false); // set everything to false.
for (i=0; i < numlines; i++)
{
tiledecal = -1;
sscanf(pFileData, "%d%n", &tiledecal, &cnt);
pFileData += cnt;
//increment the pointer to the start of the next record
pFileData = strchr(pFileData,'\n') + 1;
if ((unsigned)tiledecal > MAX_TERRAIN_TILES)
{
debug(LOG_ERROR, "Tile index is out of range! Was %d, our max is %d", tiledecal, MAX_TERRAIN_TILES);
continue;
}
mapDecals[tiledecal] = true;
}
}
// hasDecals()
// Checks to see if the requested tile has a decal on it or not.
static bool hasDecals(int i, int j)
{
int index = 0;
index = TileNumber_tile(mapTile(i, j)->texture);
if (index > MAX_TERRAIN_TILES)
{
debug(LOG_FATAL, "Tile index is out of range! Was %d, our max is %d", index, MAX_TERRAIN_TILES);
abort();
}
return mapDecals[index];
}
// mapSetGroundTypes()
// Sets the ground type to be a decal or not
static bool mapSetGroundTypes(void)
{
int i,j;
for (i=0;i<mapWidth;i++)
{
for (j=0;j<mapHeight;j++)
{
MAPTILE *psTile = mapTile(i, j);
psTile->ground = determineGroundType(i, j, tilesetDir);
if (hasDecals(i,j))
{
SET_TILE_DECAL(psTile);
}
else
{
CLEAR_TILE_DECAL(psTile);
}
}
}
return true;
}
static bool isWaterVertex(int x, int y)
{
if (x < 1 || y < 1 || x > mapWidth - 1 || y > mapHeight - 1)
{
return false;
}
return terrainType(mapTile(x, y)) == TER_WATER && terrainType(mapTile(x-1, y)) == TER_WATER
&& terrainType(mapTile(x, y-1)) == TER_WATER && terrainType(mapTile(x-1, y-1)) == TER_WATER;
}
static void generateRiverbed(void)
{
MersenneTwister mt(12345); // 12345 = random seed.
int maxIdx = 1, idx[MAP_MAXWIDTH][MAP_MAXHEIGHT];
int i, j, l = 0;
for (i = 0; i < mapWidth; i++)
{
for (j = 0; j < mapHeight; j++)
{
// initially set the seabed index to 0 for ground and 100 for water
idx[i][j] = 100 * isWaterVertex(i, j);
if (idx[i][j] > 0)
{
l++;
}
}
}
debug(LOG_TERRAIN, "Generating riverbed for %d water tiles.", l);
if (l == 0) // no water on map
{
return;
}
l = 0;
do
{
maxIdx = 1;
for (i = 1; i < mapWidth - 2; i++)
{
for (j = 1; j < mapHeight - 2; j++)
{
if (idx[i][j] > 0)
{
idx[i][j] = (idx[i-1][j] + idx[i][j-1] + idx[i][j+1] + idx[i+1][j]) / 4;
if (idx[i][j] > maxIdx)
{
maxIdx=idx[i][j];
}
}
}
}
++l;
debug(LOG_TERRAIN, "%d%% completed after %d iterations", 10 * (100 - maxIdx), l);
} while (maxIdx > 90 && l < 20);
for (i = 0; i < mapWidth; i++)
{
for (j = 0; j < mapHeight; j++)
{
if (idx[i][j] > maxIdx)
{
idx[i][j] = maxIdx;
}
if (idx[i][j] < 1)
{
idx[i][j] = 1;
}
if (isWaterVertex(i, j))
{
l = (WATER_MAX_DEPTH + 1 - WATER_MIN_DEPTH) * (maxIdx - idx[i][j] - mt.u32() % (maxIdx / 6 + 1));
mapTile(i, j)->height -= WATER_MIN_DEPTH - (l / maxIdx);
}
}
}
}
/* Initialise the map structure */
bool mapLoad(char *filename, bool preview)
{
UDWORD numGw, width, height;
char aFileType[4];
UDWORD version;
UDWORD i, x, y;
PHYSFS_file *fp = PHYSFS_openRead(filename);
MersenneTwister mt(12345); // 12345 = random seed.
if (!fp)
{
debug(LOG_ERROR, "%s not found", filename);
return false;
}
else if (PHYSFS_read(fp, aFileType, 4, 1) != 1
|| !PHYSFS_readULE32(fp, &version)
|| !PHYSFS_readULE32(fp, &width)
|| !PHYSFS_readULE32(fp, &height)
|| aFileType[0] != 'm'
|| aFileType[1] != 'a'
|| aFileType[2] != 'p')
{
debug(LOG_ERROR, "Bad header in %s", filename);
goto failure;
}
else if (version <= VERSION_9)
{
debug(LOG_ERROR, "%s: Unsupported save format version %u", filename, version);
goto failure;
}
else if (version > CURRENT_VERSION_NUM)
{
debug(LOG_ERROR, "%s: Undefined save format version %u", filename, version);
goto failure;
}
else if (width * height > MAP_MAXAREA)
{
debug(LOG_ERROR, "Map %s too large : %d %d", filename, width, height);
goto failure;
}
if (width <=1 || height <=1)
{
debug(LOG_ERROR, "Map is too small : %u, %u", width, height);
goto failure;
}
/* See if this is the first time a map has been loaded */
ASSERT(psMapTiles == NULL, "Map has not been cleared before calling mapLoad()!");
/* Allocate the memory for the map */
psMapTiles = (MAPTILE *)calloc(width * height, sizeof(MAPTILE));
ASSERT(psMapTiles != NULL, "Out of memory" );
mapWidth = width;
mapHeight = height;
// FIXME: the map preview code loads the map without setting the tileset
if (!tilesetDir)
{
tilesetDir = strdup("texpages/tertilesc1hw");
}
// load the ground types
if (!mapLoadGroundTypes())
{
goto failure;
}
//load in the map data itself
/* Load in the map data */
for (i = 0; i < mapWidth * mapHeight; i++)
{
UWORD texture;
UBYTE height;
if (!PHYSFS_readULE16(fp, &texture) || !PHYSFS_readULE8(fp, &height))
{
debug(LOG_ERROR, "%s: Error during savegame load", filename);
goto failure;
}
psMapTiles[i].texture = texture;
psMapTiles[i].height = height*ELEVATION_SCALE;
// Visibility stuff
memset(psMapTiles[i].watchers, 0, sizeof(psMapTiles[i].watchers));
memset(psMapTiles[i].sensors, 0, sizeof(psMapTiles[i].sensors));
memset(psMapTiles[i].jammers, 0, sizeof(psMapTiles[i].jammers));
psMapTiles[i].sensorBits = 0;
psMapTiles[i].jammerBits = 0;
psMapTiles[i].tileExploredBits = 0;
}
if (preview)
{
// no need to do anything else for the map preview
goto ok;
}
if (!PHYSFS_readULE32(fp, &version) || !PHYSFS_readULE32(fp, &numGw) || version != 1)
{
debug(LOG_ERROR, "Bad gateway in %s", filename);
goto failure;
}
for (i = 0; i < numGw; i++)
{
UBYTE x0, y0, x1, y1;
if (!PHYSFS_readULE8(fp, &x0) || !PHYSFS_readULE8(fp, &y0) || !PHYSFS_readULE8(fp, &x1) || !PHYSFS_readULE8(fp, &y1))
{
debug(LOG_ERROR, "%s: Failed to read gateway info", filename);
goto failure;
}
if (!gwNewGateway(x0, y0, x1, y1))
{
debug(LOG_ERROR, "%s: Unable to add gateway %d - dropping it", filename, i);
}
}
if (!mapSetGroundTypes())
{
goto failure;
}
for (y = 0; y < mapHeight; y++)
{
for (x = 0; x < mapWidth; x++)
{
// FIXME: magic number
mapTile(x, y)->waterLevel = mapTile(x, y)->height - world_coord(1) / 3;
}
}
generateRiverbed();
/* set up the scroll mins and maxs - set values to valid ones for any new map */
scrollMinX = scrollMinY = 0;
scrollMaxX = mapWidth;
scrollMaxY = mapHeight;
/* Allocate aux maps */
psBlockMap[AUX_MAP] = (uint8_t *)malloc(mapWidth * mapHeight * sizeof(*psBlockMap[0]));
psBlockMap[AUX_ASTARMAP] = (uint8_t *)malloc(mapWidth * mapHeight * sizeof(*psBlockMap[0]));
psBlockMap[AUX_DANGERMAP] = (uint8_t *)malloc(mapWidth * mapHeight * sizeof(*psBlockMap[0]));
for (x = 0; x < MAX_PLAYERS + AUX_MAX; x++)
{
psAuxMap[x] = (uint8_t *)malloc(mapWidth * mapHeight * sizeof(*psAuxMap[0]));
}
// Set our blocking bits
for (y = 0; y < mapHeight; y++)
{
for (x = 0; x < mapWidth; x++)
{
MAPTILE *psTile = mapTile(x, y);
auxClearBlocking(x, y, AUXBITS_ALL);
auxClearAll(x, y, AUXBITS_ALL);
/* All tiles outside of the map and on map border are blocking. */
if (x < 1 || y < 1 || x > mapWidth - 1 || y > mapHeight - 1)
{
auxSetBlocking(x, y, AUXBITS_ALL); // block everything
}
if (terrainType(psTile) == TER_WATER)
{
auxSetBlocking(x, y, WATER_BLOCKED);
}
else
{
auxSetBlocking(x, y, LAND_BLOCKED);
}
if (terrainType(psTile) == TER_CLIFFFACE)
{
auxSetBlocking(x, y, FEATURE_BLOCKED);
}
}
}
/* Set continents. This should ideally be done in advance by the map editor. */
mapFloodFillContinents();
ok:
PHYSFS_close(fp);
return true;
failure:
PHYSFS_close(fp);
return false;
}
/* Save the map data */
bool mapSave(char **ppFileData, UDWORD *pFileSize)
{
MAP_SAVEHEADER *psHeader = NULL;
MAP_SAVETILE *psTileData = NULL;
MAPTILE *psTile = NULL;
GATEWAY *psCurrGate = NULL;
GATEWAY_SAVEHEADER *psGateHeader = NULL;
GATEWAY_SAVE *psGate = NULL;
SDWORD numGateways = 0;
// find the number of non water gateways
for(psCurrGate = gwGetGateways(); psCurrGate; psCurrGate = psCurrGate->psNext)
{
numGateways += 1;
}
/* Allocate the data buffer */
*pFileSize = SAVE_HEADER_SIZE + mapWidth*mapHeight * SAVE_TILE_SIZE;
// Add on the size of the gateway data.
*pFileSize += sizeof(GATEWAY_SAVEHEADER) + sizeof(GATEWAY_SAVE)*numGateways;
*ppFileData = (char*)malloc(*pFileSize);
if (*ppFileData == NULL)
{
debug( LOG_FATAL, "Out of memory" );
abort();
return false;
}
/* Put the file header on the file */
psHeader = (MAP_SAVEHEADER *)*ppFileData;
psHeader->aFileType[0] = 'm';
psHeader->aFileType[1] = 'a';
psHeader->aFileType[2] = 'p';
psHeader->aFileType[3] = ' ';
psHeader->version = CURRENT_VERSION_NUM;
psHeader->width = mapWidth;
psHeader->height = mapHeight;
/* MAP_SAVEHEADER */
endian_udword(&psHeader->version);
endian_udword(&psHeader->width);
endian_udword(&psHeader->height);
/* Put the map data into the buffer */
psTileData = (MAP_SAVETILE *)(*ppFileData + SAVE_HEADER_SIZE);
psTile = psMapTiles;
for (int i = 0; i < mapWidth*mapHeight; i++)
{
psTileData->texture = psTile->texture;
if (terrainType(psTile) == TER_WATER)
{
psTileData->height = (psTile->waterLevel + world_coord(1) / 3) / ELEVATION_SCALE;
}
else
{
psTileData->height = psTile->height / ELEVATION_SCALE;
}
/* MAP_SAVETILE */
endian_uword(&psTileData->texture);
psTileData = (MAP_SAVETILE *)((UBYTE *)psTileData + SAVE_TILE_SIZE);
psTile++;
}
// Put the gateway header.
psGateHeader = (GATEWAY_SAVEHEADER*)psTileData;
psGateHeader->version = 1;
psGateHeader->numGateways = numGateways;
/* GATEWAY_SAVEHEADER */
endian_udword(&psGateHeader->version);
endian_udword(&psGateHeader->numGateways);
psGate = (GATEWAY_SAVE*)(psGateHeader+1);
// Put the gateway data.
for(psCurrGate = gwGetGateways(); psCurrGate; psCurrGate = psCurrGate->psNext)
{
psGate->x0 = psCurrGate->x1;
psGate->y0 = psCurrGate->y1;
psGate->x1 = psCurrGate->x2;
psGate->y1 = psCurrGate->y2;
ASSERT(psGate->x0 == psGate->x1 || psGate->y0 == psGate->y1, "Invalid gateway coordinates (%d, %d, %d, %d)",
psGate->x0, psGate->y0, psGate->x1, psGate->y1);
psGate++;
}
return true;
}
/* Shutdown the map module */
bool mapShutdown(void)
{
int x;
if (dangerThread)
{
wzSemaphoreWait(dangerDoneSemaphore);
lastDangerPlayer = -1;
wzSemaphorePost(dangerSemaphore);
wzThreadJoin(dangerThread);
wzSemaphoreDestroy(dangerSemaphore);
wzSemaphoreDestroy(dangerDoneSemaphore);
dangerThread = NULL;
dangerSemaphore = NULL;
dangerDoneSemaphore = NULL;
}
free(psMapTiles);
delete[] mapDecals;
free(psGroundTypes);
free(map);
free(Tile_names);
free(psBlockMap[AUX_MAP]);
psBlockMap[AUX_MAP] = NULL;
free(psBlockMap[AUX_ASTARMAP]);
psBlockMap[AUX_ASTARMAP] = NULL;
free(psBlockMap[AUX_DANGERMAP]);
free(floodbucket);
psBlockMap[AUX_DANGERMAP] = NULL;
for (x = 0; x < MAX_PLAYERS + AUX_MAX; x++)
{
free(psAuxMap[x]);
psAuxMap[x] = NULL;
}
map = NULL;
floodbucket = NULL;
psGroundTypes = NULL;
mapDecals = NULL;
psMapTiles = NULL;
mapWidth = mapHeight = 0;
numTile_names = 0;
Tile_names = NULL;
return true;
}
/**
* Intersect a tile with a line and report the points of intersection
* line is gives as point plus 2d directional vector
* returned are two coordinates at the edge
* true if the intersection also crosses the tile split line
* (which has to be taken into account)
**/
bool map_Intersect(int* Cx, int* Cy, int* Vx, int* Vy, int* Sx, int* Sy)
{
int x, y, ox, oy, Dx, Dy, tileX, tileY;
int ily,iry,itx,ibx;
// dereference pointers
x = *Cx;
y = *Cy;
Dx = *Vx;
Dy = *Vy;
/* Turn into tile coordinates */
tileX = map_coord(x);
tileY = map_coord(y);
/* Inter tile comp */
ox = map_round(x);
oy = map_round(y);
/* allow backwards tracing */
if (ox==0 && Dx<0) {
tileX--;
ox=TILE_UNITS;
}
if (oy==0 && Dy<0) {
tileY--;
oy=TILE_UNITS;
}
*Cx=-4*TILE_UNITS; // to trigger assertion
*Cy=-4*TILE_UNITS;
*Vx=-4*TILE_UNITS;
*Vy=-4*TILE_UNITS;
// calculate intersection point on the left and right (if any)
ily = y - 4 * TILE_UNITS; // make sure initial value is way outside of tile
iry = y - 4 * TILE_UNITS;
if ( Dx!=0) {
ily = y - ox*Dy/Dx;
iry = y + (TILE_UNITS-ox)*Dy/Dx;
}
// calculate intersection point on top and bottom (if any)
itx = x - 4 * TILE_UNITS; // make sure initial value is way outside of tile
ibx = x - 4 * TILE_UNITS;
if (Dy!=0) {
itx = x - oy*Dx/Dy;
ibx = x + (TILE_UNITS-oy)*Dx/Dy;
}
// line comes from the left?
if (Dx>=0) {
if (map_coord(ily)==tileY || map_coord(ily-1)==tileY) {
*Cx = world_coord(tileX);
*Cy = ily;
}
if (map_coord(iry)==tileY || map_coord(iry-1)==tileY) {
*Vx = world_coord(tileX+1);
*Vy = iry;
}
} else {
if (map_coord(ily)==tileY || map_coord(ily-1)==tileY) {
*Vx = world_coord(tileX);
*Vy = ily;
}
if (map_coord(iry)==tileY || map_coord(iry-1)==tileY) {
*Cx = world_coord(tileX+1);
*Cy = iry;
}
}
// line comes from the top?
if (Dy>=0) {
if (map_coord(itx)==tileX || map_coord(itx-1)==tileX) {
*Cx = itx;
*Cy = world_coord(tileY);
}
if (map_coord(ibx)==tileX || map_coord(ibx-1)==tileX) {
*Vx = ibx;
*Vy = world_coord(tileY+1);
}
} else {
if (map_coord(itx)==tileX || map_coord(itx-1)==tileX) {
*Vx = itx;
*Vy = world_coord(tileY);
}
if (map_coord(ibx)==tileX || map_coord(ibx-1)==tileX) {
*Cx = ibx;
*Cy = world_coord(tileY+1);
}
}
// assertions, no intersections outside of tile
ASSERT(*Cx>=world_coord(tileX) && *Cx<=world_coord(tileX+1),"map_Intersect(): tile Bounds %i %i, %i %i -> %i,%i,%i,%i",x,y,Dx,Dy,*Cx,*Cy,*Vx,*Vy);
ASSERT(*Cy>=world_coord(tileY) && *Cy<=world_coord(tileY+1),"map_Intersect(): tile Bounds %i %i, %i %i -> %i,%i,%i,%i",x,y,Dx,Dy,*Cx,*Cy,*Vx,*Vy);
ASSERT(*Vx>=world_coord(tileX) && *Vx<=world_coord(tileX+1),"map_Intersect(): tile Bounds %i %i, %i %i -> %i,%i,%i,%i",x,y,Dx,Dy,*Cx,*Cy,*Vx,*Vy);
ASSERT(*Vy>=world_coord(tileY) && *Vy<=world_coord(tileY+1),"map_Intersect(): tile Bounds %i %i, %i %i -> %i,%i,%i,%i",x,y,Dx,Dy,*Cx,*Cy,*Vx,*Vy);
ASSERT(tileX>=0 && tileY>=0 && tileX<mapWidth && tileY<mapHeight,"map_Intersect(): map Bounds %i %i, %i %i -> %i,%i,%i,%i",x,y,Dx,Dy,*Cx,*Cy,*Vx,*Vy);
//calculate midway line intersection points
if (((map_coord(itx)==tileX ) == (map_coord(ily)==tileY)) && ((map_coord(ibx)==tileX ) == (map_coord(iry)==tileY))) {
// line crosses diagnonal only
if (Dx-Dy==0) return false;
*Sx = world_coord(tileX) + (Dx*oy - Dy*ox)/(Dx - Dy);
*Sy = world_coord(tileY) + (Dx*oy - Dy*ox)/(Dx - Dy);
if (map_coord(*Sx)!=tileX || map_coord(*Sy)!=tileY) return false;
return true;
} else if (((map_coord(ibx)==tileX) == (map_coord(ily)==tileY)) && ((map_coord(itx)==tileX) == (map_coord(iry)==tileY))) {
//line crosses anti-diagonal only
if (Dx+Dy==0) return false;
*Sx = world_coord(tileX) + (Dx*(TILE_UNITS - oy) + Dy*ox)/(Dx + Dy);
*Sy = world_coord(tileY) + (Dy*(TILE_UNITS - ox) + Dx*oy)/(Dx + Dy);
if (map_coord(*Sx)!=tileX || map_coord(*Sy)!=tileY) return false;
return true;
} else {
//line crosses both tile diagonals
//TODO: trunk divides tiles into 4 parts instead of 2 in 2.3.
//We would need to check and return both intersections here now,
//but that would require an additional return parameter!
//Instead we check only one of them and know it might be wrong!
if (Dx+Dy!=0) {
// check anti-diagonal
*Sx = world_coord(tileX) + (Dx*(TILE_UNITS - oy) + Dy*ox)/(Dx + Dy);
*Sy = world_coord(tileY) + (Dy*(TILE_UNITS - ox) + Dx*oy)/(Dx + Dy);
if (map_coord(*Sx)==tileX && map_coord(*Sy)==tileY) return true;
}
if (Dx-Dy!=0) {
// check diagonal
*Sx = world_coord(tileX) + (Dx*oy - Dy*ox)/(Dx - Dy);
*Sy = world_coord(tileY) + (Dx*oy - Dy*ox)/(Dx - Dy);
if (map_coord(*Sx)==tileX && map_coord(*Sy)==tileY) return true;
}
}
return false;
}
// Rotate vector clockwise by quadrant*90° around (TILE_UNITS/2, TILE_UNITS/2). (Considering x to be to the right, and y down.)
static Vector3i rotateWorldQuadrant(Vector3i v, int quadrant)
{
switch (quadrant & 3)
{
default: // Can't get here.
case 0: return v; break; // 0°.
case 1: return Vector3i(TILE_UNITS - v.y, v.x, v.z); break; // 90° clockwise.
case 2: return Vector3i(TILE_UNITS - v.x, TILE_UNITS - v.y, v.z); break; // 180°.
case 3: return Vector3i( v.y, TILE_UNITS - v.x, v.z); break; // 90° anticlockwise.
}
}
// Returns (0, 0) rotated clockwise quadrant*90° around (½, ½). (Considering x to be to the right, and y down.)
static Vector2i quadrantCorner(int quadrant)
{
int dx[4] = {0, 1, 1, 0};
int dy[4] = {0, 0, 1, 1};
return Vector2i(dx[quadrant & 3], dy[quadrant & 3]);
}
// Returns (0, -1) rotated clockwise quadrant*90° around (0, 0). (Considering x to be to the right, and y down.)
static Vector2i quadrantDelta(int quadrant)
{
int dx[4] = {0, 1, 0, -1};
int dy[4] = {-1, 0, 1, 0};
return Vector2i(dx[quadrant & 3], dy[quadrant & 3]);
}
static inline bool fracTest(int numerA, int denomA, int numerB, int denomB)
{
return denomA > 0 && numerA >= 0 && (denomB <= 0 || numerB < 0 || (int64_t)numerA*denomB < (int64_t)numerB*denomA);
}
unsigned map_LineIntersect(Vector3i src, Vector3i dst, unsigned tMax)
{
// Transform src and dst to a coordinate system such that the tile quadrant containing src has
// corners at (0, 0), (TILE_UNITS, 0), (TILE_UNITS/2, TILE_UNITS/2).
Vector2i tile = map_coord(removeZ(src));
src -= Vector3i(world_coord(tile), 0);
dst -= Vector3i(world_coord(tile), 0);
// +0+
// quadrant = 3×1
// +2+
int quadrant = ((src.x < src.y)*3) ^ (TILE_UNITS - src.x < src.y);
src = rotateWorldQuadrant(src, -quadrant);
dst = rotateWorldQuadrant(dst, -quadrant);
while (true)
{
int height[4];
for (int q = 0; q < 4; ++q)
{
Vector2i corner = tile + quadrantCorner(quadrant + q);
height[q] = map_TileHeightSurface(corner.x, corner.y);
}
Vector3i dif = dst - src;
// We are considering the volume of a quadrant (the volume above a quarter of a map tile, which is
// a degenerate tetrahedron with a point at infinity). We have a line segment, and want to know where
// it exits the quadrant volume.
// There are 5 possible cases. Cases 0-2, our line can exit one of the three sides of the quadrant
// volume (and pass into a neighbouring quadrant volume), or case 3, can exit through the bottom of the
// quadrant volume (which means intersecting the terrain), or case 4, the line segment can end (which
// means reaching the destination with no intersection.
// Note that the height of the centre of the tile is the average of the corners, such that a tile
// consists of 4 flat triangles (which are not (in general) parallel to each other).
// +--0--+
// \ 3 /
// 2 1
// +
// Denominators are positive iff we are going in the direction of the line. First line crossed has the smallest fraction.
// numer/denom gives the intersection times for the 5 cases.
int numer[5], denom[5];
numer[0] = -(-src.y);
denom[0] = -dif.y;
numer[1] = TILE_UNITS - (src.x + src.y);
denom[1] = dif.x + dif.y;
numer[2] = -(-src.x + src.y);
denom[2] = -dif.x + dif.y;
Vector3i normal(2*(height[1] - height[0]), height[2] + height[3] - height[0] - height[1], -2*TILE_UNITS); // Normal pointing down, and not normalised.
numer[3] = height[0]*normal.z - src*normal;
denom[3] = dif*normal;
numer[4] = 1;
denom[4] = 1;
int firstIntersection = 0;
for (int test = 0; test < 5; ++test)
{
if (!fracTest(numer[firstIntersection], denom[firstIntersection], numer[test], denom[test]))
{
firstIntersection = test;
}
}
switch (firstIntersection)
{
case 0: // Cross top line first (the tile boundary).
tile += quadrantDelta(quadrant);
quadrant += 2;
src = rotateWorldQuadrant(src, -2) + Vector3i(0, -TILE_UNITS, 0);
dst = rotateWorldQuadrant(dst, -2) + Vector3i(0, -TILE_UNITS, 0);
if (tile.x < 0 || tile.x >= mapWidth || tile.y < 0 || tile.y >= mapHeight)
{
// Intersect edge of map.
return (int64_t)tMax * numer[firstIntersection]/denom[firstIntersection];
}
break;
case 1: // Cross bottom-right line first.
// Change to the new quadrant, and transform appropriately.
++quadrant;
src = rotateWorldQuadrant(src, -1);
dst = rotateWorldQuadrant(dst, -1);
break;
case 2: // Cross bottom-left line first.
// Change to the new quadrant, and transform appropriately.
--quadrant;
src = rotateWorldQuadrant(src, 1);
dst = rotateWorldQuadrant(dst, 1);
break;
case 3: // Intersect terrain!
return (int64_t)tMax * numer[firstIntersection]/denom[firstIntersection];
case 4: // Line segment ends.
return UINT32_MAX;
}
}
}
/// The max height of the terrain and water at the specified world coordinates
extern int32_t map_Height(int x, int y)
{
int tileX, tileY;
int i, j;
int32_t height[2][2], center;
int32_t onTileX, onTileY;
int32_t left, right, middle;
int32_t onBottom, result;
int towardsCenter, towardsRight;
// Clamp x and y values to actual ones
// Give one tile worth of leeway before asserting, for units/transporters coming in from off-map.
ASSERT(x >= -TILE_UNITS, "map_Height: x value is too small (%d,%d) in %dx%d",map_coord(x),map_coord(y),mapWidth,mapHeight);
ASSERT(y >= -TILE_UNITS, "map_Height: y value is too small (%d,%d) in %dx%d",map_coord(x),map_coord(y),mapWidth,mapHeight);
x = MAX(x, 0);
y = MAX(y, 0);
ASSERT(x < world_coord(mapWidth)+TILE_UNITS, "map_Height: x value is too big (%d,%d) in %dx%d",map_coord(x),map_coord(y),mapWidth,mapHeight);
ASSERT(y < world_coord(mapHeight)+TILE_UNITS, "map_Height: y value is too big (%d,%d) in %dx%d",map_coord(x),map_coord(y),mapWidth,mapHeight);
x = MIN(x, world_coord(mapWidth) - 1);
y = MIN(y, world_coord(mapHeight) - 1);
// on which tile are these coords?
tileX = map_coord(x);
tileY = map_coord(y);
// where on the tile? (scale to (0,1))
onTileX = x - world_coord(tileX);
onTileY = y - world_coord(tileY);
// get the height for the corners and center
center = 0;
for (i = 0; i < 2; i++)
{
for (j = 0; j < 2; j++)
{
height[i][j] = map_TileHeightSurface(tileX+i, tileY+j);
center += height[i][j];
}
}
center /= 4;
// we have:
// x ->
// y 0,0--D--1,0
// | | \ / |
// V A centre C
// | / \ |
// 0,1--B--1,1
// get heights for left and right corners and the distances
if (onTileY > onTileX)
{
if (onTileY < TILE_UNITS - onTileX)
{
// A
right = height[0][0];
left = height[0][1];
towardsCenter = onTileX;
towardsRight = TILE_UNITS - onTileY;
}
else
{
// B
right = height[0][1];
left = height[1][1];
towardsCenter = TILE_UNITS - onTileY;
towardsRight = TILE_UNITS - onTileX;
}
}
else
{
if (onTileX > TILE_UNITS - onTileY)
{
// C
right = height[1][1];
left = height[1][0];
towardsCenter = TILE_UNITS - onTileX;
towardsRight = onTileY;
}
else
{
// D
right = height[1][0];
left = height[0][0];
towardsCenter = onTileY;
towardsRight = onTileX;
}
}
ASSERT(towardsCenter <= TILE_UNITS/2, "towardsCenter is too high");
// now we have:
// left m right
// center
middle = (left + right)/2;
onBottom = left * (TILE_UNITS - towardsRight) + right * towardsRight;
result = onBottom + (center - middle) * towardsCenter * 2;
return (result + TILE_UNITS/2) / TILE_UNITS;
}
/* returns true if object is above ground */
bool mapObjIsAboveGround(const SIMPLE_OBJECT *psObj)
{
// min is used to make sure we don't go over array bounds!
// TODO Using the corner of the map instead doesn't make sense. Fix this...
const int mapsize = mapWidth * mapHeight - 1;
const int tileX = map_coord(psObj->pos.x);
const int tileY = map_coord(psObj->pos.y);
const int tileYOffset1 = (tileY * mapWidth);
const int tileYOffset2 = ((tileY+1) * mapWidth);
const int h1 = psMapTiles[MIN(mapsize, tileYOffset1 + tileX) ].height;
const int h2 = psMapTiles[MIN(mapsize, tileYOffset1 + tileX + 1)].height;
const int h3 = psMapTiles[MIN(mapsize, tileYOffset2 + tileX) ].height;
const int h4 = psMapTiles[MIN(mapsize, tileYOffset2 + tileX + 1)].height;
/* trivial test above */
if (psObj->pos.z > h1 && psObj->pos.z > h2 && psObj->pos.z > h3 && psObj->pos.z > h4)
{
return true;
}
/* trivial test below */
if (psObj->pos.z <= h1 && psObj->pos.z <= h2 && psObj->pos.z <= h3 && psObj->pos.z <= h4)
{
return false;
}
/* exhaustive test */
return psObj->pos.z > map_Height(psObj->pos.x, psObj->pos.y);
}
/* returns the max and min height of a tile by looking at the four corners
in tile coords */
void getTileMaxMin(int x, int y, int *pMax, int *pMin)
{
*pMin = INT32_MAX;
*pMax = INT32_MIN;
for (int j = 0; j < 2; ++j)
for (int i = 0; i < 2; ++i)
{
int height = map_TileHeight(x+i, y+j);
*pMin = std::min(*pMin, height);
*pMax = std::max(*pMax, height);
}
}
// -----------------------------------------------------------------------------------
/* This will save out the visibility data */
bool writeVisibilityData(const char* fileName)
{
unsigned int i;
VIS_SAVEHEADER fileHeader;
PHYSFS_file* fileHandle = openSaveFile(fileName);
if (!fileHandle)
{
return false;
}
fileHeader.aFileType[0] = 'v';
fileHeader.aFileType[1] = 'i';
fileHeader.aFileType[2] = 's';
fileHeader.aFileType[3] = 'd';
fileHeader.version = CURRENT_VERSION_NUM;
// Write out the current file header
if (PHYSFS_write(fileHandle, fileHeader.aFileType, sizeof(fileHeader.aFileType), 1) != 1
|| !PHYSFS_writeUBE32(fileHandle, fileHeader.version))
{
debug(LOG_ERROR, "writeVisibilityData: could not write header to %s; PHYSFS error: %s", fileName, PHYSFS_getLastError());
PHYSFS_close(fileHandle);
return false;
}
int planes = (game.maxPlayers + 7)/8;
for (unsigned plane = 0; plane < planes; ++plane)
{
for (i = 0; i < mapWidth * mapHeight; ++i)
{
if (!PHYSFS_writeUBE8(fileHandle, psMapTiles[i].tileExploredBits >> (plane*8)))
{
debug(LOG_ERROR, "writeVisibilityData: could not write to %s; PHYSFS error: %s", fileName, PHYSFS_getLastError());
PHYSFS_close(fileHandle);
return false;
}
}
}
// Everything is just fine!
PHYSFS_close(fileHandle);
return true;
}
// -----------------------------------------------------------------------------------
/* This will read in the visibility data */
bool readVisibilityData(const char* fileName)
{
VIS_SAVEHEADER fileHeader;
unsigned int expectedFileSize, fileSize;
unsigned int i;
PHYSFS_file* fileHandle = openLoadFile(fileName, false);
if (!fileHandle)
{
// Failure to open the file is no failure to read it
return true;
}
// Read the header from the file
if (PHYSFS_read(fileHandle, fileHeader.aFileType, sizeof(fileHeader.aFileType), 1) != 1
|| !PHYSFS_readUBE32(fileHandle, &fileHeader.version))
{
debug(LOG_ERROR, "readVisibilityData: error while reading header from file: %s", PHYSFS_getLastError());
PHYSFS_close(fileHandle);
return false;
}
// Check the header to see if we've been given a file of the right type
if (fileHeader.aFileType[0] != 'v'
|| fileHeader.aFileType[1] != 'i'
|| fileHeader.aFileType[2] != 's'
|| fileHeader.aFileType[3] != 'd')
{
debug(LOG_ERROR, "readVisibilityData: Weird file type found? Has header letters - '%c' '%c' '%c' '%c' (should be 'v' 'i' 's' 'd')",
fileHeader.aFileType[0],
fileHeader.aFileType[1],
fileHeader.aFileType[2],
fileHeader.aFileType[3]);
PHYSFS_close(fileHandle);
return false;
}
int planes = (game.maxPlayers + 7)/8;
// Validate the filesize
expectedFileSize = sizeof(fileHeader.aFileType) + sizeof(fileHeader.version) + mapWidth * mapHeight * planes;
fileSize = PHYSFS_fileLength(fileHandle);
if (fileSize != expectedFileSize)
{
PHYSFS_close(fileHandle);
ASSERT(!"readVisibilityData: unexpected filesize", "readVisibilityData: unexpected filesize; should be %u, but is %u", expectedFileSize, fileSize);
return false;
}
// For every tile...
for(i=0; i<mapWidth*mapHeight; i++)
{
psMapTiles[i].tileExploredBits = 0;
}
for (unsigned plane = 0; plane < planes; ++plane)
{
for(i=0; i<mapWidth*mapHeight; i++)
{
/* Get the visibility data */
uint8_t val = 0;
if (!PHYSFS_readUBE8(fileHandle, &val))
{
debug(LOG_ERROR, "readVisibilityData: could not read from %s; PHYSFS error: %s", fileName, PHYSFS_getLastError());
PHYSFS_close(fileHandle);
return false;
}
psMapTiles[i].tileExploredBits |= val << (plane*8);
}
}
// Close the file
PHYSFS_close(fileHandle);
/* Hopefully everything's just fine by now */
return true;
}
// Convert a direction into an offset.
// dir 0 => x = 0, y = -1
#define NUM_DIR 8
static const Vector2i aDirOffset[] =
{
Vector2i( 0, 1),
Vector2i(-1, 1),
Vector2i(-1, 0),
Vector2i(-1,-1),
Vector2i( 0,-1),
Vector2i( 1,-1),
Vector2i( 1, 0),
Vector2i( 1, 1),
};
// Flood fill a "continent".
// TODO take into account scroll limits and update continents on scroll limit changes
static void mapFloodFill(int x, int y, int continent, uint8_t blockedBits, uint16_t MAPTILE::*varContinent)
{
std::vector<Vector2i> open;
open.push_back(Vector2i(x, y));
mapTile(x, y)->*varContinent = continent; // Set continent value
while (!open.empty())
{
// Pop the first open node off the list for this iteration
Vector2i pos = open.back();
open.pop_back();
// Add accessible neighbouring tiles to the open list
for (int i = 0; i < NUM_DIR; ++i)
{
// rely on the fact that all border tiles are inaccessible to avoid checking explicitly
Vector2i npos = pos + aDirOffset[i];
if (npos.x < 1 || npos.y < 1 || npos.x > mapWidth - 2 || npos.y > mapHeight - 2)
{
continue;
}
MAPTILE *psTile = mapTile(npos);
if (!(blockTile(npos.x, npos.y, AUX_MAP) & blockedBits) && psTile->*varContinent == 0)
{
open.push_back(npos); // add to open list
psTile->*varContinent = continent; // Set continent value
}
}
}
}
void mapFloodFillContinents()
{
int x, y, limitedContinents = 0, hoverContinents = 0;
/* Clear continents */
for (y = 0; y < mapHeight; y++)
{
for (x = 0; x < mapWidth; x++)
{
MAPTILE *psTile = mapTile(x, y);
psTile->limitedContinent = 0;
psTile->hoverContinent = 0;
}
}
/* Iterate over the whole map, looking for unset continents */
for (y = 1; y < mapHeight - 2; y++)
{
for (x = 1; x < mapWidth - 2; x++)
{
MAPTILE *psTile = mapTile(x, y);
if (psTile->limitedContinent == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_WHEELED))
{
mapFloodFill(x, y, 1 + limitedContinents++, WATER_BLOCKED | FEATURE_BLOCKED, &MAPTILE::limitedContinent);
}
else if (psTile->limitedContinent == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_PROPELLOR))
{
mapFloodFill(x, y, 1 + limitedContinents++, LAND_BLOCKED | FEATURE_BLOCKED, &MAPTILE::limitedContinent);
}
if (psTile->hoverContinent == 0 && !fpathBlockingTile(x, y, PROPULSION_TYPE_HOVER))
{
mapFloodFill(x, y, 1 + hoverContinents++, FEATURE_BLOCKED, &MAPTILE::hoverContinent);
}
}
}
debug(LOG_MAP, "Found %d limited and %d hover continents", limitedContinents, hoverContinents);
}
void tileSetFire(int32_t x, int32_t y, uint32_t duration)
{
const int posX = map_coord(x);
const int posY = map_coord(y);
MAPTILE *const tile = mapTile(posX, posY);
uint16_t currentTime = gameTime / GAME_TICKS_PER_UPDATE;
uint16_t fireEndTime = (gameTime + duration) / GAME_TICKS_PER_UPDATE;
if (currentTime == fireEndTime)
{
return; // Fire already ended.
}
if ((tile->tileInfoBits & BITS_ON_FIRE) != 0 && (uint16_t)(fireEndTime - currentTime) < (uint16_t)(tile->fireEndTime - currentTime))
{
return; // Tile already on fire, and that fire lasts longer.
}
// Burn, tile, burn!
tile->tileInfoBits |= BITS_ON_FIRE;
tile->fireEndTime = fireEndTime;
syncDebug("Fire tile{%d, %d} dur%u end%d", posX, posY, duration, fireEndTime);
}
/** Check if tile contained within the given world coordinates is burning. */
bool fireOnLocation(unsigned int x, unsigned int y)
{
const int posX = map_coord(x);
const int posY = map_coord(y);
const MAPTILE *psTile = mapTile(posX, posY);
ASSERT(psTile, "Checking fire on tile outside the map (%d, %d)", posX, posY);
return psTile != NULL && TileIsBurning(psTile);
}
// This function runs in a separate thread!
static int dangerFloodFill(int player)
{
int i;
Vector2i pos = getPlayerStartPosition(player);
Vector2i npos;
uint8_t aux, block;
int x, y;
bool start = true; // hack to disregard the blocking status of any building exactly on the starting position
// Set our danger bits
for (y = 0; y < mapHeight; y++)
{
for (x = 0; x < mapWidth; x++)
{
auxSet(x, y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_DANGER);
auxClear(x, y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_TEMPORARY);
}
}
pos.x = map_coord(pos.x);
pos.y = map_coord(pos.y);
bucketcounter = 0;
do
{
// Add accessible neighbouring tiles to the open list
for (i = 0; i < NUM_DIR; i++)
{
npos.x = pos.x + aDirOffset[i].x;
npos.y = pos.y + aDirOffset[i].y;
if (!tileOnMap(npos.x, npos.y))
{
continue;
}
aux = auxTile(npos.x, npos.y, MAX_PLAYERS + AUX_DANGERMAP);
block = blockTile(pos.x, pos.y, AUX_DANGERMAP);
if (!(aux & AUXBITS_TEMPORARY) && !(aux & AUXBITS_THREAT) && (aux & AUXBITS_DANGER))
{
// Note that we do not consider water to be a blocker here. This may or may not be a feature...
if (!(block & FEATURE_BLOCKED) && (!(aux & AUXBITS_NONPASSABLE) || start))
{
floodbucket[bucketcounter].x = npos.x;
floodbucket[bucketcounter].y = npos.y;
bucketcounter++;
if (start && !(aux & AUXBITS_NONPASSABLE))
{
start = false;
}
}
else
{
auxClear(npos.x, npos.y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_DANGER);
}
auxSet(npos.x, npos.y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_TEMPORARY); // make sure we do not process it more than once
}
}
// Clear danger
auxClear(pos.x, pos.y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_DANGER);
// Pop the last open node off the bucket list for the next iteration
if (bucketcounter)
{
bucketcounter--;
pos.x = floodbucket[bucketcounter].x;
pos.y = floodbucket[bucketcounter].y;
}
} while (bucketcounter);
return 0;
}
// This function runs in a separate thread!
static int dangerThreadFunc(WZ_DECL_UNUSED void *data)
{
while (lastDangerPlayer != -1)
{
dangerFloodFill(lastDangerPlayer); // Do the actual work
wzSemaphorePost(dangerDoneSemaphore); // Signal that we are done
wzSemaphoreWait(dangerSemaphore); // Go to sleep until needed.
}
return 0;
}
static inline void threatUpdateTarget(int player, BASE_OBJECT *psObj, bool ground, bool air)
{
int i;
if (psObj->visible[player] || psObj->born == 2)
{
for (i = 0; i < psObj->numWatchedTiles; i++)
{
const TILEPOS pos = psObj->watchedTiles[i];
if (ground)
{
auxSet(pos.x, pos.y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_THREAT); // set ground threat for this tile
}
if (air)
{
auxSet(pos.x, pos.y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_AATHREAT); // set air threat for this tile
}
}
}
}
static void threatUpdate(int player)
{
int i, weapon, x, y;
// Step 1: Clear our threat bits
for (y = 0; y < mapHeight; y++)
{
for (x = 0; x < mapWidth; x++)
{
auxClear(x, y, MAX_PLAYERS + AUX_DANGERMAP, AUXBITS_THREAT | AUXBITS_AATHREAT);
}
}
// Step 2: Set threat bits
for (i = 0; i < MAX_PLAYERS; i++)
{
DROID *psDroid;
STRUCTURE *psStruct;
if (aiCheckAlliances(player, i))
{
// No need to iterate friendly objects
continue;
}
for (psDroid = apsDroidLists[i]; psDroid; psDroid = psDroid->psNext)
{
UBYTE mode = 0;
if (psDroid->droidType == DROID_CONSTRUCT || psDroid->droidType == DROID_CYBORG_CONSTRUCT
|| psDroid->droidType == DROID_REPAIR || psDroid->droidType == DROID_CYBORG_REPAIR)
{
continue; // hack that really should not be needed, but is -- trucks can SHOOT_ON_GROUND...!
}
for (weapon = 0; weapon < psDroid->numWeaps; weapon++)
{
mode |= asWeaponStats[psDroid->asWeaps[weapon].nStat].surfaceToAir;
}
if (psDroid->droidType == DROID_SENSOR) // special treatment for sensor turrets, no multiweapon support
{
mode |= SHOOT_ON_GROUND; // assume it only shoots at ground targets for now
}
if (mode > 0)
{
threatUpdateTarget(player, (BASE_OBJECT *)psDroid, mode & SHOOT_ON_GROUND, mode & SHOOT_IN_AIR);
}
}
for (psStruct = apsStructLists[i]; psStruct; psStruct = psStruct->psNext)
{
UBYTE mode = 0;
for (weapon = 0; weapon < psStruct->numWeaps; weapon++)
{
mode |= asWeaponStats[psStruct->asWeaps[weapon].nStat].surfaceToAir;
}
if (psStruct->pStructureType->pSensor && psStruct->pStructureType->pSensor->location == LOC_TURRET) // special treatment for sensor turrets
{
mode |= SHOOT_ON_GROUND; // assume it only shoots at ground targets for now
}
if (mode > 0)
{
threatUpdateTarget(player, (BASE_OBJECT *)psStruct, mode & SHOOT_ON_GROUND, mode & SHOOT_IN_AIR);
}
}
}
}
void mapInit()
{
int player;
free(floodbucket);
floodbucket = (struct floodtile *)malloc(mapWidth * mapHeight * sizeof(*floodbucket));
lastDangerUpdate = 0;
lastDangerPlayer = -1;
// Initialize danger maps
for (player = 0; player < MAX_PLAYERS; player++)
{
auxMapStore(player, AUX_DANGERMAP);
threatUpdate(player);
dangerFloodFill(player);
auxMapRestore(player, AUX_DANGERMAP, AUXBITS_DANGER | AUXBITS_THREAT | AUXBITS_AATHREAT);
}
// Start thread
ASSERT(dangerSemaphore == NULL && dangerThread == NULL, "Map data not cleaned up before starting!");
if (game.type == SKIRMISH)
{
lastDangerPlayer = 0;
dangerSemaphore = wzSemaphoreCreate(0);
dangerDoneSemaphore = wzSemaphoreCreate(0);
dangerThread = wzThreadCreate(dangerThreadFunc, NULL);
wzThreadStart(dangerThread);
}
}
void mapUpdate()
{
const uint16_t currentTime = gameTime / GAME_TICKS_PER_UPDATE;
int posX, posY;
for (posY = 0; posY < mapHeight; ++posY)
for (posX = 0; posX < mapWidth; ++posX)
{
MAPTILE *const tile = mapTile(posX, posY);
if ((tile->tileInfoBits & BITS_ON_FIRE) != 0 && tile->fireEndTime == currentTime)
{
// Extinguish, tile, extinguish!
tile->tileInfoBits &= ~BITS_ON_FIRE;
syncDebug("Extinguished tile{%d, %d}", posX, posY);
}
}
if (gameTime > lastDangerUpdate + GAME_TICKS_FOR_DANGER && game.type == SKIRMISH)
{
// Lock if previous job not done yet
wzSemaphoreWait(dangerDoneSemaphore);
auxMapRestore(lastDangerPlayer, AUX_DANGERMAP, AUXBITS_THREAT | AUXBITS_AATHREAT | AUXBITS_DANGER);
lastDangerPlayer = (lastDangerPlayer + 1 ) % game.maxPlayers;
auxMapStore(lastDangerPlayer, AUX_DANGERMAP);
threatUpdate(lastDangerPlayer);
wzSemaphorePost(dangerSemaphore);
}
}