Index: ps/trunk/binaries/data/mods/public/art/terrains/biome-mediterranean/medit_city_tile.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/art/terrains/biome-mediterranean/medit_city_tile.xml (revision 26268)
+++ ps/trunk/binaries/data/mods/public/art/terrains/biome-mediterranean/medit_city_tile.xml (revision 26269)
@@ -1,10 +1,10 @@
-
+ terrain_norm_spec.xml
-
\ No newline at end of file
+
Index: ps/trunk/binaries/data/mods/public/art/terrains/biome-mediterranean/medit_road_broken.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/art/terrains/biome-mediterranean/medit_road_broken.xml (revision 26268)
+++ ps/trunk/binaries/data/mods/public/art/terrains/biome-mediterranean/medit_road_broken.xml (revision 26269)
@@ -1,10 +1,10 @@
-
+ terrain_norm_spec.xml
-
\ No newline at end of file
+
Index: ps/trunk/binaries/data/mods/public/art/terrains/biome-savanna/savanna_tile_a.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/art/terrains/biome-savanna/savanna_tile_a.xml (revision 26268)
+++ ps/trunk/binaries/data/mods/public/art/terrains/biome-savanna/savanna_tile_a.xml (revision 26269)
@@ -1,10 +1,10 @@
-
+ terrain_norm_spec.xml
-
\ No newline at end of file
+
Index: ps/trunk/binaries/data/mods/public/art/terrains/biome-savanna/savanna_tile_a_red.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/art/terrains/biome-savanna/savanna_tile_a_red.xml (revision 26268)
+++ ps/trunk/binaries/data/mods/public/art/terrains/biome-savanna/savanna_tile_a_red.xml (revision 26269)
@@ -1,10 +1,10 @@
-
+ terrain_norm_spec.xml
-
\ No newline at end of file
+
Index: ps/trunk/binaries/data/mods/public/art/terrains/biome-temperate/temp_road_muddy.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/art/terrains/biome-temperate/temp_road_muddy.xml (revision 26268)
+++ ps/trunk/binaries/data/mods/public/art/terrains/biome-temperate/temp_road_muddy.xml (revision 26269)
@@ -1,8 +1,8 @@
-
+ terrain_base.xml
Index: ps/trunk/binaries/data/mods/public/art/terrains/biome-tropic/tropic_citytile_a.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/art/terrains/biome-tropic/tropic_citytile_a.xml (revision 26268)
+++ ps/trunk/binaries/data/mods/public/art/terrains/biome-tropic/tropic_citytile_a.xml (revision 26269)
@@ -1,8 +1,8 @@
-
+ terrain_base.xml
Index: ps/trunk/binaries/data/mods/public/art/terrains/terrain.rng
===================================================================
--- ps/trunk/binaries/data/mods/public/art/terrains/terrain.rng (revision 26268)
+++ ps/trunk/binaries/data/mods/public/art/terrains/terrain.rng (revision 26269)
@@ -1,46 +1,43 @@
0
255
0
255
0
255
-
-
-
Index: ps/trunk/binaries/data/mods/public/art/terrains/terrain_texture.rng
===================================================================
--- ps/trunk/binaries/data/mods/public/art/terrains/terrain_texture.rng (revision 26268)
+++ ps/trunk/binaries/data/mods/public/art/terrains/terrain_texture.rng (revision 26269)
@@ -1,69 +1,66 @@
0
0
0
-
-
-
Index: ps/trunk/source/graphics/MapReader.cpp
===================================================================
--- ps/trunk/source/graphics/MapReader.cpp (revision 26268)
+++ ps/trunk/source/graphics/MapReader.cpp (revision 26269)
@@ -1,1608 +1,1612 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "MapReader.h"
#include "graphics/Camera.h"
#include "graphics/CinemaManager.h"
#include "graphics/Entity.h"
#include "graphics/GameView.h"
#include "graphics/MapGenerator.h"
#include "graphics/Patch.h"
#include "graphics/Terrain.h"
#include "graphics/TerrainTextureEntry.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/timer.h"
#include "lib/external_libraries/libsdl.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Loader.h"
#include "ps/LoaderThunks.h"
#include "ps/World.h"
#include "ps/XML/Xeromyces.h"
#include "renderer/PostprocManager.h"
#include "renderer/SkyManager.h"
#include "renderer/WaterManager.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptRequest.h"
#include "scriptinterface/JSON.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpCinemaManager.h"
#include "simulation2/components/ICmpGarrisonHolder.h"
#include "simulation2/components/ICmpObstruction.h"
#include "simulation2/components/ICmpOwnership.h"
#include "simulation2/components/ICmpPlayer.h"
#include "simulation2/components/ICmpPlayerManager.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/components/ICmpTurretHolder.h"
#include "simulation2/components/ICmpVisual.h"
#include "simulation2/components/ICmpWaterManager.h"
#include
#if defined(_MSC_VER) && _MSC_VER > 1900
#pragma warning(disable: 4456) // Declaration hides previous local declaration.
#pragma warning(disable: 4458) // Declaration hides class member.
#endif
CMapReader::CMapReader()
: xml_reader(0), m_PatchesPerSide(0), m_MapGen(0)
{
cur_terrain_tex = 0; // important - resets generator state
}
// LoadMap: try to load the map from given file; reinitialise the scene to new data if successful
void CMapReader::LoadMap(const VfsPath& pathname, const ScriptContext& cx, JS::HandleValue settings, CTerrain *pTerrain_,
WaterManager* pWaterMan_, SkyManager* pSkyMan_,
CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_,
CSimulation2 *pSimulation2_, const CSimContext* pSimContext_, int playerID_, bool skipEntities)
{
pTerrain = pTerrain_;
pLightEnv = pLightEnv_;
pGameView = pGameView_;
pWaterMan = pWaterMan_;
pSkyMan = pSkyMan_;
pCinema = pCinema_;
pTrigMan = pTrigMan_;
pPostproc = pPostproc_;
pSimulation2 = pSimulation2_;
pSimContext = pSimContext_;
m_PlayerID = playerID_;
m_SkipEntities = skipEntities;
m_StartingCameraTarget = INVALID_ENTITY;
m_ScriptSettings.init(cx.GetGeneralJSContext(), settings);
filename_xml = pathname.ChangeExtension(L".xml");
// In some cases (particularly tests) we don't want to bother storing a large
// mostly-empty .pmp file, so we let the XML file specify basic terrain instead.
// If there's an .xml file and no .pmp, then we're probably in this XML-only mode
only_xml = false;
if (!VfsFileExists(pathname) && VfsFileExists(filename_xml))
{
only_xml = true;
}
file_format_version = CMapIO::FILE_VERSION; // default if there's no .pmp
if (!only_xml)
{
// [25ms]
unpacker.Read(pathname, "PSMP");
file_format_version = unpacker.GetVersion();
}
// check oldest supported version
if (file_format_version < FILE_READ_VERSION)
throw PSERROR_Game_World_MapLoadFailed("Could not load terrain file - too old version!");
// delete all existing entities
if (pSimulation2)
pSimulation2->ResetState();
// reset post effects
if (pPostproc)
pPostproc->SetPostEffect(L"default");
// load map or script settings script
if (settings.isUndefined())
RegMemFun(this, &CMapReader::LoadScriptSettings, L"CMapReader::LoadScriptSettings", 50);
else
RegMemFun(this, &CMapReader::LoadRMSettings, L"CMapReader::LoadRMSettings", 50);
// load player settings script (must be done before reading map)
RegMemFun(this, &CMapReader::LoadPlayerSettings, L"CMapReader::LoadPlayerSettings", 50);
// unpack the data
if (!only_xml)
RegMemFun(this, &CMapReader::UnpackMap, L"CMapReader::UnpackMap", 1200);
// read the corresponding XML file
RegMemFun(this, &CMapReader::ReadXML, L"CMapReader::ReadXML", 50);
// apply terrain data to the world
RegMemFun(this, &CMapReader::ApplyTerrainData, L"CMapReader::ApplyTerrainData", 5);
// read entities
RegMemFun(this, &CMapReader::ReadXMLEntities, L"CMapReader::ReadXMLEntities", 5800);
// apply misc data to the world
RegMemFun(this, &CMapReader::ApplyData, L"CMapReader::ApplyData", 5);
// load map settings script (must be done after reading map)
RegMemFun(this, &CMapReader::LoadMapSettings, L"CMapReader::LoadMapSettings", 5);
}
// LoadRandomMap: try to load the map data; reinitialise the scene to new data if successful
void CMapReader::LoadRandomMap(const CStrW& scriptFile, const ScriptContext& cx, JS::HandleValue settings, CTerrain *pTerrain_,
WaterManager* pWaterMan_, SkyManager* pSkyMan_,
CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_,
CSimulation2 *pSimulation2_, int playerID_)
{
m_ScriptFile = scriptFile;
pSimulation2 = pSimulation2_;
pSimContext = pSimulation2 ? &pSimulation2->GetSimContext() : NULL;
m_ScriptSettings.init(cx.GetGeneralJSContext(), settings);
pTerrain = pTerrain_;
pLightEnv = pLightEnv_;
pGameView = pGameView_;
pWaterMan = pWaterMan_;
pSkyMan = pSkyMan_;
pCinema = pCinema_;
pTrigMan = pTrigMan_;
pPostproc = pPostproc_;
m_PlayerID = playerID_;
m_SkipEntities = false;
m_StartingCameraTarget = INVALID_ENTITY;
// delete all existing entities
if (pSimulation2)
pSimulation2->ResetState();
only_xml = false;
// copy random map settings (before entity creation)
RegMemFun(this, &CMapReader::LoadRMSettings, L"CMapReader::LoadRMSettings", 50);
// load player settings script (must be done before reading map)
RegMemFun(this, &CMapReader::LoadPlayerSettings, L"CMapReader::LoadPlayerSettings", 50);
// load map generator with random map script
RegMemFun(this, &CMapReader::GenerateMap, L"CMapReader::GenerateMap", 20000);
// parse RMS results into terrain structure
RegMemFun(this, &CMapReader::ParseTerrain, L"CMapReader::ParseTerrain", 500);
// parse RMS results into environment settings
RegMemFun(this, &CMapReader::ParseEnvironment, L"CMapReader::ParseEnvironment", 5);
// parse RMS results into camera settings
RegMemFun(this, &CMapReader::ParseCamera, L"CMapReader::ParseCamera", 5);
// apply terrain data to the world
RegMemFun(this, &CMapReader::ApplyTerrainData, L"CMapReader::ApplyTerrainData", 5);
// parse RMS results into entities
RegMemFun(this, &CMapReader::ParseEntities, L"CMapReader::ParseEntities", 1000);
// apply misc data to the world
RegMemFun(this, &CMapReader::ApplyData, L"CMapReader::ApplyData", 5);
// load map settings script (must be done after reading map)
RegMemFun(this, &CMapReader::LoadMapSettings, L"CMapReader::LoadMapSettings", 5);
}
// UnpackMap: unpack the given data from the raw data stream into local variables
int CMapReader::UnpackMap()
{
return UnpackTerrain();
}
// UnpackTerrain: unpack the terrain from the end of the input data stream
// - data: map size, heightmap, list of textures used by map, texture tile assignments
int CMapReader::UnpackTerrain()
{
// yield after this time is reached. balances increased progress bar
// smoothness vs. slowing down loading.
const double end_time = timer_Time() + 200e-3;
// first call to generator (this is skipped after first call,
// i.e. when the loop below was interrupted)
if (cur_terrain_tex == 0)
{
m_PatchesPerSide = (ssize_t)unpacker.UnpackSize();
// unpack heightmap [600us]
size_t verticesPerSide = m_PatchesPerSide*PATCH_SIZE+1;
m_Heightmap.resize(SQR(verticesPerSide));
unpacker.UnpackRaw(&m_Heightmap[0], SQR(verticesPerSide)*sizeof(u16));
// unpack # textures
num_terrain_tex = unpacker.UnpackSize();
m_TerrainTextures.reserve(num_terrain_tex);
}
// unpack texture names; find handle for each texture.
// interruptible.
while (cur_terrain_tex < num_terrain_tex)
{
CStr texturename;
unpacker.UnpackString(texturename);
- ENSURE(CTerrainTextureManager::IsInitialised()); // we need this for the terrain properties (even when graphics are disabled)
- CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texturename);
- m_TerrainTextures.push_back(texentry);
+ if(CTerrainTextureManager::IsInitialised())
+ {
+ CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texturename);
+ m_TerrainTextures.push_back(texentry);
+ }
cur_terrain_tex++;
LDR_CHECK_TIMEOUT(cur_terrain_tex, num_terrain_tex);
}
// unpack tile data [3ms]
ssize_t tilesPerSide = m_PatchesPerSide*PATCH_SIZE;
m_Tiles.resize(size_t(SQR(tilesPerSide)));
unpacker.UnpackRaw(&m_Tiles[0], sizeof(STileDesc)*m_Tiles.size());
// reset generator state.
cur_terrain_tex = 0;
return 0;
}
int CMapReader::ApplyTerrainData()
{
if (m_PatchesPerSide == 0)
{
// we'll probably crash when trying to use this map later
throw PSERROR_Game_World_MapLoadFailed("Error loading map: no terrain data.\nCheck application log for details.");
}
if (!only_xml)
{
// initialise the terrain
pTerrain->Initialize(m_PatchesPerSide, &m_Heightmap[0]);
// setup the textures on the minipatches
STileDesc* tileptr = &m_Tiles[0];
for (ssize_t j=0; jGetPatch(i,j)->m_MiniPatches[m][k]; // can't fail
mp.Tex = m_TerrainTextures[tileptr->m_Tex1Index];
mp.Priority = tileptr->m_Priority;
tileptr++;
}
}
}
}
}
CmpPtr cmpTerrain(*pSimContext, SYSTEM_ENTITY);
if (cmpTerrain)
cmpTerrain->ReloadTerrain();
return 0;
}
// ApplyData: take all the input data, and rebuild the scene from it
int CMapReader::ApplyData()
{
// copy over the lighting parameters
if (pLightEnv)
*pLightEnv = m_LightEnv;
CmpPtr cmpPlayerManager(*pSimContext, SYSTEM_ENTITY);
if (pGameView && cmpPlayerManager)
{
// Default to global camera (with constraints)
pGameView->ResetCameraTarget(pGameView->GetCamera()->GetFocus());
// TODO: Starting rotation?
CmpPtr cmpPlayer(*pSimContext, cmpPlayerManager->GetPlayerByID(m_PlayerID));
if (cmpPlayer && cmpPlayer->HasStartingCamera())
{
// Use player starting camera
CFixedVector3D pos = cmpPlayer->GetStartingCameraPos();
pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat()));
}
else if (m_StartingCameraTarget != INVALID_ENTITY)
{
// Point camera at entity
CmpPtr cmpPosition(*pSimContext, m_StartingCameraTarget);
if (cmpPosition)
{
CFixedVector3D pos = cmpPosition->GetPosition();
pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat()));
}
}
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
PSRETURN CMapSummaryReader::LoadMap(const VfsPath& pathname)
{
VfsPath filename_xml = pathname.ChangeExtension(L".xml");
CXeromyces xmb_file;
if (xmb_file.Load(g_VFS, filename_xml, "scenario") != PSRETURN_OK)
return PSRETURN_File_ReadFailed;
// Define all the relevant elements used in the XML file
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(scenario);
EL(scriptsettings);
#undef AT
#undef EL
XMBElement root = xmb_file.GetRoot();
ENSURE(root.GetNodeName() == el_scenario);
XERO_ITER_EL(root, child)
{
int child_name = child.GetNodeName();
if (child_name == el_scriptsettings)
{
m_ScriptSettings = child.GetText();
}
}
return PSRETURN_OK;
}
void CMapSummaryReader::GetMapSettings(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
ScriptRequest rq(scriptInterface);
Script::CreateObject(rq, ret);
if (m_ScriptSettings.empty())
return;
JS::RootedValue scriptSettingsVal(rq.cx);
Script::ParseJSON(rq, m_ScriptSettings, &scriptSettingsVal);
Script::SetProperty(rq, ret, "settings", scriptSettingsVal, false);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Holds various state data while reading maps, so that loading can be
// interrupted (e.g. to update the progress display) then later resumed.
class CXMLReader
{
NONCOPYABLE(CXMLReader);
public:
CXMLReader(const VfsPath& xml_filename, CMapReader& mapReader)
: m_MapReader(mapReader), nodes(NULL, 0, NULL)
{
Init(xml_filename);
}
CStr ReadScriptSettings();
// read everything except for entities
void ReadXML();
// return semantics: see Loader.cpp!LoadFunc.
int ProgressiveReadEntities();
private:
CXeromyces xmb_file;
CMapReader& m_MapReader;
int el_entity;
int el_tracks;
int el_template, el_player;
int el_position, el_orientation, el_obstruction;
int el_garrison;
int el_turrets;
int el_actor;
int at_x;
int at_y;
int at_z;
int at_group, at_group2;
int at_angle;
int at_uid;
int at_seed;
int at_turret;
XMBElementList nodes; // children of root
// loop counters
size_t node_idx;
size_t entity_idx;
// # entities+nonentities processed and total (for progress calc)
int completed_jobs, total_jobs;
// maximum used entity ID, so we can safely allocate new ones
entity_id_t max_uid;
void Init(const VfsPath& xml_filename);
void ReadTerrain(XMBElement parent);
void ReadEnvironment(XMBElement parent);
void ReadCamera(XMBElement parent);
void ReadPaths(XMBElement parent);
void ReadTriggers(XMBElement parent);
int ReadEntities(XMBElement parent, double end_time);
};
void CXMLReader::Init(const VfsPath& xml_filename)
{
// must only assign once, so do it here
node_idx = entity_idx = 0;
if (xmb_file.Load(g_VFS, xml_filename, "scenario") != PSRETURN_OK)
throw PSERROR_Game_World_MapLoadFailed("Could not read map XML file!");
// define the elements and attributes that are frequently used in the XML file,
// so we don't need to do lots of string construction and comparison when
// reading the data.
// (Needs to be synchronised with the list in CXMLReader - ugh)
#define EL(x) el_##x = xmb_file.GetElementID(#x)
#define AT(x) at_##x = xmb_file.GetAttributeID(#x)
EL(entity);
EL(tracks);
EL(template);
EL(player);
EL(position);
EL(garrison);
EL(turrets);
EL(orientation);
EL(obstruction);
EL(actor);
AT(x); AT(y); AT(z);
AT(group); AT(group2);
AT(angle);
AT(uid);
AT(seed);
AT(turret);
#undef AT
#undef EL
XMBElement root = xmb_file.GetRoot();
ENSURE(xmb_file.GetElementStringView(root.GetNodeName()) == "Scenario");
nodes = root.GetChildNodes();
// find out total number of entities+nonentities
// (used when calculating progress)
completed_jobs = 0;
total_jobs = 0;
for (XMBElement node : nodes)
total_jobs += node.GetChildNodes().size();
// Find the maximum entity ID, so we can safely allocate new IDs without conflicts
max_uid = SYSTEM_ENTITY;
XMBElement ents = nodes.GetFirstNamedItem(xmb_file.GetElementID("Entities"));
XERO_ITER_EL(ents, ent)
{
CStr uid = ent.GetAttributes().GetNamedItem(at_uid);
max_uid = std::max(max_uid, (entity_id_t)uid.ToUInt());
}
}
CStr CXMLReader::ReadScriptSettings()
{
XMBElement root = xmb_file.GetRoot();
ENSURE(xmb_file.GetElementStringView(root.GetNodeName()) == "Scenario");
nodes = root.GetChildNodes();
XMBElement settings = nodes.GetFirstNamedItem(xmb_file.GetElementID("ScriptSettings"));
return settings.GetText();
}
void CXMLReader::ReadTerrain(XMBElement parent)
{
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
AT(patches);
AT(texture);
AT(priority);
AT(height);
#undef AT
ssize_t patches = 9;
CStr texture = "grass1_spring";
int priority = 0;
u16 height = 16384;
XERO_ITER_ATTR(parent, attr)
{
if (attr.Name == at_patches)
patches = attr.Value.ToInt();
else if (attr.Name == at_texture)
texture = attr.Value;
else if (attr.Name == at_priority)
priority = attr.Value.ToInt();
else if (attr.Name == at_height)
height = (u16)attr.Value.ToInt();
}
m_MapReader.m_PatchesPerSide = patches;
// Load the texture
ENSURE(CTerrainTextureManager::IsInitialised()); // we need this for the terrain properties (even when graphics are disabled)
CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texture);
m_MapReader.pTerrain->Initialize(patches, NULL);
// Fill the heightmap
u16* heightmap = m_MapReader.pTerrain->GetHeightMap();
ssize_t verticesPerSide = m_MapReader.pTerrain->GetVerticesPerSide();
for (ssize_t i = 0; i < SQR(verticesPerSide); ++i)
heightmap[i] = height;
// Fill the texture map
for (ssize_t pz = 0; pz < patches; ++pz)
{
for (ssize_t px = 0; px < patches; ++px)
{
CPatch* patch = m_MapReader.pTerrain->GetPatch(px, pz); // can't fail
for (ssize_t z = 0; z < PATCH_SIZE; ++z)
{
for (ssize_t x = 0; x < PATCH_SIZE; ++x)
{
patch->m_MiniPatches[z][x].Tex = texentry;
patch->m_MiniPatches[z][x].Priority = priority;
}
}
}
}
}
void CXMLReader::ReadEnvironment(XMBElement parent)
{
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(posteffect);
EL(skyset);
EL(suncolor);
EL(sunelevation);
EL(sunrotation);
EL(ambientcolor);
EL(water);
EL(waterbody);
EL(type);
EL(color);
EL(tint);
EL(height);
EL(waviness);
EL(murkiness);
EL(windangle);
EL(fog);
EL(fogcolor);
EL(fogfactor);
EL(fogthickness);
EL(postproc);
EL(brightness);
EL(contrast);
EL(saturation);
EL(bloom);
AT(r); AT(g); AT(b);
#undef AT
#undef EL
XERO_ITER_EL(parent, element)
{
int element_name = element.GetNodeName();
XMBAttributeList attrs = element.GetAttributes();
if (element_name == el_skyset)
{
if (m_MapReader.pSkyMan)
m_MapReader.pSkyMan->SetSkySet(element.GetText().FromUTF8());
}
else if (element_name == el_suncolor)
{
m_MapReader.m_LightEnv.m_SunColor = RGBColor(
attrs.GetNamedItem(at_r).ToFloat(),
attrs.GetNamedItem(at_g).ToFloat(),
attrs.GetNamedItem(at_b).ToFloat());
}
else if (element_name == el_sunelevation)
{
m_MapReader.m_LightEnv.m_Elevation = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_sunrotation)
{
m_MapReader.m_LightEnv.m_Rotation = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_ambientcolor)
{
m_MapReader.m_LightEnv.m_AmbientColor = RGBColor(
attrs.GetNamedItem(at_r).ToFloat(),
attrs.GetNamedItem(at_g).ToFloat(),
attrs.GetNamedItem(at_b).ToFloat());
}
else if (element_name == el_fog)
{
XERO_ITER_EL(element, fog)
{
int fog_element_name = fog.GetNodeName();
if (fog_element_name == el_fogcolor)
{
XMBAttributeList fogAttributes = fog.GetAttributes();
m_MapReader.m_LightEnv.m_FogColor = RGBColor(
fogAttributes.GetNamedItem(at_r).ToFloat(),
fogAttributes.GetNamedItem(at_g).ToFloat(),
fogAttributes.GetNamedItem(at_b).ToFloat());
}
else if (fog_element_name == el_fogfactor)
{
m_MapReader.m_LightEnv.m_FogFactor = fog.GetText().ToFloat();
}
else if (fog_element_name == el_fogthickness)
{
m_MapReader.m_LightEnv.m_FogMax = fog.GetText().ToFloat();
}
}
}
else if (element_name == el_postproc)
{
XERO_ITER_EL(element, postproc)
{
int post_element_name = postproc.GetNodeName();
if (post_element_name == el_brightness)
{
m_MapReader.m_LightEnv.m_Brightness = postproc.GetText().ToFloat();
}
else if (post_element_name == el_contrast)
{
m_MapReader.m_LightEnv.m_Contrast = postproc.GetText().ToFloat();
}
else if (post_element_name == el_saturation)
{
m_MapReader.m_LightEnv.m_Saturation = postproc.GetText().ToFloat();
}
else if (post_element_name == el_bloom)
{
m_MapReader.m_LightEnv.m_Bloom = postproc.GetText().ToFloat();
}
else if (post_element_name == el_posteffect)
{
if (m_MapReader.pPostproc)
m_MapReader.pPostproc->SetPostEffect(postproc.GetText().FromUTF8());
}
}
}
else if (element_name == el_water)
{
XERO_ITER_EL(element, waterbody)
{
ENSURE(waterbody.GetNodeName() == el_waterbody);
XERO_ITER_EL(waterbody, waterelement)
{
int water_element_name = waterelement.GetNodeName();
if (water_element_name == el_height)
{
CmpPtr cmpWaterManager(*m_MapReader.pSimContext, SYSTEM_ENTITY);
ENSURE(cmpWaterManager);
cmpWaterManager->SetWaterLevel(entity_pos_t::FromString(waterelement.GetText()));
continue;
}
// The rest are purely graphical effects, and should be ignored if
// graphics are disabled
if (!m_MapReader.pWaterMan)
continue;
if (water_element_name == el_type)
{
if (waterelement.GetText() == "default")
m_MapReader.pWaterMan->m_WaterType = L"ocean";
else
m_MapReader.pWaterMan->m_WaterType = waterelement.GetText().FromUTF8();
}
#define READ_COLOR(el, out) \
else if (water_element_name == el) \
{ \
XMBAttributeList colorAttrs = waterelement.GetAttributes(); \
out = CColor( \
colorAttrs.GetNamedItem(at_r).ToFloat(), \
colorAttrs.GetNamedItem(at_g).ToFloat(), \
colorAttrs.GetNamedItem(at_b).ToFloat(), \
1.f); \
}
#define READ_FLOAT(el, out) \
else if (water_element_name == el) \
{ \
out = waterelement.GetText().ToFloat(); \
} \
READ_COLOR(el_color, m_MapReader.pWaterMan->m_WaterColor)
READ_COLOR(el_tint, m_MapReader.pWaterMan->m_WaterTint)
READ_FLOAT(el_waviness, m_MapReader.pWaterMan->m_Waviness)
READ_FLOAT(el_murkiness, m_MapReader.pWaterMan->m_Murkiness)
READ_FLOAT(el_windangle, m_MapReader.pWaterMan->m_WindAngle)
#undef READ_FLOAT
#undef READ_COLOR
else
debug_warn(L"Invalid map XML data");
}
}
}
else
debug_warn(L"Invalid map XML data");
}
m_MapReader.m_LightEnv.CalculateSunDirection();
}
void CXMLReader::ReadCamera(XMBElement parent)
{
// defaults if we don't find player starting camera
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(declination);
EL(rotation);
EL(position);
AT(angle);
AT(x); AT(y); AT(z);
#undef AT
#undef EL
float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f);
CVector3D translation = CVector3D(100, 150, -100);
XERO_ITER_EL(parent, element)
{
int element_name = element.GetNodeName();
XMBAttributeList attrs = element.GetAttributes();
if (element_name == el_declination)
{
declination = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_rotation)
{
rotation = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_position)
{
translation = CVector3D(
attrs.GetNamedItem(at_x).ToFloat(),
attrs.GetNamedItem(at_y).ToFloat(),
attrs.GetNamedItem(at_z).ToFloat());
}
else
debug_warn(L"Invalid map XML data");
}
if (m_MapReader.pGameView)
{
m_MapReader.pGameView->GetCamera()->m_Orientation.SetXRotation(declination);
m_MapReader.pGameView->GetCamera()->m_Orientation.RotateY(rotation);
m_MapReader.pGameView->GetCamera()->m_Orientation.Translate(translation);
m_MapReader.pGameView->GetCamera()->UpdateFrustum();
}
}
void CXMLReader::ReadPaths(XMBElement parent)
{
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(path);
EL(rotation);
EL(node);
EL(position);
EL(target);
AT(name);
AT(timescale);
AT(orientation);
AT(mode);
AT(style);
AT(x);
AT(y);
AT(z);
AT(deltatime);
#undef EL
#undef AT
CmpPtr cmpCinemaManager(*m_MapReader.pSimContext, SYSTEM_ENTITY);
XERO_ITER_EL(parent, element)
{
int elementName = element.GetNodeName();
if (elementName == el_path)
{
CCinemaData pathData;
XMBAttributeList attrs = element.GetAttributes();
CStrW pathName(attrs.GetNamedItem(at_name).FromUTF8());
pathData.m_Name = pathName;
pathData.m_Timescale = fixed::FromString(attrs.GetNamedItem(at_timescale));
pathData.m_Orientation = attrs.GetNamedItem(at_orientation).FromUTF8();
pathData.m_Mode = attrs.GetNamedItem(at_mode).FromUTF8();
pathData.m_Style = attrs.GetNamedItem(at_style).FromUTF8();
TNSpline positionSpline, targetSpline;
fixed lastPositionTime = fixed::Zero();
fixed lastTargetTime = fixed::Zero();
XERO_ITER_EL(element, pathChild)
{
elementName = pathChild.GetNodeName();
attrs = pathChild.GetAttributes();
// Load node data used for spline
if (elementName == el_node)
{
lastPositionTime += fixed::FromString(attrs.GetNamedItem(at_deltatime));
lastTargetTime += fixed::FromString(attrs.GetNamedItem(at_deltatime));
XERO_ITER_EL(pathChild, nodeChild)
{
elementName = nodeChild.GetNodeName();
attrs = nodeChild.GetAttributes();
if (elementName == el_position)
{
CFixedVector3D position(fixed::FromString(attrs.GetNamedItem(at_x)),
fixed::FromString(attrs.GetNamedItem(at_y)),
fixed::FromString(attrs.GetNamedItem(at_z)));
positionSpline.AddNode(position, CFixedVector3D(), lastPositionTime);
lastPositionTime = fixed::Zero();
}
else if (elementName == el_rotation)
{
// TODO: Implement rotation slerp/spline as another object
}
else if (elementName == el_target)
{
CFixedVector3D targetPosition(fixed::FromString(attrs.GetNamedItem(at_x)),
fixed::FromString(attrs.GetNamedItem(at_y)),
fixed::FromString(attrs.GetNamedItem(at_z)));
targetSpline.AddNode(targetPosition, CFixedVector3D(), lastTargetTime);
lastTargetTime = fixed::Zero();
}
else
LOGWARNING("Invalid cinematic element for node child");
}
}
else
LOGWARNING("Invalid cinematic element for path child");
}
// Construct cinema path with data gathered
CCinemaPath path(pathData, positionSpline, targetSpline);
if (path.Empty())
{
LOGWARNING("Path with name '%s' is empty", pathName.ToUTF8());
return;
}
if (!cmpCinemaManager)
continue;
if (!cmpCinemaManager->HasPath(pathName))
cmpCinemaManager->AddPath(path);
else
LOGWARNING("Path with name '%s' already exists", pathName.ToUTF8());
}
else
LOGWARNING("Invalid path child with name '%s'", element.GetText());
}
}
void CXMLReader::ReadTriggers(XMBElement UNUSED(parent))
{
}
int CXMLReader::ReadEntities(XMBElement parent, double end_time)
{
XMBElementList entities = parent.GetChildNodes();
ENSURE(m_MapReader.pSimulation2);
CSimulation2& sim = *m_MapReader.pSimulation2;
CmpPtr cmpPlayerManager(sim, SYSTEM_ENTITY);
while (entity_idx < entities.size())
{
// all new state at this scope and below doesn't need to be
// wrapped, since we only yield after a complete iteration.
XMBElement entity = entities[entity_idx++];
ENSURE(entity.GetNodeName() == el_entity);
XMBAttributeList attrs = entity.GetAttributes();
CStr uid = attrs.GetNamedItem(at_uid);
ENSURE(!uid.empty());
int EntityUid = uid.ToInt();
CStrW TemplateName;
int PlayerID = 0;
std::vector Garrison;
std::vector> Turrets;
CFixedVector3D Position;
CFixedVector3D Orientation;
long Seed = -1;
// Obstruction control groups.
entity_id_t ControlGroup = INVALID_ENTITY;
entity_id_t ControlGroup2 = INVALID_ENTITY;
XERO_ITER_EL(entity, setting)
{
int element_name = setting.GetNodeName();
//
if (element_name == el_template)
{
TemplateName = setting.GetText().FromUTF8();
}
//
else if (element_name == el_player)
{
PlayerID = setting.GetText().ToInt();
}
//
else if (element_name == el_position)
{
XMBAttributeList positionAttrs = setting.GetAttributes();
Position = CFixedVector3D(
fixed::FromString(positionAttrs.GetNamedItem(at_x)),
fixed::FromString(positionAttrs.GetNamedItem(at_y)),
fixed::FromString(positionAttrs.GetNamedItem(at_z)));
}
//
else if (element_name == el_orientation)
{
XMBAttributeList orientationAttrs = setting.GetAttributes();
Orientation = CFixedVector3D(
fixed::FromString(orientationAttrs.GetNamedItem(at_x)),
fixed::FromString(orientationAttrs.GetNamedItem(at_y)),
fixed::FromString(orientationAttrs.GetNamedItem(at_z)));
// TODO: what happens if some attributes are missing?
}
//
else if (element_name == el_obstruction)
{
XMBAttributeList obstructionAttrs = setting.GetAttributes();
ControlGroup = obstructionAttrs.GetNamedItem(at_group).ToInt();
ControlGroup2 = obstructionAttrs.GetNamedItem(at_group2).ToInt();
}
//
else if (element_name == el_garrison)
{
XMBElementList garrison = setting.GetChildNodes();
Garrison.reserve(garrison.size());
for (const XMBElement& garr_ent : garrison)
{
XMBAttributeList garrisonAttrs = garr_ent.GetAttributes();
Garrison.push_back(garrisonAttrs.GetNamedItem(at_uid).ToInt());
}
}
//
else if (element_name == el_turrets)
{
XMBElementList turrets = setting.GetChildNodes();
Turrets.reserve(turrets.size());
for (const XMBElement& turretPoint : turrets)
{
XMBAttributeList turretAttrs = turretPoint.GetAttributes();
Turrets.emplace_back(
turretAttrs.GetNamedItem(at_turret),
turretAttrs.GetNamedItem(at_uid).ToInt()
);
}
}
//
else if (element_name == el_actor)
{
XMBAttributeList attrs = setting.GetAttributes();
CStr seedStr = attrs.GetNamedItem(at_seed);
if (!seedStr.empty())
{
Seed = seedStr.ToLong();
ENSURE(Seed >= 0);
}
}
else
debug_warn(L"Invalid map XML data");
}
entity_id_t ent = sim.AddEntity(TemplateName, EntityUid);
entity_id_t player = cmpPlayerManager->GetPlayerByID(PlayerID);
if (ent == INVALID_ENTITY || player == INVALID_ENTITY)
{ // Don't add entities with invalid player IDs
LOGERROR("Failed to load entity template '%s'", utf8_from_wstring(TemplateName));
}
else
{
CmpPtr cmpPosition(sim, ent);
if (cmpPosition)
{
cmpPosition->JumpTo(Position.X, Position.Z);
cmpPosition->SetYRotation(Orientation.Y);
// TODO: other parts of the position
}
if (!Garrison.empty())
{
CmpPtr cmpGarrisonHolder(sim, ent);
if (cmpGarrisonHolder)
cmpGarrisonHolder->SetInitEntities(std::move(Garrison));
else
LOGERROR("CXMLMapReader::ReadEntities() entity '%d' of player '%d' has no GarrisonHolder component and thus cannot garrison units.", ent, PlayerID);
}
// Needs to be before ownership changes to prevent initialising
// subunits too soon.
if (!Turrets.empty())
{
CmpPtr cmpTurretHolder(sim, ent);
if (cmpTurretHolder)
cmpTurretHolder->SetInitEntities(std::move(Turrets));
else
LOGERROR("CXMLMapReader::ReadEntities() entity '%d' of player '%d' has no TurretHolder component and thus cannot use turrets.", ent, PlayerID);
}
CmpPtr cmpOwnership(sim, ent);
if (cmpOwnership)
cmpOwnership->SetOwner(PlayerID);
CmpPtr cmpObstruction(sim, ent);
if (cmpObstruction)
{
if (ControlGroup != INVALID_ENTITY)
cmpObstruction->SetControlGroup(ControlGroup);
if (ControlGroup2 != INVALID_ENTITY)
cmpObstruction->SetControlGroup2(ControlGroup2);
cmpObstruction->ResolveFoundationCollisions();
}
CmpPtr cmpVisual(sim, ent);
if (cmpVisual)
{
if (Seed != -1)
cmpVisual->SetActorSeed((u32)Seed);
// TODO: variation/selection strings
}
if (PlayerID == m_MapReader.m_PlayerID && (boost::algorithm::ends_with(TemplateName, L"civil_centre") || m_MapReader.m_StartingCameraTarget == INVALID_ENTITY))
{
// Focus on civil centre or first entity owned by player
m_MapReader.m_StartingCameraTarget = ent;
}
}
completed_jobs++;
LDR_CHECK_TIMEOUT(completed_jobs, total_jobs);
}
return 0;
}
void CXMLReader::ReadXML()
{
for (XMBElement node : nodes)
{
CStr name = xmb_file.GetElementString(node.GetNodeName());
if (name == "Terrain")
{
ReadTerrain(node);
}
else if (name == "Environment")
{
ReadEnvironment(node);
}
else if (name == "Camera")
{
ReadCamera(node);
}
else if (name == "ScriptSettings")
{
// Already loaded - this is to prevent an assertion
}
else if (name == "Entities")
{
// Handled by ProgressiveReadEntities instead
}
else if (name == "Paths")
{
ReadPaths(node);
}
else if (name == "Triggers")
{
ReadTriggers(node);
}
else if (name == "Script")
{
if (m_MapReader.pSimulation2)
m_MapReader.pSimulation2->SetStartupScript(node.GetText());
}
else
{
debug_printf("Invalid XML element in map file: %s\n", name.c_str());
debug_warn(L"Invalid map XML data");
}
}
}
int CXMLReader::ProgressiveReadEntities()
{
// yield after this time is reached. balances increased progress bar
// smoothness vs. slowing down loading.
const double end_time = timer_Time() + 200e-3;
int ret;
while (node_idx < nodes.size())
{
XMBElement node = nodes[node_idx];
CStr name = xmb_file.GetElementString(node.GetNodeName());
if (name == "Entities")
{
if (!m_MapReader.m_SkipEntities)
{
ret = ReadEntities(node, end_time);
if (ret != 0) // error or timed out
return ret;
}
}
node_idx++;
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// load script settings from map
int CMapReader::LoadScriptSettings()
{
if (!xml_reader)
xml_reader = new CXMLReader(filename_xml, *this);
// parse the script settings
if (pSimulation2)
pSimulation2->SetMapSettings(xml_reader->ReadScriptSettings());
return 0;
}
// load player settings script
int CMapReader::LoadPlayerSettings()
{
if (pSimulation2)
pSimulation2->LoadPlayerSettings(true);
return 0;
}
// load map settings script
int CMapReader::LoadMapSettings()
{
if (pSimulation2)
pSimulation2->LoadMapSettings();
return 0;
}
int CMapReader::ReadXML()
{
if (!xml_reader)
xml_reader = new CXMLReader(filename_xml, *this);
xml_reader->ReadXML();
return 0;
}
// progressive
int CMapReader::ReadXMLEntities()
{
if (!xml_reader)
xml_reader = new CXMLReader(filename_xml, *this);
int ret = xml_reader->ProgressiveReadEntities();
// finished or failed
if (ret <= 0)
{
SAFE_DELETE(xml_reader);
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int CMapReader::LoadRMSettings()
{
// copy random map settings over to sim
ENSURE(pSimulation2);
pSimulation2->SetMapSettings(m_ScriptSettings);
return 0;
}
int CMapReader::GenerateMap()
{
ScriptRequest rq(pSimulation2->GetScriptInterface());
if (!m_MapGen)
{
// Initialize map generator
m_MapGen = new CMapGenerator();
VfsPath scriptPath;
if (m_ScriptFile.length())
scriptPath = L"maps/random/"+m_ScriptFile;
// Stringify settings to pass across threads
std::string scriptSettings = Script::StringifyJSON(rq, &m_ScriptSettings);
// Try to generate map
m_MapGen->GenerateMap(scriptPath, scriptSettings);
}
// Check status
int progress = m_MapGen->GetProgress();
if (progress < 0)
{
// RMS failed - return to main menu
throw PSERROR_Game_World_MapLoadFailed("Error generating random map.\nCheck application log for details.");
}
else if (progress == 0)
{
// Finished, get results as StructuredClone object, which must be read to obtain the JS::Value
Script::StructuredClone results = m_MapGen->GetResults();
// Parse data into simulation context
JS::RootedValue data(rq.cx);
Script::ReadStructuredClone(rq, results, &data);
if (data.isUndefined())
{
// RMS failed - return to main menu
throw PSERROR_Game_World_MapLoadFailed("Error generating random map.\nCheck application log for details.");
}
else
{
m_MapData.init(rq.cx, data);
}
}
else
{
// Still working
// Sleep for a while, slowing down the rendering thread
// to allow more CPU for the map generator thread
SDL_Delay(100);
}
// return progress
return progress;
};
int CMapReader::ParseTerrain()
{
TIMER(L"ParseTerrain");
ScriptRequest rq(pSimulation2->GetScriptInterface());
// parse terrain from map data
// an error here should stop the loading process
#define GET_TERRAIN_PROPERTY(val, prop, out)\
if (!Script::GetProperty(rq, val, #prop, out))\
{ LOGERROR("CMapReader::ParseTerrain() failed to get '%s' property", #prop);\
throw PSERROR_Game_World_MapLoadFailed("Error parsing terrain data.\nCheck application log for details"); }
u32 size;
GET_TERRAIN_PROPERTY(m_MapData, size, size)
m_PatchesPerSide = size / PATCH_SIZE;
// flat heightmap of u16 data
GET_TERRAIN_PROPERTY(m_MapData, height, m_Heightmap)
// load textures
std::vector textureNames;
GET_TERRAIN_PROPERTY(m_MapData, textureNames, textureNames)
num_terrain_tex = textureNames.size();
while (cur_terrain_tex < num_terrain_tex)
{
- ENSURE(CTerrainTextureManager::IsInitialised()); // we need this for the terrain properties (even when graphics are disabled)
- CTerrainTextureEntry* texentry = g_TexMan.FindTexture(textureNames[cur_terrain_tex]);
- m_TerrainTextures.push_back(texentry);
+ if (CTerrainTextureManager::IsInitialised())
+ {
+ CTerrainTextureEntry* texentry = g_TexMan.FindTexture(textureNames[cur_terrain_tex]);
+ m_TerrainTextures.push_back(texentry);
+ }
cur_terrain_tex++;
}
// build tile data
m_Tiles.resize(SQR(size));
JS::RootedValue tileData(rq.cx);
GET_TERRAIN_PROPERTY(m_MapData, tileData, &tileData)
// parse tile data object into flat arrays
std::vector tileIndex;
std::vector tilePriority;
GET_TERRAIN_PROPERTY(tileData, index, tileIndex);
GET_TERRAIN_PROPERTY(tileData, priority, tilePriority);
ENSURE(SQR(size) == tileIndex.size() && SQR(size) == tilePriority.size());
// reorder by patches and store
for (size_t x = 0; x < size; ++x)
{
size_t patchX = x / PATCH_SIZE;
size_t offX = x % PATCH_SIZE;
for (size_t y = 0; y < size; ++y)
{
size_t patchY = y / PATCH_SIZE;
size_t offY = y % PATCH_SIZE;
STileDesc tile;
tile.m_Tex1Index = tileIndex[y*size + x];
tile.m_Tex2Index = 0xFFFF;
tile.m_Priority = tilePriority[y*size + x];
m_Tiles[(patchY * m_PatchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)] = tile;
}
}
// reset generator state
cur_terrain_tex = 0;
#undef GET_TERRAIN_PROPERTY
return 0;
}
int CMapReader::ParseEntities()
{
TIMER(L"ParseEntities");
ScriptRequest rq(pSimulation2->GetScriptInterface());
// parse entities from map data
std::vector entities;
if (!Script::GetProperty(rq, m_MapData, "entities", entities))
LOGWARNING("CMapReader::ParseEntities() failed to get 'entities' property");
CSimulation2& sim = *pSimulation2;
CmpPtr cmpPlayerManager(sim, SYSTEM_ENTITY);
size_t entity_idx = 0;
size_t num_entities = entities.size();
Entity currEnt;
while (entity_idx < num_entities)
{
// Get current entity struct
currEnt = entities[entity_idx];
entity_id_t ent = pSimulation2->AddEntity(currEnt.templateName, currEnt.entityID);
entity_id_t player = cmpPlayerManager->GetPlayerByID(currEnt.playerID);
if (ent == INVALID_ENTITY || player == INVALID_ENTITY)
{ // Don't add entities with invalid player IDs
LOGERROR("Failed to load entity template '%s'", utf8_from_wstring(currEnt.templateName));
}
else
{
CmpPtr cmpPosition(sim, ent);
if (cmpPosition)
{
cmpPosition->JumpTo(currEnt.position.X * (int)TERRAIN_TILE_SIZE, currEnt.position.Z * (int)TERRAIN_TILE_SIZE);
cmpPosition->SetYRotation(currEnt.rotation.Y);
// TODO: other parts of the position
}
CmpPtr cmpOwnership(sim, ent);
if (cmpOwnership)
cmpOwnership->SetOwner(currEnt.playerID);
// Detect and fix collisions between foundation-blocking entities.
// This presently serves to copy wall tower control groups to wall
// segments, allowing players to expand RMS-generated walls.
CmpPtr cmpObstruction(sim, ent);
if (cmpObstruction)
cmpObstruction->ResolveFoundationCollisions();
if (currEnt.playerID == m_PlayerID && (boost::algorithm::ends_with(currEnt.templateName, L"civil_centre") || m_StartingCameraTarget == INVALID_ENTITY))
{
// Focus on civil centre or first entity owned by player
m_StartingCameraTarget = currEnt.entityID;
}
}
entity_idx++;
}
return 0;
}
int CMapReader::ParseEnvironment()
{
// parse environment settings from map data
ScriptRequest rq(pSimulation2->GetScriptInterface());
#define GET_ENVIRONMENT_PROPERTY(val, prop, out)\
if (!Script::GetProperty(rq, val, #prop, out))\
LOGWARNING("CMapReader::ParseEnvironment() failed to get '%s' property", #prop);
JS::RootedValue envObj(rq.cx);
GET_ENVIRONMENT_PROPERTY(m_MapData, Environment, &envObj)
if (envObj.isUndefined())
{
LOGWARNING("CMapReader::ParseEnvironment(): Environment settings not found");
return 0;
}
if (pPostproc)
pPostproc->SetPostEffect(L"default");
std::wstring skySet;
GET_ENVIRONMENT_PROPERTY(envObj, SkySet, skySet)
if (pSkyMan)
pSkyMan->SetSkySet(skySet);
CColor sunColor;
GET_ENVIRONMENT_PROPERTY(envObj, SunColor, sunColor)
m_LightEnv.m_SunColor = RGBColor(sunColor.r, sunColor.g, sunColor.b);
GET_ENVIRONMENT_PROPERTY(envObj, SunElevation, m_LightEnv.m_Elevation)
GET_ENVIRONMENT_PROPERTY(envObj, SunRotation, m_LightEnv.m_Rotation)
CColor ambientColor;
GET_ENVIRONMENT_PROPERTY(envObj, AmbientColor, ambientColor)
m_LightEnv.m_AmbientColor = RGBColor(ambientColor.r, ambientColor.g, ambientColor.b);
// Water properties
JS::RootedValue waterObj(rq.cx);
GET_ENVIRONMENT_PROPERTY(envObj, Water, &waterObj)
JS::RootedValue waterBodyObj(rq.cx);
GET_ENVIRONMENT_PROPERTY(waterObj, WaterBody, &waterBodyObj)
// Water level - necessary
float waterHeight;
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Height, waterHeight)
CmpPtr cmpWaterManager(*pSimulation2, SYSTEM_ENTITY);
ENSURE(cmpWaterManager);
cmpWaterManager->SetWaterLevel(entity_pos_t::FromFloat(waterHeight));
// If we have graphics, get rest of settings
if (pWaterMan)
{
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Type, pWaterMan->m_WaterType)
if (pWaterMan->m_WaterType == L"default")
pWaterMan->m_WaterType = L"ocean";
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Color, pWaterMan->m_WaterColor)
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Tint, pWaterMan->m_WaterTint)
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Waviness, pWaterMan->m_Waviness)
GET_ENVIRONMENT_PROPERTY(waterBodyObj, Murkiness, pWaterMan->m_Murkiness)
GET_ENVIRONMENT_PROPERTY(waterBodyObj, WindAngle, pWaterMan->m_WindAngle)
}
JS::RootedValue fogObject(rq.cx);
GET_ENVIRONMENT_PROPERTY(envObj, Fog, &fogObject);
GET_ENVIRONMENT_PROPERTY(fogObject, FogFactor, m_LightEnv.m_FogFactor);
GET_ENVIRONMENT_PROPERTY(fogObject, FogThickness, m_LightEnv.m_FogMax);
CColor fogColor;
GET_ENVIRONMENT_PROPERTY(fogObject, FogColor, fogColor);
m_LightEnv.m_FogColor = RGBColor(fogColor.r, fogColor.g, fogColor.b);
JS::RootedValue postprocObject(rq.cx);
GET_ENVIRONMENT_PROPERTY(envObj, Postproc, &postprocObject);
std::wstring postProcEffect;
GET_ENVIRONMENT_PROPERTY(postprocObject, PostprocEffect, postProcEffect);
if (pPostproc)
pPostproc->SetPostEffect(postProcEffect);
GET_ENVIRONMENT_PROPERTY(postprocObject, Brightness, m_LightEnv.m_Brightness);
GET_ENVIRONMENT_PROPERTY(postprocObject, Contrast, m_LightEnv.m_Contrast);
GET_ENVIRONMENT_PROPERTY(postprocObject, Saturation, m_LightEnv.m_Saturation);
GET_ENVIRONMENT_PROPERTY(postprocObject, Bloom, m_LightEnv.m_Bloom);
m_LightEnv.CalculateSunDirection();
#undef GET_ENVIRONMENT_PROPERTY
return 0;
}
int CMapReader::ParseCamera()
{
ScriptRequest rq(pSimulation2->GetScriptInterface());
// parse camera settings from map data
// defaults if we don't find player starting camera
float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f);
CVector3D translation = CVector3D(100, 150, -100);
#define GET_CAMERA_PROPERTY(val, prop, out)\
if (!Script::GetProperty(rq, val, #prop, out))\
LOGWARNING("CMapReader::ParseCamera() failed to get '%s' property", #prop);
JS::RootedValue cameraObj(rq.cx);
GET_CAMERA_PROPERTY(m_MapData, Camera, &cameraObj)
if (!cameraObj.isUndefined())
{ // If camera property exists, read values
CFixedVector3D pos;
GET_CAMERA_PROPERTY(cameraObj, Position, pos)
translation = pos;
GET_CAMERA_PROPERTY(cameraObj, Rotation, rotation)
GET_CAMERA_PROPERTY(cameraObj, Declination, declination)
}
#undef GET_CAMERA_PROPERTY
if (pGameView)
{
pGameView->GetCamera()->m_Orientation.SetXRotation(declination);
pGameView->GetCamera()->m_Orientation.RotateY(rotation);
pGameView->GetCamera()->m_Orientation.Translate(translation);
pGameView->GetCamera()->UpdateFrustum();
}
return 0;
}
CMapReader::~CMapReader()
{
// Cleaup objects
delete xml_reader;
delete m_MapGen;
}
Index: ps/trunk/source/graphics/Terrain.cpp
===================================================================
--- ps/trunk/source/graphics/Terrain.cpp (revision 26268)
+++ ps/trunk/source/graphics/Terrain.cpp (revision 26269)
@@ -1,842 +1,831 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "graphics/Terrain.h"
#include "graphics/Patch.h"
#include "graphics/TerrainProperties.h"
#include "graphics/TerrainTextureEntry.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/sysdep/cpu.h"
#include "maths/FixedVector3D.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "renderer/Renderer.h"
#include "simulation2/helpers/Pathfinding.h"
#include
///////////////////////////////////////////////////////////////////////////////
// CTerrain constructor
CTerrain::CTerrain()
: m_Heightmap(0), m_Patches(0), m_MapSize(0), m_MapSizePatches(0),
m_BaseColor(255, 255, 255, 255)
{
}
///////////////////////////////////////////////////////////////////////////////
// CTerrain constructor
CTerrain::~CTerrain()
{
ReleaseData();
}
///////////////////////////////////////////////////////////////////////////////
// ReleaseData: delete any data allocated by this terrain
void CTerrain::ReleaseData()
{
m_HeightMipmap.ReleaseData();
delete[] m_Heightmap;
delete[] m_Patches;
}
///////////////////////////////////////////////////////////////////////////////
// Initialise: initialise this terrain to the given size
// using given heightmap to setup elevation data
bool CTerrain::Initialize(ssize_t patchesPerSide, const u16* data)
{
// clean up any previous terrain
ReleaseData();
// store terrain size
m_MapSize = patchesPerSide * PATCH_SIZE + 1;
m_MapSizePatches = patchesPerSide;
// allocate data for new terrain
m_Heightmap = new u16[m_MapSize * m_MapSize];
m_Patches = new CPatch[m_MapSizePatches * m_MapSizePatches];
// given a heightmap?
if (data)
{
// yes; keep a copy of it
memcpy(m_Heightmap, data, m_MapSize*m_MapSize*sizeof(u16));
}
else
{
// build a flat terrain
memset(m_Heightmap, 0, m_MapSize*m_MapSize*sizeof(u16));
}
// setup patch parents, indices etc
InitialisePatches();
// initialise mipmap
m_HeightMipmap.Initialize(m_MapSize, m_Heightmap);
return true;
}
///////////////////////////////////////////////////////////////////////////////
-
-CStr8 CTerrain::GetMovementClass(ssize_t i, ssize_t j) const
-{
- CMiniPatch* tile = GetTile(i, j);
- if (tile && tile->GetTextureEntry())
- return tile->GetTextureEntry()->GetProperties().GetMovementClass();
-
- return "default";
-}
-
-///////////////////////////////////////////////////////////////////////////////
// CalcPosition: calculate the world space position of the vertex at (i,j)
// If i,j is off the map, it acts as if the edges of the terrain are extended
// outwards to infinity
void CTerrain::CalcPosition(ssize_t i, ssize_t j, CVector3D& pos) const
{
ssize_t hi = Clamp(i, 0, m_MapSize - 1);
ssize_t hj = Clamp(j, 0, m_MapSize - 1);
u16 height = m_Heightmap[hj*m_MapSize + hi];
pos.X = float(i*TERRAIN_TILE_SIZE);
pos.Y = float(height*HEIGHT_SCALE);
pos.Z = float(j*TERRAIN_TILE_SIZE);
}
///////////////////////////////////////////////////////////////////////////////
// CalcPositionFixed: calculate the world space position of the vertex at (i,j)
void CTerrain::CalcPositionFixed(ssize_t i, ssize_t j, CFixedVector3D& pos) const
{
ssize_t hi = Clamp(i, 0, m_MapSize - 1);
ssize_t hj = Clamp(j, 0, m_MapSize - 1);
u16 height = m_Heightmap[hj*m_MapSize + hi];
pos.X = fixed::FromInt(i) * (int)TERRAIN_TILE_SIZE;
// fixed max value is 32767, but height is a u16, so divide by two to avoid overflow
pos.Y = fixed::FromInt(height/ 2 ) / ((int)HEIGHT_UNITS_PER_METRE / 2);
pos.Z = fixed::FromInt(j) * (int)TERRAIN_TILE_SIZE;
}
///////////////////////////////////////////////////////////////////////////////
// CalcNormal: calculate the world space normal of the vertex at (i,j)
void CTerrain::CalcNormal(ssize_t i, ssize_t j, CVector3D& normal) const
{
CVector3D left, right, up, down;
// Calculate normals of the four half-tile triangles surrounding this vertex:
// get position of vertex where normal is being evaluated
CVector3D basepos;
CalcPosition(i, j, basepos);
if (i > 0) {
CalcPosition(i-1, j, left);
left -= basepos;
left.Normalize();
}
if (i < m_MapSize-1) {
CalcPosition(i+1, j, right);
right -= basepos;
right.Normalize();
}
if (j > 0) {
CalcPosition(i, j-1, up);
up -= basepos;
up.Normalize();
}
if (j < m_MapSize-1) {
CalcPosition(i, j+1, down);
down -= basepos;
down.Normalize();
}
CVector3D n0 = up.Cross(left);
CVector3D n1 = left.Cross(down);
CVector3D n2 = down.Cross(right);
CVector3D n3 = right.Cross(up);
// Compute the mean of the normals
normal = n0 + n1 + n2 + n3;
float nlen=normal.Length();
if (nlen>0.00001f) normal*=1.0f/nlen;
}
///////////////////////////////////////////////////////////////////////////////
// CalcNormalFixed: calculate the world space normal of the vertex at (i,j)
void CTerrain::CalcNormalFixed(ssize_t i, ssize_t j, CFixedVector3D& normal) const
{
CFixedVector3D left, right, up, down;
// Calculate normals of the four half-tile triangles surrounding this vertex:
// get position of vertex where normal is being evaluated
CFixedVector3D basepos;
CalcPositionFixed(i, j, basepos);
if (i > 0) {
CalcPositionFixed(i-1, j, left);
left -= basepos;
left.Normalize();
}
if (i < m_MapSize-1) {
CalcPositionFixed(i+1, j, right);
right -= basepos;
right.Normalize();
}
if (j > 0) {
CalcPositionFixed(i, j-1, up);
up -= basepos;
up.Normalize();
}
if (j < m_MapSize-1) {
CalcPositionFixed(i, j+1, down);
down -= basepos;
down.Normalize();
}
CFixedVector3D n0 = up.Cross(left);
CFixedVector3D n1 = left.Cross(down);
CFixedVector3D n2 = down.Cross(right);
CFixedVector3D n3 = right.Cross(up);
// Compute the mean of the normals
normal = n0 + n1 + n2 + n3;
normal.Normalize();
}
CVector3D CTerrain::CalcExactNormal(float x, float z) const
{
// Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1)
const ssize_t xi = Clamp(floor(x / TERRAIN_TILE_SIZE), 0, m_MapSize - 2);
const ssize_t zi = Clamp(floor(z / TERRAIN_TILE_SIZE), 0, m_MapSize - 2);
const float xf = Clamp(x / TERRAIN_TILE_SIZE-xi, 0.0f, 1.0f);
const float zf = Clamp(z / TERRAIN_TILE_SIZE-zi, 0.0f, 1.0f);
float h00 = m_Heightmap[zi*m_MapSize + xi];
float h01 = m_Heightmap[(zi+1)*m_MapSize + xi];
float h10 = m_Heightmap[zi*m_MapSize + (xi+1)];
float h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)];
// Determine which terrain triangle this point is on,
// then compute the normal of that triangle's plane
if (GetTriangulationDir(xi, zi))
{
if (xf + zf <= 1.f)
{
// Lower-left triangle (don't use h11)
return -CVector3D(TERRAIN_TILE_SIZE, (h10-h00)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h01-h00)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized();
}
else
{
// Upper-right triangle (don't use h00)
return -CVector3D(TERRAIN_TILE_SIZE, (h11-h01)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h11-h10)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized();
}
}
else
{
if (xf <= zf)
{
// Upper-left triangle (don't use h10)
return -CVector3D(TERRAIN_TILE_SIZE, (h11-h01)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h01-h00)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized();
}
else
{
// Lower-right triangle (don't use h01)
return -CVector3D(TERRAIN_TILE_SIZE, (h10-h00)*HEIGHT_SCALE, 0).Cross(CVector3D(0, (h11-h10)*HEIGHT_SCALE, TERRAIN_TILE_SIZE)).Normalized();
}
}
}
///////////////////////////////////////////////////////////////////////////////
// GetPatch: return the patch at (i,j) in patch space, or null if the patch is
// out of bounds
CPatch* CTerrain::GetPatch(ssize_t i, ssize_t j) const
{
// range check (invalid indices are passed in by the culling and
// patch blend code because they iterate from 0..#patches and examine
// neighbors without checking if they're already on the edge)
if( (size_t)i >= (size_t)m_MapSizePatches || (size_t)j >= (size_t)m_MapSizePatches )
return 0;
return &m_Patches[(j*m_MapSizePatches)+i];
}
///////////////////////////////////////////////////////////////////////////////
// GetTile: return the tile at (i,j) in tile space, or null if the tile is out
// of bounds
CMiniPatch* CTerrain::GetTile(ssize_t i, ssize_t j) const
{
// see comment above
if( (size_t)i >= (size_t)(m_MapSize-1) || (size_t)j >= (size_t)(m_MapSize-1) )
return 0;
CPatch* patch=GetPatch(i/PATCH_SIZE, j/PATCH_SIZE); // can't fail (due to above check)
return &patch->m_MiniPatches[j%PATCH_SIZE][i%PATCH_SIZE];
}
float CTerrain::GetVertexGroundLevel(ssize_t i, ssize_t j) const
{
i = Clamp(i, 0, m_MapSize - 1);
j = Clamp(j, 0, m_MapSize - 1);
return HEIGHT_SCALE * m_Heightmap[j*m_MapSize + i];
}
fixed CTerrain::GetVertexGroundLevelFixed(ssize_t i, ssize_t j) const
{
i = Clamp(i, 0, m_MapSize - 1);
j = Clamp(j, 0, m_MapSize - 1);
// Convert to fixed metres (being careful to avoid intermediate overflows)
return fixed::FromInt(m_Heightmap[j*m_MapSize + i] / 2) / (int)(HEIGHT_UNITS_PER_METRE / 2);
}
fixed CTerrain::GetSlopeFixed(ssize_t i, ssize_t j) const
{
// Clamp to size-2 so we can use the tiles (i,j)-(i+1,j+1)
i = Clamp(i, 0, m_MapSize - 2);
j = Clamp(j, 0, m_MapSize - 2);
u16 h00 = m_Heightmap[j*m_MapSize + i];
u16 h01 = m_Heightmap[(j+1)*m_MapSize + i];
u16 h10 = m_Heightmap[j*m_MapSize + (i+1)];
u16 h11 = m_Heightmap[(j+1)*m_MapSize + (i+1)];
// Difference of highest point from lowest point
u16 delta = std::max(std::max(h00, h01), std::max(h10, h11)) -
std::min(std::min(h00, h01), std::min(h10, h11));
// Compute fractional slope (being careful to avoid intermediate overflows)
return fixed::FromInt(delta / TERRAIN_TILE_SIZE) / (int)HEIGHT_UNITS_PER_METRE;
}
fixed CTerrain::GetExactSlopeFixed(fixed x, fixed z) const
{
// Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1)
const ssize_t xi = Clamp((x / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2);
const ssize_t zi = Clamp((z / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2);
const fixed one = fixed::FromInt(1);
const fixed xf = Clamp((x / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(xi), fixed::Zero(), one);
const fixed zf = Clamp((z / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(zi), fixed::Zero(), one);
u16 h00 = m_Heightmap[zi*m_MapSize + xi];
u16 h01 = m_Heightmap[(zi+1)*m_MapSize + xi];
u16 h10 = m_Heightmap[zi*m_MapSize + (xi+1)];
u16 h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)];
u16 delta;
if (GetTriangulationDir(xi, zi))
{
if (xf + zf <= one)
{
// Lower-left triangle (don't use h11)
delta = std::max(std::max(h00, h01), h10) -
std::min(std::min(h00, h01), h10);
}
else
{
// Upper-right triangle (don't use h00)
delta = std::max(std::max(h01, h10), h11) -
std::min(std::min(h01, h10), h11);
}
}
else
{
if (xf <= zf)
{
// Upper-left triangle (don't use h10)
delta = std::max(std::max(h00, h01), h11) -
std::min(std::min(h00, h01), h11);
}
else
{
// Lower-right triangle (don't use h01)
delta = std::max(std::max(h00, h10), h11) -
std::min(std::min(h00, h10), h11);
}
}
// Compute fractional slope (being careful to avoid intermediate overflows)
return fixed::FromInt(delta / TERRAIN_TILE_SIZE) / (int)HEIGHT_UNITS_PER_METRE;
}
float CTerrain::GetFilteredGroundLevel(float x, float z, float radius) const
{
// convert to [0,1] interval
float nx = x / (TERRAIN_TILE_SIZE*m_MapSize);
float nz = z / (TERRAIN_TILE_SIZE*m_MapSize);
float nr = radius / (TERRAIN_TILE_SIZE*m_MapSize);
// get trilinear filtered mipmap height
return HEIGHT_SCALE * m_HeightMipmap.GetTrilinearGroundLevel(nx, nz, nr);
}
float CTerrain::GetExactGroundLevel(float x, float z) const
{
// Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1)
const ssize_t xi = Clamp(floor(x / TERRAIN_TILE_SIZE), 0, m_MapSize - 2);
const ssize_t zi = Clamp(floor(z / TERRAIN_TILE_SIZE), 0, m_MapSize - 2);
const float xf = Clamp(x / TERRAIN_TILE_SIZE - xi, 0.0f, 1.0f);
const float zf = Clamp(z / TERRAIN_TILE_SIZE - zi, 0.0f, 1.0f);
float h00 = m_Heightmap[zi*m_MapSize + xi];
float h01 = m_Heightmap[(zi+1)*m_MapSize + xi];
float h10 = m_Heightmap[zi*m_MapSize + (xi+1)];
float h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)];
// Determine which terrain triangle this point is on,
// then compute the linearly-interpolated height on that triangle's plane
if (GetTriangulationDir(xi, zi))
{
if (xf + zf <= 1.f)
{
// Lower-left triangle (don't use h11)
return HEIGHT_SCALE * (h00 + (h10-h00)*xf + (h01-h00)*zf);
}
else
{
// Upper-right triangle (don't use h00)
return HEIGHT_SCALE * (h11 + (h01-h11)*(1-xf) + (h10-h11)*(1-zf));
}
}
else
{
if (xf <= zf)
{
// Upper-left triangle (don't use h10)
return HEIGHT_SCALE * (h00 + (h11-h01)*xf + (h01-h00)*zf);
}
else
{
// Lower-right triangle (don't use h01)
return HEIGHT_SCALE * (h00 + (h10-h00)*xf + (h11-h10)*zf);
}
}
}
fixed CTerrain::GetExactGroundLevelFixed(fixed x, fixed z) const
{
// Clamp to size-2 so we can use the tiles (xi,zi)-(xi+1,zi+1)
const ssize_t xi = Clamp((x / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2);
const ssize_t zi = Clamp((z / static_cast(TERRAIN_TILE_SIZE)).ToInt_RoundToZero(), 0, m_MapSize - 2);
const fixed one = fixed::FromInt(1);
const fixed xf = Clamp((x / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(xi), fixed::Zero(), one);
const fixed zf = Clamp((z / static_cast(TERRAIN_TILE_SIZE)) - fixed::FromInt(zi), fixed::Zero(), one);
u16 h00 = m_Heightmap[zi*m_MapSize + xi];
u16 h01 = m_Heightmap[(zi+1)*m_MapSize + xi];
u16 h10 = m_Heightmap[zi*m_MapSize + (xi+1)];
u16 h11 = m_Heightmap[(zi+1)*m_MapSize + (xi+1)];
// Intermediate scaling of xf, so we don't overflow in the multiplications below
// (h00 <= 65535, xf <= 1, max fixed is < 32768; divide by 2 here so xf1*h00 <= 32767.5)
const fixed xf0 = xf / 2;
const fixed xf1 = (one - xf) / 2;
// Linearly interpolate
return ((one - zf).Multiply(xf1 * h00 + xf0 * h10)
+ zf.Multiply(xf1 * h01 + xf0 * h11)) / (int)(HEIGHT_UNITS_PER_METRE / 2);
// TODO: This should probably be more like GetExactGroundLevel()
// in handling triangulation properly
}
bool CTerrain::GetTriangulationDir(ssize_t i, ssize_t j) const
{
// Clamp to size-2 so we can use the tiles (i,j)-(i+1,j+1)
i = Clamp(i, 0, m_MapSize - 2);
j = Clamp(j, 0, m_MapSize - 2);
int h00 = m_Heightmap[j*m_MapSize + i];
int h01 = m_Heightmap[(j+1)*m_MapSize + i];
int h10 = m_Heightmap[j*m_MapSize + (i+1)];
int h11 = m_Heightmap[(j+1)*m_MapSize + (i+1)];
// Prefer triangulating in whichever direction means the midpoint of the diagonal
// will be the highest. (In particular this means a diagonal edge will be straight
// along the top, and jagged along the bottom, which makes sense for terrain.)
int mid1 = h00+h11;
int mid2 = h01+h10;
return (mid1 < mid2);
}
void CTerrain::ResizeAndOffset(ssize_t size, ssize_t horizontalOffset, ssize_t verticalOffset)
{
if (size == m_MapSizePatches && horizontalOffset == 0 && verticalOffset == 0)
{
// Inexplicable request to resize terrain to the same size, ignore it.
return;
}
if (!m_Heightmap ||
std::abs(horizontalOffset) >= size / 2 + m_MapSizePatches / 2 ||
std::abs(verticalOffset) >= size / 2 + m_MapSizePatches / 2)
{
// We have not yet created a terrain, or we are offsetting outside the current source.
// Let's build a default terrain of the given size now.
Initialize(size, 0);
return;
}
// Allocate data for new terrain.
const ssize_t newMapSize = size * PATCH_SIZE + 1;
u16* newHeightmap = new u16[newMapSize * newMapSize];
memset(newHeightmap, 0, newMapSize * newMapSize * sizeof(u16));
CPatch* newPatches = new CPatch[size * size];
// O--------------------+
// | Source |
// | |
// | Source Center (SC) |
// | X |
// | A------+----------------+
// | | | Destination |
// | | | |
// +-------------+------B |
// | Dest. Center (DC) |
// | X |
// | |
// | |
// | |
// | |
// +-----------------------+
//
// Calculations below should also account cases like:
//
// +----------+ +----------+ +----------+ +---+--+---+ +------+
// |S | |D | |S | |S | | D| |D |
// | +---+ | | +---+ | +-+-+ | | | | | | +---+--+
// | | D | | | | S | | |D| | | +---+--+---+ +--+---+ |
// | +---+ | | +---+ | +-+-+ | | S|
// +----------+ +----------+ +----------+ +------+
//
// O = (0, 0)
// SC = (m_MapSizePatches / 2, m_MapSizePatches / 2)
// DC - SC = (horizontalOffset, verticalOffset)
//
// Source upper left:
// A = (max(0, (m_MapSizePatches - size) / 2 + horizontalOffset),
// max(0, (m_MapSizePatches - size) / 2 + verticalOffset))
// Source bottom right:
// B = (min(m_MapSizePatches, (m_MapSizePatches + size) / 2 + horizontalOffset),
// min(m_MapSizePatches, (m_MapSizePatches + size) / 2 + verticalOffset))
//
// A-B is the area that we have to copy from the source to the destination.
// Restate center offset as a window over destination.
// This has the effect of always considering the source to be the same size or smaller.
const ssize_t sourceUpperLeftX = std::max(
static_cast(0), m_MapSizePatches / 2 - size / 2 + horizontalOffset);
const ssize_t sourceUpperLeftZ = std::max(
static_cast(0), m_MapSizePatches / 2 - size / 2 + verticalOffset);
const ssize_t destUpperLeftX = std::max(
static_cast(0), (size / 2 - m_MapSizePatches / 2 - horizontalOffset));
const ssize_t destUpperLeftZ = std::max(
static_cast(0), (size / 2 - m_MapSizePatches / 2 - verticalOffset));
const ssize_t width =
std::min(m_MapSizePatches, m_MapSizePatches / 2 + horizontalOffset + size / 2) - sourceUpperLeftX;
const ssize_t depth =
std::min(m_MapSizePatches, m_MapSizePatches / 2 + verticalOffset + size / 2) - sourceUpperLeftZ;
for (ssize_t j = 0; j < depth * PATCH_SIZE; ++j)
{
// Copy the main part from the source. Destination heightmap:
// +----------+
// | |
// | 1234 | < current j-th row for example.
// | 5678 |
// | |
// +----------+
u16* dst = newHeightmap + (j + destUpperLeftZ * PATCH_SIZE) * newMapSize + destUpperLeftX * PATCH_SIZE;
u16* src = m_Heightmap + (j + sourceUpperLeftZ * PATCH_SIZE) * m_MapSize + sourceUpperLeftX * PATCH_SIZE;
std::copy_n(src, width * PATCH_SIZE, dst);
if (destUpperLeftX > 0)
{
// Fill the preceding part by copying the first elements of the
// main part. Destination heightmap:
// +----------+
// | |
// |1111234 | < current j-th row for example.
// | 5678 |
// | |
// +----------+
u16* dst_prefix = newHeightmap + (j + destUpperLeftZ * PATCH_SIZE) * newMapSize;
std::fill_n(dst_prefix, destUpperLeftX * PATCH_SIZE, dst[0]);
}
if ((destUpperLeftX + width) * PATCH_SIZE < newMapSize)
{
// Fill the succeeding part by copying the last elements of the
// main part. Destination heightmap:
// +----------+
// | |
// |1111234444| < current j-th row for example.
// | 5678 |
// | |
// +----------+
u16* dst_suffix = dst + width * PATCH_SIZE;
std::fill_n(
dst_suffix,
newMapSize - (width + destUpperLeftX) * PATCH_SIZE,
dst[width * PATCH_SIZE - 1]);
}
}
// Copy over heights from the preceding row. Destination heightmap:
// +----------+
// |1111234444| < copied from the row below
// |1111234444|
// |5555678888|
// | |
// +----------+
for (ssize_t j = 0; j < destUpperLeftZ * PATCH_SIZE; ++j)
{
u16* dst = newHeightmap + j * newMapSize;
u16* src = newHeightmap + destUpperLeftZ * PATCH_SIZE * newMapSize;
std::copy_n(src, newMapSize, dst);
}
// Copy over heights from the succeeding row. Destination heightmap:
// +----------+
// |1111234444|
// |1111234444|
// |5555678888|
// |5555678888| < copied from the row above
// +----------+
for (ssize_t j = (destUpperLeftZ + depth) * PATCH_SIZE; j < newMapSize; ++j)
{
u16* dst = newHeightmap + j * newMapSize;
u16* src = newHeightmap + ((destUpperLeftZ + depth) * PATCH_SIZE - 1) * newMapSize;
std::copy_n(src, newMapSize, dst);
}
// Now build new patches. The same process as for the heightmap.
for (ssize_t j = 0; j < depth; ++j)
{
for (ssize_t i = 0; i < width; ++i)
{
const CPatch& src =
m_Patches[(sourceUpperLeftZ + j) * m_MapSizePatches + sourceUpperLeftX + i];
CPatch& dst =
newPatches[(destUpperLeftZ + j) * size + destUpperLeftX + i];
std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]);
}
for (ssize_t i = 0; i < destUpperLeftX; ++i)
for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch)
{
const CMiniPatch& src =
newPatches[(destUpperLeftZ + j) * size + destUpperLeftX]
.m_MiniPatches[jPatch][0];
for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch)
{
CMiniPatch& dst =
newPatches[(destUpperLeftZ + j) * size + i]
.m_MiniPatches[jPatch][iPatch];
dst = src;
}
}
for (ssize_t i = destUpperLeftX + width; i < size; ++i)
{
for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch)
{
const CMiniPatch& src =
newPatches[(destUpperLeftZ + j) * size + destUpperLeftX + width - 1]
.m_MiniPatches[jPatch][PATCH_SIZE - 1];
for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch)
{
CMiniPatch& dst =
newPatches[(destUpperLeftZ + j) * size + i].m_MiniPatches[jPatch][iPatch];
dst = src;
}
}
}
}
for (ssize_t j = 0; j < destUpperLeftZ; ++j)
for (ssize_t i = 0; i < size; ++i)
for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch)
{
const CMiniPatch& src =
newPatches[destUpperLeftZ * size + i].m_MiniPatches[0][iPatch];
for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch)
{
CMiniPatch& dst =
newPatches[j * size + i].m_MiniPatches[jPatch][iPatch];
dst = src;
}
}
for (ssize_t j = destUpperLeftZ + depth; j < size; ++j)
for (ssize_t i = 0; i < size; ++i)
for (ssize_t iPatch = 0; iPatch < PATCH_SIZE; ++iPatch)
{
const CMiniPatch& src =
newPatches[(destUpperLeftZ + depth - 1) * size + i].m_MiniPatches[0][iPatch];
for (ssize_t jPatch = 0; jPatch < PATCH_SIZE; ++jPatch)
{
CMiniPatch& dst =
newPatches[j * size + i].m_MiniPatches[jPatch][iPatch];
dst = src;
}
}
// Release all the original data.
ReleaseData();
// Store new data.
m_Heightmap = newHeightmap;
m_Patches = newPatches;
m_MapSize = newMapSize;
m_MapSizePatches = size;
// Initialise all the new patches.
InitialisePatches();
// Initialise mipmap.
m_HeightMipmap.Initialize(m_MapSize, m_Heightmap);
}
///////////////////////////////////////////////////////////////////////////////
// InitialisePatches: initialise patch data
void CTerrain::InitialisePatches()
{
for (ssize_t j = 0; j < m_MapSizePatches; j++)
{
for (ssize_t i = 0; i < m_MapSizePatches; i++)
{
CPatch* patch = GetPatch(i, j); // can't fail
patch->Initialize(this, i, j);
}
}
}
///////////////////////////////////////////////////////////////////////////////
// SetHeightMap: set up a new heightmap from 16-bit source data;
// assumes heightmap matches current terrain size
void CTerrain::SetHeightMap(u16* heightmap)
{
// keep a copy of the given heightmap
memcpy(m_Heightmap, heightmap, m_MapSize*m_MapSize*sizeof(u16));
// recalculate patch bounds, invalidate vertices
for (ssize_t j = 0; j < m_MapSizePatches; j++)
{
for (ssize_t i = 0; i < m_MapSizePatches; i++)
{
CPatch* patch = GetPatch(i, j); // can't fail
patch->InvalidateBounds();
patch->SetDirty(RENDERDATA_UPDATE_VERTICES);
}
}
// update mipmap
m_HeightMipmap.Update(m_Heightmap);
}
///////////////////////////////////////////////////////////////////////////////
void CTerrain::MakeDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1, int dirtyFlags)
{
// Finds the inclusive limits of the patches that include the specified range of tiles
ssize_t pi0 = Clamp( i0 /PATCH_SIZE, 0, m_MapSizePatches-1);
ssize_t pi1 = Clamp((i1-1)/PATCH_SIZE, 0, m_MapSizePatches-1);
ssize_t pj0 = Clamp( j0 /PATCH_SIZE, 0, m_MapSizePatches-1);
ssize_t pj1 = Clamp((j1-1)/PATCH_SIZE, 0, m_MapSizePatches-1);
for (ssize_t j = pj0; j <= pj1; j++)
{
for (ssize_t i = pi0; i <= pi1; i++)
{
CPatch* patch = GetPatch(i, j); // can't fail (i,j were clamped)
if (dirtyFlags & RENDERDATA_UPDATE_VERTICES)
patch->CalcBounds();
patch->SetDirty(dirtyFlags);
}
}
if (m_Heightmap)
{
m_HeightMipmap.Update(m_Heightmap,
Clamp(i0, 0, m_MapSize - 1),
Clamp(j0, 0, m_MapSize - 1),
Clamp(i1, 1, m_MapSize),
Clamp(j1, 1, m_MapSize)
);
}
}
void CTerrain::MakeDirty(int dirtyFlags)
{
for (ssize_t j = 0; j < m_MapSizePatches; j++)
{
for (ssize_t i = 0; i < m_MapSizePatches; i++)
{
CPatch* patch = GetPatch(i, j); // can't fail
if (dirtyFlags & RENDERDATA_UPDATE_VERTICES)
patch->CalcBounds();
patch->SetDirty(dirtyFlags);
}
}
if (m_Heightmap)
m_HeightMipmap.Update(m_Heightmap);
}
CBoundingBoxAligned CTerrain::GetVertexesBound(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1)
{
i0 = Clamp(i0, 0, m_MapSize - 1);
j0 = Clamp(j0, 0, m_MapSize - 1);
i1 = Clamp(i1, 0, m_MapSize - 1);
j1 = Clamp(j1, 0, m_MapSize - 1);
u16 minH = 65535;
u16 maxH = 0;
for (ssize_t j = j0; j <= j1; ++j)
{
for (ssize_t i = i0; i <= i1; ++i)
{
minH = std::min(minH, m_Heightmap[j*m_MapSize + i]);
maxH = std::max(maxH, m_Heightmap[j*m_MapSize + i]);
}
}
CBoundingBoxAligned bound;
bound[0].X = (float)(i0*TERRAIN_TILE_SIZE);
bound[0].Y = (float)(minH*HEIGHT_SCALE);
bound[0].Z = (float)(j0*TERRAIN_TILE_SIZE);
bound[1].X = (float)(i1*TERRAIN_TILE_SIZE);
bound[1].Y = (float)(maxH*HEIGHT_SCALE);
bound[1].Z = (float)(j1*TERRAIN_TILE_SIZE);
return bound;
}
Index: ps/trunk/source/graphics/Terrain.h
===================================================================
--- ps/trunk/source/graphics/Terrain.h (revision 26268)
+++ ps/trunk/source/graphics/Terrain.h (revision 26269)
@@ -1,183 +1,181 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 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 .
*/
/*
* Describes ground via heightmap and array of CPatch.
*/
#ifndef INCLUDED_TERRAIN
#define INCLUDED_TERRAIN
#include "graphics/HeightMipmap.h"
#include "graphics/SColor.h"
#include "maths/Fixed.h"
#include "maths/Vector3D.h"
#include "ps/CStr.h"
class CPatch;
class CMiniPatch;
class CFixedVector3D;
class CBoundingBoxAligned;
///////////////////////////////////////////////////////////////////////////////
// Terrain Constants:
/// metres [world space units] per tile in x and z
const ssize_t TERRAIN_TILE_SIZE = 4;
/// number of u16 height units per metre
const ssize_t HEIGHT_UNITS_PER_METRE = 92;
/// metres per u16 height unit
const float HEIGHT_SCALE = 1.f / HEIGHT_UNITS_PER_METRE;
///////////////////////////////////////////////////////////////////////////////
// CTerrain: main terrain class; contains the heightmap describing elevation
// data, and the smaller subpatches that form the terrain
class CTerrain
{
public:
CTerrain();
~CTerrain();
// Coordinate naming convention: world-space coordinates are float x,z;
// tile-space coordinates are ssize_t i,j. rationale: signed types can
// more efficiently be converted to/from floating point. use ssize_t
// instead of int/long because these are sizes.
bool Initialize(ssize_t patchesPerSide, const u16* ptr);
// return number of vertices along edge of the terrain
ssize_t GetVerticesPerSide() const { return m_MapSize; }
// return number of tiles along edge of the terrain
ssize_t GetTilesPerSide() const { return GetVerticesPerSide()-1; }
// return number of patches along edge of the terrain
ssize_t GetPatchesPerSide() const { return m_MapSizePatches; }
float GetMinX() const { return 0.0f; }
float GetMinZ() const { return 0.0f; }
float GetMaxX() const { return (float)((m_MapSize-1) * TERRAIN_TILE_SIZE); }
float GetMaxZ() const { return (float)((m_MapSize-1) * TERRAIN_TILE_SIZE); }
bool IsOnMap(float x, float z) const
{
return ((x >= GetMinX()) && (x < GetMaxX())
&& (z >= GetMinZ()) && (z < GetMaxZ()));
}
- CStr8 GetMovementClass(ssize_t i, ssize_t j) const;
-
float GetVertexGroundLevel(ssize_t i, ssize_t j) const;
fixed GetVertexGroundLevelFixed(ssize_t i, ssize_t j) const;
float GetExactGroundLevel(float x, float z) const;
fixed GetExactGroundLevelFixed(fixed x, fixed z) const;
float GetFilteredGroundLevel(float x, float z, float radius) const;
// get the approximate slope of a tile
// (0 = horizontal, 0.5 = 30 degrees, 1.0 = 45 degrees, etc)
fixed GetSlopeFixed(ssize_t i, ssize_t j) const;
// get the precise slope of a point, accounting for triangulation direction
fixed GetExactSlopeFixed(fixed x, fixed z) const;
// Returns true if the triangulation diagonal for tile (i, j)
// should be in the direction (1,-1); false if it should be (1,1)
bool GetTriangulationDir(ssize_t i, ssize_t j) const;
// Resize this terrain such that each side has given number of patches,
// with the center offset in patches from the center of the source.
void ResizeAndOffset(ssize_t size, ssize_t horizontalOffset = 0, ssize_t verticalOffset = 0);
// set up a new heightmap from 16 bit data; assumes heightmap matches current terrain size
void SetHeightMap(u16* heightmap);
// return a pointer to the heightmap
u16* GetHeightMap() const { return m_Heightmap; }
// get patch at given coordinates, expressed in patch-space; return 0 if
// coordinates represent patch off the edge of the map
CPatch* GetPatch(ssize_t i, ssize_t j) const;
// get tile at given coordinates, expressed in tile-space; return 0 if
// coordinates represent tile off the edge of the map
CMiniPatch* GetTile(ssize_t i, ssize_t j) const;
// calculate the position of a given vertex
void CalcPosition(ssize_t i, ssize_t j, CVector3D& pos) const;
void CalcPositionFixed(ssize_t i, ssize_t j, CFixedVector3D& pos) const;
// calculate the vertex under a given position (rounding down coordinates)
static void CalcFromPosition(const CVector3D& pos, ssize_t& i, ssize_t& j)
{
i = (ssize_t)(pos.X/TERRAIN_TILE_SIZE);
j = (ssize_t)(pos.Z/TERRAIN_TILE_SIZE);
}
// calculate the vertex under a given position (rounding down coordinates)
static void CalcFromPosition(float x, float z, ssize_t& i, ssize_t& j)
{
i = (ssize_t)(x/TERRAIN_TILE_SIZE);
j = (ssize_t)(z/TERRAIN_TILE_SIZE);
}
// calculate the normal at a given vertex
void CalcNormal(ssize_t i, ssize_t j, CVector3D& normal) const;
void CalcNormalFixed(ssize_t i, ssize_t j, CFixedVector3D& normal) const;
CVector3D CalcExactNormal(float x, float z) const;
// Mark a specific square of tiles (inclusive lower bound, exclusive upper bound)
// as dirty - use this after modifying the heightmap.
// If you modify a vertex (i,j), you should dirty tiles
// from (i-1, j-1) [inclusive] to (i+1, j+1) [exclusive]
// since their geometry depends on that vertex.
// If you modify a tile (i,j), you should dirty tiles
// from (i-1, j-1) [inclusive] to (i+2, j+2) [exclusive]
// since their texture blends depend on that tile.
void MakeDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1, int dirtyFlags);
// mark the entire map as dirty
void MakeDirty(int dirtyFlags);
/**
* Returns a 3D bounding box encompassing the given vertex range (inclusive)
*/
CBoundingBoxAligned GetVertexesBound(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1);
// get the base color for the terrain (typically pure white - other colors
// will interact badly with LOS - but used by the Actor Viewer tool)
SColor4ub GetBaseColor() const { return m_BaseColor; }
// set the base color for the terrain
void SetBaseColor(SColor4ub color) { m_BaseColor = color; }
const CHeightMipmap& GetHeightMipmap() const { return m_HeightMipmap; }
private:
// delete any data allocated by this terrain
void ReleaseData();
// setup patch pointers etc
void InitialisePatches();
// size of this map in each direction, in vertices; ie. total tiles = sqr(m_MapSize-1)
ssize_t m_MapSize;
// size of this map in each direction, in patches; total patches = sqr(m_MapSizePatches)
ssize_t m_MapSizePatches;
// the patches comprising this terrain
CPatch* m_Patches;
// 16-bit heightmap data
u16* m_Heightmap;
// base color (usually white)
SColor4ub m_BaseColor;
// heightmap mipmap
CHeightMipmap m_HeightMipmap;
};
#endif // INCLUDED_TERRAIN
Index: ps/trunk/source/graphics/TerrainProperties.cpp
===================================================================
--- ps/trunk/source/graphics/TerrainProperties.cpp (revision 26268)
+++ ps/trunk/source/graphics/TerrainProperties.cpp (revision 26269)
@@ -1,160 +1,154 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "TerrainProperties.h"
#include
#include
#include
#include "graphics/Color.h"
#include "graphics/TerrainTextureManager.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/XML/Xeromyces.h"
CTerrainProperties::CTerrainProperties(CTerrainPropertiesPtr parent):
m_pParent(parent),
m_BaseColor(0),
m_HasBaseColor(false),
m_TextureAngle((float)M_PI / 4.f),
- m_TextureSize(32.f),
- m_MovementClass("default")
+ m_TextureSize(32.f)
{
if (m_pParent)
m_Groups = m_pParent->m_Groups;
}
CTerrainPropertiesPtr CTerrainProperties::FromXML(const CTerrainPropertiesPtr& parent, const VfsPath& pathname)
{
CXeromyces XeroFile;
if (XeroFile.Load(g_VFS, pathname, "terrain") != PSRETURN_OK)
return CTerrainPropertiesPtr();
XMBElement root = XeroFile.GetRoot();
CStr rootName = XeroFile.GetElementString(root.GetNodeName());
// Check that we've got the right kind of xml document
if (rootName != "Terrains")
{
LOGERROR("TerrainProperties: Loading %s: Root node is not terrains (found \"%s\")",
pathname.string8(),
rootName);
return CTerrainPropertiesPtr();
}
#define ELMT(x) int el_##x = XeroFile.GetElementID(#x)
ELMT(terrain);
#undef ELMT
// Ignore all non-terrain nodes, loading the first terrain node and
// returning it.
// Really, we only expect there to be one child and it to be of the right
// type, though.
XERO_ITER_EL(root, child)
{
if (child.GetNodeName() == el_terrain)
{
CTerrainPropertiesPtr ret (new CTerrainProperties(parent));
ret->LoadXml(child, &XeroFile, pathname);
return ret;
}
else
{
LOGWARNING("TerrainProperties: Loading %s: Unexpected node %s\n",
pathname.string8(),
XeroFile.GetElementString(child.GetNodeName()));
// Keep reading - typos shouldn't be showstoppers
}
}
return CTerrainPropertiesPtr();
}
void CTerrainProperties::LoadXml(XMBElement node, CXeromyces *pFile, const VfsPath& UNUSED(pathname))
{
#define ELMT(x) int elmt_##x = pFile->GetElementID(#x)
#define ATTR(x) int attr_##x = pFile->GetAttributeID(#x)
// Terrain Attribs
ATTR(mmap);
ATTR(groups);
- ATTR(movementclass);
ATTR(angle);
ATTR(size);
#undef ELMT
#undef ATTR
XERO_ITER_ATTR(node, attr)
{
if (attr.Name == attr_groups)
{
// Parse a comma-separated list of groups, add the new entry to
// each of them
m_Groups.clear();
boost::char_separator sep(", ");
typedef boost::tokenizer > tokenizer;
tokenizer tok(attr.Value, sep);
for(tokenizer::iterator it = tok.begin(); it != tok.end(); ++it)
m_Groups.push_back(g_TexMan.FindGroup(*it));
}
else if (attr.Name == attr_mmap)
{
CColor col;
if (!col.ParseString(attr.Value, 255))
continue;
// m_BaseColor is BGRA
u8 *baseColor = (u8*)&m_BaseColor;
baseColor[0] = (u8)(col.b*255);
baseColor[1] = (u8)(col.g*255);
baseColor[2] = (u8)(col.r*255);
baseColor[3] = (u8)(col.a*255);
m_HasBaseColor = true;
}
else if (attr.Name == attr_angle)
{
m_TextureAngle = DEGTORAD(attr.Value.ToFloat());
}
else if (attr.Name == attr_size)
{
m_TextureSize = attr.Value.ToFloat();
}
- else if (attr.Name == attr_movementclass)
- {
- m_MovementClass = attr.Value;
- }
}
}
bool CTerrainProperties::HasBaseColor()
{
return m_HasBaseColor || (m_pParent && m_pParent->HasBaseColor());
}
u32 CTerrainProperties::GetBaseColor()
{
if (m_HasBaseColor || !m_pParent)
return m_BaseColor;
else if (m_pParent)
return m_pParent->GetBaseColor();
else
// White, full opacity.. but this value shouldn't ever be used
return 0xFFFFFFFF;
}
Index: ps/trunk/source/graphics/TerrainProperties.h
===================================================================
--- ps/trunk/source/graphics/TerrainProperties.h (revision 26268)
+++ ps/trunk/source/graphics/TerrainProperties.h (revision 26269)
@@ -1,117 +1,110 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 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 .
*/
/*
///////////////////////////////////////////////
CTerrainProperties
Basically represents a set of terrain attributes loaded from XML. These
objects are organized in an inheritance tree, determined at load time.
*/
#ifndef INCLUDED_TERRAINPROPERTIES
#define INCLUDED_TERRAINPROPERTIES
#include
#include "ps/CStr.h"
#include "lib/file/vfs/vfs_path.h"
class CTerrainGroup;
class XMBElement;
class CXeromyces;
class CTerrainProperties;
typedef std::shared_ptr CTerrainPropertiesPtr;
class CTerrainProperties
{
public:
typedef std::vector GroupVector;
private:
CTerrainPropertiesPtr m_pParent;
// BGRA color of topmost mipmap level, for coloring minimap, or a color
// manually specified in the Terrain XML (or by any parent)
// ..Valid is true if the base color is specified in this terrain XML
// No caching here, since ideally, a saved XML file of an object should
// produce be equivalent to the source file
u32 m_BaseColor;
bool m_HasBaseColor;
- CStr m_MovementClass;
-
// Orientation of texture (in radians) (default pi/4 = 45 degrees)
float m_TextureAngle;
// Size of texture in metres (default 32m = 8 tiles)
float m_TextureSize;
// All terrain type groups we're a member of
GroupVector m_Groups;
public:
CTerrainProperties(CTerrainPropertiesPtr parent);
// Create a new object and load the XML file specified. Returns NULL upon
// failure
// The parent pointer may be NULL, for the "root" terrainproperties object.
static CTerrainPropertiesPtr FromXML(const CTerrainPropertiesPtr& parent, const VfsPath& pathname);
void LoadXml(XMBElement node, CXeromyces *pFile, const VfsPath& pathname);
// Save the object to an XML file. Implement when needed! ;-)
// bool WriteXML(const CStr& path);
inline CTerrainPropertiesPtr GetParent() const
{
return m_pParent;
}
// Return true if this property object or any of its parents has a basecolor
// override (mmap attribute in the XML file)
bool HasBaseColor();
// Return the minimap color specified in this property object or in any of
// its parents. If no minimap color is specified, return garbage.
// Use HasBaseColor() to see if the value is valid.
// The color value is in BGRA format
u32 GetBaseColor();
float GetTextureAngle()
{
return m_TextureAngle;
}
float GetTextureSize()
{
return m_TextureSize;
}
- CStr GetMovementClass() const
- {
- return m_MovementClass;
- }
-
const GroupVector &GetGroups() const
{
return m_Groups;
}
};
#endif
Index: ps/trunk/source/network/tests/test_Net.h
===================================================================
--- ps/trunk/source/network/tests/test_Net.h (revision 26268)
+++ ps/trunk/source/network/tests/test_Net.h (revision 26269)
@@ -1,384 +1,377 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "lib/self_test.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/external_libraries/enet.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/tex/tex.h"
#include "network/NetServer.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetMessages.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "ps/Filesystem.h"
#include "ps/Loader.h"
#include "ps/XML/Xeromyces.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
class TestNetComms : public CxxTest::TestSuite
{
public:
void setUp()
{
g_VFS = CreateVfs();
TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST));
TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY));
CXeromyces::Startup();
- // Need some stuff for terrain movement costs:
- // (TODO: this ought to be independent of any graphics code)
- new CTerrainTextureManager;
- g_TexMan.LoadTerrainTextures();
-
enet_initialize();
}
void tearDown()
{
enet_deinitialize();
- delete &g_TexMan;
-
CXeromyces::Terminate();
g_VFS.reset();
DeleteDirectory(DataDir()/"_testcache");
}
bool clients_are_all(const std::vector& clients, uint state)
{
for (size_t j = 0; j < clients.size(); ++j)
if (clients[j]->GetCurrState() != state)
return false;
return true;
}
void connect(CNetServer& server, const std::vector& clients)
{
TS_ASSERT(server.SetupConnection(PS_DEFAULT_PORT));
for (CNetClient* client: clients)
{
client->SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false);
TS_ASSERT(client->SetupConnection(nullptr));
}
for (size_t i = 0; ; ++i)
{
// debug_printf(".");
for (size_t j = 0; j < clients.size(); ++j)
clients[j]->Poll();
if (clients_are_all(clients, NCS_PREGAME))
break;
if (i > 20)
{
TS_FAIL("connection timeout");
break;
}
SDL_Delay(100);
}
}
#if 0
void disconnect(CNetServer& server, const std::vector& clients)
{
for (size_t i = 0; ; ++i)
{
// debug_printf(".");
server.Poll();
for (size_t j = 0; j < clients.size(); ++j)
clients[j]->Poll();
if (server.GetState() == SERVER_STATE_UNCONNECTED && clients_are_all(clients, NCS_UNCONNECTED))
break;
if (i > 20)
{
TS_FAIL("disconnection timeout");
break;
}
SDL_Delay(100);
}
}
#endif
void wait(const std::vector& clients, size_t msecs)
{
for (size_t i = 0; i < msecs/10; ++i)
{
for (size_t j = 0; j < clients.size(); ++j)
clients[j]->Poll();
SDL_Delay(10);
}
}
void test_basic_DISABLED()
{
// This doesn't actually test much, it just runs a very quick multiplayer game
// and prints a load of debug output so you can see if anything funny's going on
ScriptInterface scriptInterface("Engine", "Test", g_ScriptContext);
ScriptRequest rq(scriptInterface);
TestStdoutLogger logger;
std::vector clients;
CGame client1Game(false);
CGame client2Game(false);
CGame client3Game(false);
CNetServer server("no_secret");
JS::RootedValue attrs(rq.cx);
Script::CreateObject(
rq,
&attrs,
"mapType", "scenario",
"map", "maps/scenarios/Saharan Oases",
"mapPath", "maps/scenarios/",
"thing", "example");
server.UpdateInitAttributes(&attrs, scriptInterface);
CNetClient client1(&client1Game);
CNetClient client2(&client2Game);
CNetClient client3(&client3Game);
clients.push_back(&client1);
clients.push_back(&client2);
clients.push_back(&client3);
connect(server, clients);
debug_printf("%s", client1.TestReadGuiMessages().c_str());
server.StartGame();
SDL_Delay(100);
for (size_t j = 0; j < clients.size(); ++j)
{
clients[j]->Poll();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
clients[j]->LoadFinished();
}
wait(clients, 100);
{
JS::RootedValue cmd(rq.cx);
Script::CreateObject(
rq,
&cmd,
"type", "debug-print",
"message", "[>>> client1 test sim command]\\n");
client1Game.GetTurnManager()->PostCommand(cmd);
}
{
JS::RootedValue cmd(rq.cx);
Script::CreateObject(
rq,
&cmd,
"type", "debug-print",
"message", "[>>> client2 test sim command]\\n");
client2Game.GetTurnManager()->PostCommand(cmd);
}
wait(clients, 100);
client1Game.GetTurnManager()->Update(1.0f, 1);
client2Game.GetTurnManager()->Update(1.0f, 1);
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
client1Game.GetTurnManager()->Update(1.0f, 1);
client2Game.GetTurnManager()->Update(1.0f, 1);
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
}
void test_rejoin_DISABLED()
{
ScriptInterface scriptInterface("Engine", "Test", g_ScriptContext);
ScriptRequest rq(scriptInterface);
TestStdoutLogger logger;
std::vector clients;
CGame client1Game(false);
CGame client2Game(false);
CGame client3Game(false);
CNetServer server("no_secret");
JS::RootedValue attrs(rq.cx);
Script::CreateObject(
rq,
&attrs,
"mapType", "scenario",
"map", "maps/scenarios/Saharan Oases",
"mapPath", "maps/scenarios/",
"thing", "example");
server.UpdateInitAttributes(&attrs, scriptInterface);
CNetClient client1(&client1Game);
CNetClient client2(&client2Game);
CNetClient client3(&client3Game);
client1.SetUserName(L"alice");
client2.SetUserName(L"bob");
client3.SetUserName(L"charlie");
clients.push_back(&client1);
clients.push_back(&client2);
clients.push_back(&client3);
connect(server, clients);
debug_printf("%s", client1.TestReadGuiMessages().c_str());
server.StartGame();
SDL_Delay(100);
for (size_t j = 0; j < clients.size(); ++j)
{
clients[j]->Poll();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
clients[j]->LoadFinished();
}
wait(clients, 100);
{
JS::RootedValue cmd(rq.cx);
Script::CreateObject(
rq,
&cmd,
"type", "debug-print",
"message", "[>>> client1 test sim command 1]\\n");
client1Game.GetTurnManager()->PostCommand(cmd);
}
wait(clients, 100);
client1Game.GetTurnManager()->Update(1.0f, 1);
client2Game.GetTurnManager()->Update(1.0f, 1);
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
{
JS::RootedValue cmd(rq.cx);
Script::CreateObject(
rq,
&cmd,
"type", "debug-print",
"message", "[>>> client1 test sim command 2]\\n");
client1Game.GetTurnManager()->PostCommand(cmd);
}
debug_printf("==== Disconnecting client 2\n");
client2.DestroyConnection();
clients.erase(clients.begin()+1);
debug_printf("==== Connecting client 2B\n");
CGame client2BGame(false);
CNetClient client2B(&client2BGame);
client2B.SetUserName(L"bob");
clients.push_back(&client2B);
client2B.SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false);
TS_ASSERT(client2B.SetupConnection(nullptr));
for (size_t i = 0; ; ++i)
{
debug_printf("[%u]\n", client2B.GetCurrState());
client2B.Poll();
if (client2B.GetCurrState() == NCS_PREGAME)
break;
if (client2B.GetCurrState() == NCS_UNCONNECTED)
{
TS_FAIL("connection rejected");
return;
}
if (i > 20)
{
TS_FAIL("connection timeout");
return;
}
SDL_Delay(100);
}
wait(clients, 100);
client1Game.GetTurnManager()->Update(1.0f, 1);
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
server.SetTurnLength(100);
client1Game.GetTurnManager()->Update(1.0f, 1);
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
// (This SetTurnLength thing doesn't actually detect errors unless you change
// CTurnManager::TurnNeedsFullHash to always return true)
{
JS::RootedValue cmd(rq.cx);
Script::CreateObject(
rq,
&cmd,
"type", "debug-print",
"message", "[>>> client1 test sim command 3]\\n");
client1Game.GetTurnManager()->PostCommand(cmd);
}
clients[2]->Poll();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
clients[2]->LoadFinished();
wait(clients, 100);
{
JS::RootedValue cmd(rq.cx);
Script::CreateObject(
rq,
&cmd,
"type", "debug-print",
"message", "[>>> client1 test sim command 4]\\n");
client1Game.GetTurnManager()->PostCommand(cmd);
}
for (size_t i = 0; i < 3; ++i)
{
client1Game.GetTurnManager()->Update(1.0f, 1);
client2BGame.GetTurnManager()->Update(1.0f, 1);
client3Game.GetTurnManager()->Update(1.0f, 1);
wait(clients, 100);
}
}
};
Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp
===================================================================
--- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 26268)
+++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 26269)
@@ -1,1371 +1,1363 @@
/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "ps/GameSetup/GameSetup.h"
#include "graphics/GameView.h"
#include "graphics/MapReader.h"
#include "graphics/TerrainTextureManager.h"
#include "gui/CGUI.h"
#include "gui/GUIManager.h"
#include "i18n/L10n.h"
#include "lib/app_hooks.h"
#include "lib/config2.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/file/common/file_stats.h"
#include "lib/input.h"
#include "lib/ogl.h"
#include "lib/res/graphics/ogl_tex.h"
#include "lib/res/h_mgr.h"
#include "lib/timer.h"
#include "lobby/IXmppClient.h"
#include "network/NetServer.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetMessages.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/Paths.h"
#include "ps/GameSetup/Config.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "ps/GameSetup/HWDetect.h"
#include "ps/Globals.h"
#include "ps/GUID.h"
#include "ps/Hotkey.h"
#include "ps/Joystick.h"
#include "ps/Loader.h"
#include "ps/Mod.h"
#include "ps/ModIo.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
#include "ps/Profiler2.h"
#include "ps/Pyrogenesis.h" // psSetLogDir
#include "ps/scripting/JSInterface_Console.h"
#include "ps/TouchInput.h"
#include "ps/UserReport.h"
#include "ps/Util.h"
#include "ps/VideoMode.h"
#include "ps/VisualReplay.h"
#include "ps/World.h"
#include "renderer/Renderer.h"
#include "renderer/SceneRenderer.h"
#include "renderer/VertexBufferManager.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/JSON.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptStats.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptConversions.h"
#include "simulation2/Simulation2.h"
#include "soundmanager/scripting/JSInterface_Sound.h"
#include "soundmanager/ISoundManager.h"
#include "tools/atlas/GameInterface/GameLoop.h"
#if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets
#define MUST_INIT_X11 1
#include
#else
#define MUST_INIT_X11 0
#endif
extern void RestartEngine();
#include
#include
#include
#include
#include
ERROR_GROUP(System);
ERROR_TYPE(System, SDLInitFailed);
ERROR_TYPE(System, VmodeFailed);
ERROR_TYPE(System, RequiredExtensionsMissing);
thread_local std::shared_ptr g_ScriptContext;
static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code
bool g_InDevelopmentCopy;
bool g_CheckedIfInDevelopmentCopy = false;
static void SetTextureQuality(int quality)
{
int q_flags;
GLint filter;
retry:
// keep this in sync with SANE_TEX_QUALITY_DEFAULT
switch(quality)
{
// worst quality
case 0:
q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP;
filter = GL_NEAREST;
break;
// [perf] add bilinear filtering
case 1:
q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP;
filter = GL_LINEAR;
break;
// [vmem] no longer reduce resolution
case 2:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_LINEAR;
break;
// [vmem] add mipmaps
case 3:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_NEAREST_MIPMAP_LINEAR;
break;
// [perf] better filtering
case 4:
q_flags = OGL_TEX_HALF_BPP;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// [vmem] no longer reduce bpp
case SANE_TEX_QUALITY_DEFAULT:
q_flags = OGL_TEX_FULL_QUALITY;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// [perf] add anisotropy
case 6:
// TODO: add anisotropic filtering
q_flags = OGL_TEX_FULL_QUALITY;
filter = GL_LINEAR_MIPMAP_LINEAR;
break;
// invalid
default:
debug_warn(L"SetTextureQuality: invalid quality");
quality = SANE_TEX_QUALITY_DEFAULT;
// careful: recursion doesn't work and we don't want to duplicate
// the "sane" default values.
goto retry;
}
ogl_tex_set_defaults(q_flags, filter);
}
ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags))
{
// If we're fullscreen, then sometimes (at least on some particular drivers on Linux)
// displaying the error dialog hangs the desktop since the dialog box is behind the
// fullscreen window. So we just force the game to windowed mode before displaying the dialog.
// (But only if we're in the main thread, and not if we're being reentrant.)
if (Threading::IsMainThread())
{
static bool reentering = false;
if (!reentering)
{
reentering = true;
g_VideoMode.SetFullscreen(false);
reentering = false;
}
}
// We don't actually implement the error display here, so return appropriately
return ERI_NOT_IMPLEMENTED;
}
void MountMods(const Paths& paths, const std::vector& mods)
{
OsPath modPath = paths.RData()/"mods";
OsPath modUserPath = paths.UserData()/"mods";
size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE;
size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST;
size_t priority = 0;
for (size_t i = 0; i < mods.size(); ++i)
{
priority = i + 1; // Mods are higher priority than regular mountings, which default to priority 0
OsPath modName(mods[i]);
// Only mount mods from the user path if they don't exist in the 'rdata' path.
if (DirectoryExists(modPath / modName / ""))
g_VFS->Mount(L"", modPath / modName / "", baseFlags, priority);
else
g_VFS->Mount(L"", modUserPath / modName / "", userFlags, priority);
}
// Mount the user mod last. In dev copy, mount it with a low priority. Otherwise, make it writable.
g_VFS->Mount(L"", modUserPath / "user" / "", userFlags, InDevelopmentCopy() ? 0 : priority + 1);
}
static void InitVfs(const CmdLineArgs& args, int flags)
{
TIMER(L"InitVfs");
const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0;
const Paths paths(args);
OsPath logs(paths.Logs());
CreateDirectories(logs, 0700);
psSetLogDir(logs);
// desired location for crashlog is now known. update AppHooks ASAP
// (particularly before the following error-prone operations):
AppHooks hooks = {0};
hooks.bundle_logs = psBundleLogs;
hooks.get_log_dir = psLogDir;
if (setup_error)
hooks.display_error = psDisplayError;
app_hooks_update(&hooks);
g_VFS = CreateVfs();
const OsPath readonlyConfig = paths.RData()/"config"/"";
// Mount these dirs with highest priority so that mods can't overwrite them.
g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); // (adding XMBs to archive speeds up subsequent reads)
if (readonlyConfig != paths.Config())
g_VFS->Mount(L"config/", readonlyConfig, 0, VFS_MAX_PRIORITY-1);
g_VFS->Mount(L"config/", paths.Config(), 0, VFS_MAX_PRIORITY);
g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY);
g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH, VFS_MAX_PRIORITY);
// Engine localization files (regular priority, these can be overwritten).
g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/"");
// Mods will be mounted later.
// note: don't bother with g_VFS->TextRepresentation - directories
// haven't yet been populated and are empty.
}
static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData)
{
{
// console
TIMER(L"ps_console");
g_Console->Init();
}
// hotkeys
{
TIMER(L"ps_lang_hotkeys");
LoadHotkeys(g_ConfigDB);
}
if (!setup_gui)
{
// We do actually need *some* kind of GUI loaded, so use the
// (currently empty) Atlas one
g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData);
return;
}
// GUI uses VFS, so this must come after VFS init.
g_GUI->SwitchPage(gui_page, srcScriptInterface, initData);
}
void InitPsAutostart(bool networked, JS::HandleValue attrs)
{
// The GUI has not been initialized yet, so use the simulation scriptinterface for this variable
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue playerAssignments(rq.cx);
Script::CreateObject(rq, &playerAssignments);
if (!networked)
{
JS::RootedValue localPlayer(rq.cx);
Script::CreateObject(rq, &localPlayer, "player", g_Game->GetPlayerID());
Script::SetProperty(rq, playerAssignments, "local", localPlayer);
}
JS::RootedValue sessionInitData(rq.cx);
Script::CreateObject(
rq,
&sessionInitData,
"attribs", attrs,
"playerAssignments", playerAssignments);
InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData);
}
void InitInput()
{
g_Joystick.Initialise();
// register input handlers
// This stack is constructed so the first added, will be the last
// one called. This is important, because each of the handlers
// has the potential to block events to go further down
// in the chain. I.e. the last one in the list added, is the
// only handler that can block all messages before they are
// processed.
in_add_handler(game_view_handler);
in_add_handler(CProfileViewer::InputThunk);
in_add_handler(HotkeyInputActualHandler);
// gui_handler needs to be registered after (i.e. called before!) the
// hotkey handler so that input boxes can be typed in without
// setting off hotkeys.
in_add_handler(gui_handler);
// Likewise for the console.
in_add_handler(conInputHandler);
in_add_handler(touch_input_handler);
// Should be called after scancode map update (i.e. after the global input, but before UI).
// This never blocks the event, but it does some processing necessary for hotkeys,
// which are triggered later down the input chain.
// (by calling this before the UI, we can use 'EventWouldTriggerHotkey' in the UI).
in_add_handler(HotkeyInputPrepHandler);
// These two must be called first (i.e. pushed last)
// GlobalsInputHandler deals with some important global state,
// such as which scancodes are being pressed, mouse buttons pressed, etc.
// while HotkeyStateChange updates the map of active hotkeys.
in_add_handler(GlobalsInputHandler);
in_add_handler(HotkeyStateChange);
}
static void ShutdownPs()
{
SAFE_DELETE(g_GUI);
UnloadHotkeys();
}
static void InitSDL()
{
#if OS_LINUX
// In fullscreen mode when SDL is compiled with DGA support, the mouse
// sensitivity often appears to be unusably wrong (typically too low).
// (This seems to be reported almost exclusively on Ubuntu, but can be
// reproduced on Gentoo after explicitly enabling DGA.)
// Disabling the DGA mouse appears to fix that problem, and doesn't
// have any obvious negative effects.
setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0);
#endif
if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0)
{
LOGERROR("SDL library initialization failed: %s", SDL_GetError());
throw PSERROR_System_SDLInitFailed();
}
atexit(SDL_Quit);
// Text input is active by default, disable it until it is actually needed.
SDL_StopTextInput();
#if SDL_VERSION_ATLEAST(2, 0, 9)
// SDL2 >= 2.0.9 defaults to 32 pixels (to support touch screens) but that can break our double-clicking.
SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, "1");
#endif
#if SDL_VERSION_ATLEAST(2, 0, 14) && OS_WIN
// SDL2 >= 2.0.14 Before SDL 2.0.14, this defaulted to true. In 2.0.14 they switched to false
// breaking the behavior on Windows.
// https://github.com/libsdl-org/SDL/commit/1947ca7028ab165cc3e6cbdb0b4b7c4db68d1710
// https://github.com/libsdl-org/SDL/issues/5033
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "1");
#endif
#if OS_MACOSX
// Some Mac mice only have one button, so they can't right-click
// but SDL2 can emulate that with Ctrl+Click
bool macMouse = false;
CFG_GET_VAL("macmouse", macMouse);
SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0");
#endif
}
static void ShutdownSDL()
{
SDL_Quit();
}
void EndGame()
{
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_Game);
if (CRenderer::IsInitialised())
{
ISoundManager::CloseGame();
g_Renderer.GetSceneRenderer().ResetState();
}
}
void Shutdown(int flags)
{
const bool hasRenderer = CRenderer::IsInitialised();
if ((flags & SHUTDOWN_FROM_CONFIG))
goto from_config;
EndGame();
SAFE_DELETE(g_XmppClient);
SAFE_DELETE(g_ModIo);
ShutdownPs();
- TIMER_BEGIN(L"shutdown TexMan");
- delete &g_TexMan;
- TIMER_END(L"shutdown TexMan");
-
if (hasRenderer)
{
TIMER_BEGIN(L"shutdown Renderer");
g_Renderer.~CRenderer();
g_VBMan.Shutdown();
TIMER_END(L"shutdown Renderer");
}
g_RenderingOptions.ClearHooks();
g_Profiler2.ShutdownGPU();
TIMER_BEGIN(L"shutdown SDL");
ShutdownSDL();
TIMER_END(L"shutdown SDL");
if (hasRenderer)
g_VideoMode.Shutdown();
TIMER_BEGIN(L"shutdown UserReporter");
g_UserReporter.Deinitialize();
TIMER_END(L"shutdown UserReporter");
// Cleanup curl now that g_ModIo and g_UserReporter have been shutdown.
curl_global_cleanup();
delete &g_L10n;
from_config:
TIMER_BEGIN(L"shutdown ConfigDB");
CConfigDB::Shutdown();
TIMER_END(L"shutdown ConfigDB");
SAFE_DELETE(g_Console);
// This is needed to ensure that no callbacks from the JSAPI try to use
// the profiler when it's already destructed
g_ScriptContext.reset();
// resource
// first shut down all resource owners, and then the handle manager.
TIMER_BEGIN(L"resource modules");
ISoundManager::SetEnabled(false);
g_VFS.reset();
// this forcibly frees all open handles (thus preventing real leaks),
// and makes further access to h_mgr impossible.
h_mgr_shutdown();
file_stats_dump();
TIMER_END(L"resource modules");
TIMER_BEGIN(L"shutdown misc");
timer_DisplayClientTotals();
CNetHost::Deinitialize();
// should be last, since the above use them
SAFE_DELETE(g_Logger);
delete &g_Profiler;
delete &g_ProfileViewer;
SAFE_DELETE(g_ScriptStatsTable);
TIMER_END(L"shutdown misc");
}
#if OS_UNIX
static void FixLocales()
{
#if OS_MACOSX || OS_BSD
// OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle
// wide characters. Peculiarly the string "UTF-8" seems to be acceptable
// despite not being a real locale, and it's conveniently language-agnostic,
// so use that.
setlocale(LC_CTYPE, "UTF-8");
#endif
// On misconfigured systems with incorrect locale settings, we'll die
// with a C++ exception when some code (e.g. Boost) tries to use locales.
// To avoid death, we'll detect the problem here and warn the user and
// reset to the default C locale.
// For informing the user of the problem, use the list of env vars that
// glibc setlocale looks at. (LC_ALL is checked first, and LANG last.)
const char* const LocaleEnvVars[] = {
"LC_ALL",
"LC_COLLATE",
"LC_CTYPE",
"LC_MONETARY",
"LC_NUMERIC",
"LC_TIME",
"LC_MESSAGES",
"LANG"
};
try
{
// this constructor is similar to setlocale(LC_ALL, ""),
// but instead of returning NULL, it throws runtime_error
// when the first locale env variable found contains an invalid value
std::locale("");
}
catch (std::runtime_error&)
{
LOGWARNING("Invalid locale settings");
for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++)
{
if (char* envval = getenv(LocaleEnvVars[i]))
LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval);
else
LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]);
}
// We should set LC_ALL since it overrides LANG
if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1))
debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable.");
else
LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL"));
}
}
#else
static void FixLocales()
{
// Do nothing on Windows
}
#endif
void EarlyInit()
{
// If you ever want to catch a particular allocation:
//_CrtSetBreakAlloc(232647);
Threading::SetMainThread();
debug_SetThreadName("main");
// add all debug_printf "tags" that we are interested in:
debug_filter_add("TIMER");
timer_Init();
// initialise profiler early so it can profile startup,
// but only after LatchStartTime
g_Profiler2.Initialise();
FixLocales();
// Because we do GL calls from a secondary thread, Xlib needs to
// be told to support multiple threads safely.
// This is needed for Atlas, but we have to call it before any other
// Xlib functions (e.g. the ones used when drawing the main menu
// before launching Atlas)
#if MUST_INIT_X11
int status = XInitThreads();
if (status == 0)
debug_printf("Error enabling thread-safety via XInitThreads\n");
#endif
// Initialise the low-quality rand function
srand(time(NULL)); // NOTE: this rand should *not* be used for simulation!
}
bool Autostart(const CmdLineArgs& args);
/**
* Returns true if the user has intended to start a visual replay from command line.
*/
bool AutostartVisualReplay(const std::string& replayFile);
bool Init(const CmdLineArgs& args, int flags)
{
h_mgr_init();
// Do this as soon as possible, because it chdirs
// and will mess up the error reporting if anything
// crashes before the working directory is set.
InitVfs(args, flags);
// This must come after VFS init, which sets the current directory
// (required for finding our output log files).
g_Logger = new CLogger;
new CProfileViewer;
new CProfileManager; // before any script code
g_ScriptStatsTable = new CScriptStatsTable;
g_ProfileViewer.AddRootTable(g_ScriptStatsTable);
// Set up the console early, so that debugging
// messages can be logged to it. (The console's size
// and fonts are set later in InitPs())
g_Console = new CConsole();
// g_ConfigDB, command line args, globals
CONFIG_Init(args);
// Using a global object for the context is a workaround until Simulation and AI use
// their own threads and also their own contexts.
const int contextSize = 384 * 1024 * 1024;
const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024;
g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger);
// On the first Init (INIT_MODS), check for command-line arguments
// or use the default mods from the config and enable those.
// On later engine restarts (e.g. the mod selector), we will skip this path,
// to avoid overwriting the newly selected mods.
if (flags & INIT_MODS)
{
ScriptInterface modInterface("Engine", "Mod", g_ScriptContext);
g_Mods.UpdateAvailableMods(modInterface);
std::vector mods;
if (args.Has("mod"))
mods = args.GetMultiple("mod");
else
{
CStr modsStr;
CFG_GET_VAL("mod.enabledmods", modsStr);
boost::split(mods, modsStr, boost::algorithm::is_space(), boost::token_compress_on);
}
if (!g_Mods.EnableMods(mods, flags & INIT_MODS_PUBLIC))
{
// In non-visual mode, fail entirely.
if (args.Has("autostart-nonvisual"))
{
LOGERROR("Trying to start with incompatible mods: %s.", boost::algorithm::join(g_Mods.GetIncompatibleMods(), ", "));
return false;
}
}
}
// If there are incompatible mods, switch to the mod selector so players can resolve the problem.
if (g_Mods.GetIncompatibleMods().empty())
MountMods(Paths(args), g_Mods.GetEnabledMods());
else
MountMods(Paths(args), { "mod" });
// Special command-line mode to dump the entity schemas instead of running the game.
// (This must be done after loading VFS etc, but should be done before wasting time
// on anything else.)
if (args.Has("dumpSchema"))
{
CSimulation2 sim(NULL, g_ScriptContext, NULL);
sim.LoadDefaultScripts();
std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc);
f << sim.GenerateSchema();
std::cout << "Generated entity.rng\n";
exit(0);
}
CNetHost::Initialize();
#if CONFIG2_AUDIO
if (!args.Has("autostart-nonvisual") && !g_DisableAudio)
ISoundManager::CreateSoundManager();
#endif
new L10n;
// Optionally start profiler HTTP output automatically
// (By default it's only enabled by a hotkey, for security/performance)
bool profilerHTTPEnable = false;
CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable);
if (profilerHTTPEnable)
g_Profiler2.EnableHTTP();
// Initialise everything except Win32 sockets (because our networking
// system already inits those)
curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32);
if (!g_Quickstart)
g_UserReporter.Initialize(); // after config
PROFILE2_EVENT("Init finished");
return true;
}
void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods)
{
const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0;
if(setup_vmode)
{
InitSDL();
if (!g_VideoMode.InitSDL())
throw PSERROR_System_VmodeFailed(); // abort startup
}
RunHardwareDetection();
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
SetTextureQuality(SANE_TEX_QUALITY_DEFAULT);
else
{
int textureQuality = SANE_TEX_QUALITY_DEFAULT;
CFG_GET_VAL("texturequality", textureQuality);
SetTextureQuality(textureQuality);
}
ogl_WarnIfError();
// Optionally start profiler GPU timings automatically
// (By default it's only enabled by a hotkey, for performance/compatibility)
bool profilerGPUEnable = false;
CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable);
if (profilerGPUEnable)
g_Profiler2.EnableGPU();
if(!g_Quickstart)
{
WriteSystemInfo();
// note: no longer vfs_display here. it's dog-slow due to unbuffered
// file output and very rarely needed.
}
if(g_DisableAudio)
ISoundManager::SetEnabled(false);
g_GUI = new CGUIManager();
CStr8 renderPath = "default";
CFG_GET_VAL("renderpath", renderPath);
if (RenderPathEnum::FromString(renderPath) == FIXED)
{
// It doesn't make sense to continue working here, because we're not
// able to display anything.
DEBUG_DISPLAY_FATAL_ERROR(
L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders."
L" The game does not support pre-shader graphics cards."
L" You are advised to try installing newer drivers and/or upgrade your graphics card."
L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734"
);
}
ogl_WarnIfError();
g_RenderingOptions.ReadConfigAndSetupHooks();
// create renderer
new CRenderer;
InitInput();
ogl_WarnIfError();
// TODO: Is this the best place for this?
if (VfsDirectoryExists(L"maps/"))
CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng");
try
{
if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args))
{
const bool setup_gui = ((flags & INIT_NO_GUI) == 0);
// We only want to display the splash screen at startup
std::shared_ptr scriptInterface = g_GUI->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue data(rq.cx);
if (g_GUI)
{
Script::CreateObject(rq, &data, "isStartup", true);
if (!installedMods.empty())
Script::SetProperty(rq, data, "installedMods", installedMods);
}
InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data);
}
}
catch (PSERROR_Game_World_MapLoadFailed& e)
{
// Map Loading failed
// Start the engine so we have a GUI
InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue);
// Call script function to do the actual work
// (delete game data, switch GUI page, show error, etc.)
CancelLoad(CStr(e.what()).FromUTF8());
}
}
void InitNonVisual(const CmdLineArgs& args)
{
- // Need some stuff for terrain movement costs:
- // (TODO: this ought to be independent of any graphics code)
- new CTerrainTextureManager;
- g_TexMan.LoadTerrainTextures();
Autostart(args);
}
/**
* Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON
* data from it.
* The scenario map format is used for scenario and skirmish map types (random
* games do not use a "map" (format) but a small JavaScript program which
* creates a map on the fly). It contains a section to initialize the game
* setup screen.
* @param mapPath Absolute path (from VFS root) to the map file to peek in.
* @return ScriptSettings in JSON format extracted from the map.
*/
CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath)
{
CXeromyces mapFile;
const char *pathToSettings[] =
{
"Scenario", "ScriptSettings", "" // Path to JSON data in map
};
Status loadResult = mapFile.Load(g_VFS, mapPath);
if (INFO::OK != loadResult)
{
LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8());
throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos.");
}
XMBElement mapElement = mapFile.GetRoot();
// Select the ScriptSettings node in the map file...
for (int i = 0; pathToSettings[i][0]; ++i)
{
int childId = mapFile.GetElementID(pathToSettings[i]);
XMBElementList nodes = mapElement.GetChildNodes();
auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) {
return child.GetNodeName() == childId;
});
if (it != nodes.end())
mapElement = *it;
}
// ... they contain a JSON document to initialize the game setup
// screen
return mapElement.GetText();
}
/*
* Command line options for autostart
* (keep synchronized with binaries/system/readme.txt):
*
* -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME;
* TYPEDIR is skirmishes, scenarios, or random
* -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random)
* -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra)
* -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI
* (0: sandbox, 5: very hard)
* -autostart-aiseed=AISEED sets the seed used for the AI random
* generator (default 0, use -1 for random)
* -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer)
* -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV
* (skirmish and random maps only)
* -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2).
* -autostart-ceasefire=NUM sets a ceasefire duration NUM
* (default 0 minutes)
* -autostart-nonvisual disable any graphics and sounds
* -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME
* located in simulation/data/settings/victory_conditions/
* (default conquest). When the first given SCRIPTNAME is
* "endless", no victory conditions will apply.
* -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition
* (default 10 minutes)
* -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition
* (default 10 minutes)
* -autostart-reliccount=NUM sets the number of relics for relic victory condition
* (default 2 relics)
* -autostart-disable-replay disable saving of replays
*
* Multiplayer:
* -autostart-playername=NAME sets local player NAME (default 'anonymous')
* -autostart-host sets multiplayer host mode
* -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer
* game (default 2)
* -autostart-client=IP sets multiplayer client to join host at
* given IP address
* Random maps only:
* -autostart-size=TILES sets random map size in TILES (default 192)
* -autostart-players=NUMBER sets NUMBER of players on random map
* (default 2)
*
* Examples:
* 1) "Bob" will host a 2 player game on the Arcadia map:
* -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob"
* "Alice" joins the match as player 2:
* -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice"
* The players use the developer overlay to control players.
*
* 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot:
* -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra
*
* 3) Observe the PetraBot on a triggerscript map:
* -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1
*/
bool Autostart(const CmdLineArgs& args)
{
CStr autoStartName = args.Get("autostart");
if (autoStartName.empty())
return false;
g_Game = new CGame(!args.Has("autostart-disable-replay"));
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue attrs(rq.cx);
JS::RootedValue settings(rq.cx);
JS::RootedValue playerData(rq.cx);
Script::CreateObject(rq, &attrs);
Script::CreateObject(rq, &settings);
Script::CreateArray(rq, &playerData);
// The directory in front of the actual map name indicates which type
// of map is being loaded. Drawback of this approach is the association
// of map types and folders is hard-coded, but benefits are:
// - No need to pass the map type via command line separately
// - Prevents mixing up of scenarios and skirmish maps to some degree
Path mapPath = Path(autoStartName);
std::wstring mapDirectory = mapPath.Parent().Filename().string();
std::string mapType;
if (mapDirectory == L"random")
{
// Random map definition will be loaded from JSON file, so we need to parse it
std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json";
JS::RootedValue scriptData(rq.cx);
Script::ReadJSONFile(rq, scriptPath, &scriptData);
if (!scriptData.isUndefined() && Script::GetProperty(rq, scriptData, "settings", &settings))
{
// JSON loaded ok - copy script name over to game attributes
std::wstring scriptFile;
if (!Script::GetProperty(rq, settings, "Script", scriptFile))
{
LOGERROR("Autostart: random map '%s' data has no 'Script' property.", utf8_from_wstring(scriptPath));
throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details.");
}
Script::SetProperty(rq, attrs, "script", scriptFile); // RMS filename
}
else
{
// Problem with JSON file
LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath));
throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details.");
}
// Get optional map size argument (default 192)
uint mapSize = 192;
if (args.Has("autostart-size"))
{
CStr size = args.Get("autostart-size");
mapSize = size.ToUInt();
}
Script::SetProperty(rq, settings, "Size", mapSize); // Random map size (in patches)
// Get optional number of players (default 2)
size_t numPlayers = 2;
if (args.Has("autostart-players"))
{
CStr num = args.Get("autostart-players");
numPlayers = num.ToUInt();
}
// Set up player data
for (size_t i = 0; i < numPlayers; ++i)
{
JS::RootedValue player(rq.cx);
// We could load player_defaults.json here, but that would complicate the logic
// even more and autostart is only intended for developers anyway
Script::CreateObject(rq, &player, "Civ", "athen");
Script::SetPropertyInt(rq, playerData, i, player);
}
mapType = "random";
}
else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// Initialize general settings from the map data so some values
// (e.g. name of map) are always present, even when autostart is
// partially configured
CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml");
Script::ParseJSON(rq, mapSettingsJSON, &settings);
// Initialize the playerData array being modified by autostart
// with the real map data, so sensible values are present:
Script::GetProperty(rq, settings, "PlayerData", &playerData);
if (mapDirectory == L"scenarios")
mapType = "scenario";
else
mapType = "skirmish";
}
else
{
LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory));
throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types.");
}
Script::SetProperty(rq, attrs, "mapType", mapType);
Script::SetProperty(rq, attrs, "map", "maps/" + autoStartName);
Script::SetProperty(rq, settings, "mapType", mapType);
Script::SetProperty(rq, settings, "CheatsEnabled", true);
// The seed is used for both random map generation and simulation
u32 seed = 0;
if (args.Has("autostart-seed"))
{
CStr seedArg = args.Get("autostart-seed");
if (seedArg == "-1")
seed = rand();
else
seed = seedArg.ToULong();
}
Script::SetProperty(rq, settings, "Seed", seed);
// Set seed for AIs
u32 aiseed = 0;
if (args.Has("autostart-aiseed"))
{
CStr seedArg = args.Get("autostart-aiseed");
if (seedArg == "-1")
aiseed = rand();
else
aiseed = seedArg.ToULong();
}
Script::SetProperty(rq, settings, "AISeed", aiseed);
// Set player data for AIs
// attrs.settings = { PlayerData: [ { AI: ... }, ... ] }
// or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set
int offset = 1;
JS::RootedValue player(rq.cx);
if (Script::GetPropertyInt(rq, playerData, 0, &player) && player.isNull())
offset = 0;
// Set teams
if (args.Has("autostart-team"))
{
std::vector civArgs = args.GetMultiple("autostart-team");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue currentPlayer(rq.cx);
if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined())
{
if (mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID);
continue;
}
Script::CreateObject(rq, ¤tPlayer);
}
int teamID = civArgs[i].AfterFirst(":").ToInt() - 1;
Script::SetProperty(rq, currentPlayer, "Team", teamID);
Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer);
}
}
int ceasefire = 0;
if (args.Has("autostart-ceasefire"))
ceasefire = args.Get("autostart-ceasefire").ToInt();
Script::SetProperty(rq, settings, "Ceasefire", ceasefire);
if (args.Has("autostart-ai"))
{
std::vector aiArgs = args.GetMultiple("autostart-ai");
for (size_t i = 0; i < aiArgs.size(); ++i)
{
int playerID = aiArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue currentPlayer(rq.cx);
if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined())
{
if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID);
continue;
}
Script::CreateObject(rq, ¤tPlayer);
}
Script::SetProperty(rq, currentPlayer, "AI", aiArgs[i].AfterFirst(":"));
Script::SetProperty(rq, currentPlayer, "AIDiff", 3);
Script::SetProperty(rq, currentPlayer, "AIBehavior", "balanced");
Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer);
}
}
// Set AI difficulty
if (args.Has("autostart-aidiff"))
{
std::vector civArgs = args.GetMultiple("autostart-aidiff");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue currentPlayer(rq.cx);
if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined())
{
if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID);
continue;
}
Script::CreateObject(rq, ¤tPlayer);
}
Script::SetProperty(rq, currentPlayer, "AIDiff", civArgs[i].AfterFirst(":").ToInt());
Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer);
}
}
// Set player data for Civs
if (args.Has("autostart-civ"))
{
if (mapDirectory != L"scenarios")
{
std::vector civArgs = args.GetMultiple("autostart-civ");
for (size_t i = 0; i < civArgs.size(); ++i)
{
int playerID = civArgs[i].BeforeFirst(":").ToInt();
// Instead of overwriting existing player data, modify the array
JS::RootedValue currentPlayer(rq.cx);
if (!Script::GetPropertyInt(rq, playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined())
{
if (mapDirectory == L"skirmishes")
{
// playerID is certainly bigger than this map player number
LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID);
continue;
}
Script::CreateObject(rq, ¤tPlayer);
}
Script::SetProperty(rq, currentPlayer, "Civ", civArgs[i].AfterFirst(":"));
Script::SetPropertyInt(rq, playerData, playerID-offset, currentPlayer);
}
}
else
LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios");
}
// Add player data to map settings
Script::SetProperty(rq, settings, "PlayerData", playerData);
// Add map settings to game attributes
Script::SetProperty(rq, attrs, "settings", settings);
// Get optional playername
CStrW userName = L"anonymous";
if (args.Has("autostart-playername"))
userName = args.Get("autostart-playername").FromUTF8();
// Add additional scripts to the TriggerScripts property
std::vector triggerScriptsVector;
JS::RootedValue triggerScripts(rq.cx);
if (Script::HasProperty(rq, settings, "TriggerScripts"))
{
Script::GetProperty(rq, settings, "TriggerScripts", &triggerScripts);
Script::FromJSVal(rq, triggerScripts, triggerScriptsVector);
}
if (!CRenderer::IsInitialised())
{
CStr nonVisualScript = "scripts/NonVisualTrigger.js";
triggerScriptsVector.push_back(nonVisualScript.FromUTF8());
}
std::vector victoryConditions(1, "conquest");
if (args.Has("autostart-victory"))
victoryConditions = args.GetMultiple("autostart-victory");
if (victoryConditions.size() == 1 && victoryConditions[0] == "endless")
victoryConditions.clear();
Script::SetProperty(rq, settings, "VictoryConditions", victoryConditions);
for (const CStr& victory : victoryConditions)
{
JS::RootedValue scriptData(rq.cx);
JS::RootedValue data(rq.cx);
JS::RootedValue victoryScripts(rq.cx);
CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json";
Script::ReadJSONFile(rq, scriptPath, &scriptData);
if (!scriptData.isUndefined() && Script::GetProperty(rq, scriptData, "Data", &data) && !data.isUndefined()
&& Script::GetProperty(rq, data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined())
{
std::vector victoryScriptsVector;
Script::FromJSVal(rq, victoryScripts, victoryScriptsVector);
triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end());
}
else
{
LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath));
throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details.");
}
}
Script::ToJSVal(rq, &triggerScripts, triggerScriptsVector);
Script::SetProperty(rq, settings, "TriggerScripts", triggerScripts);
int wonderDuration = 10;
if (args.Has("autostart-wonderduration"))
wonderDuration = args.Get("autostart-wonderduration").ToInt();
Script::SetProperty(rq, settings, "WonderDuration", wonderDuration);
int relicDuration = 10;
if (args.Has("autostart-relicduration"))
relicDuration = args.Get("autostart-relicduration").ToInt();
Script::SetProperty(rq, settings, "RelicDuration", relicDuration);
int relicCount = 2;
if (args.Has("autostart-reliccount"))
relicCount = args.Get("autostart-reliccount").ToInt();
Script::SetProperty(rq, settings, "RelicCount", relicCount);
if (args.Has("autostart-host"))
{
InitPsAutostart(true, attrs);
size_t maxPlayers = 2;
if (args.Has("autostart-host-players"))
maxPlayers = args.Get("autostart-host-players").ToUInt();
// Generate a secret to identify the host client.
std::string secret = ps_generate_guid();
g_NetServer = new CNetServer(false, maxPlayers);
g_NetServer->SetControllerSecret(secret);
g_NetServer->UpdateInitAttributes(&attrs, scriptInterface);
bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT);
ENSURE(ok);
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(userName);
g_NetClient->SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false);
g_NetClient->SetControllerSecret(secret);
g_NetClient->SetupConnection(nullptr);
}
else if (args.Has("autostart-client"))
{
InitPsAutostart(true, attrs);
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(userName);
CStr ip = args.Get("autostart-client");
if (ip.empty())
ip = "127.0.0.1";
g_NetClient->SetupServerData(ip, PS_DEFAULT_PORT, false);
ENSURE(g_NetClient->SetupConnection(nullptr));
}
else
{
g_Game->SetPlayerID(args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1);
g_Game->StartGame(&attrs, "");
if (CRenderer::IsInitialised())
{
InitPsAutostart(false, attrs);
}
else
{
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();
ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK);
}
}
return true;
}
bool AutostartVisualReplay(const std::string& replayFile)
{
if (!FileExists(OsPath(replayFile)))
return false;
g_Game = new CGame(false);
g_Game->SetPlayerID(-1);
g_Game->StartVisualReplay(replayFile);
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue attrs(rq.cx, g_Game->GetSimulation2()->GetInitAttributes());
InitPsAutostart(false, attrs);
return true;
}
void CancelLoad(const CStrW& message)
{
std::shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface();
ScriptRequest rq(pScriptInterface);
JS::RootedValue global(rq.cx, rq.globalValue());
LDR_Cancel();
if (g_GUI &&
g_GUI->GetPageCount() &&
Script::HasProperty(rq, global, "cancelOnLoadGameError"))
ScriptFunction::CallVoid(rq, global, "cancelOnLoadGameError", message);
}
bool InDevelopmentCopy()
{
if (!g_CheckedIfInDevelopmentCopy)
{
g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK);
g_CheckedIfInDevelopmentCopy = true;
}
return g_InDevelopmentCopy;
}
Index: ps/trunk/source/ps/Replay.cpp
===================================================================
--- ps/trunk/source/ps/Replay.cpp (revision 26268)
+++ ps/trunk/source/ps/Replay.cpp (revision 26269)
@@ -1,355 +1,347 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "Replay.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/timer.h"
#include "lib/file/file_system.h"
#include "lib/res/h_mgr.h"
#include "lib/tex/tex.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "ps/GameSetup/Paths.h"
#include "ps/Loader.h"
#include "ps/Mod.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
#include "ps/Pyrogenesis.h"
#include "ps/Mod.h"
#include "ps/Util.h"
#include "ps/VisualReplay.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRequest.h"
#include "scriptinterface/ScriptStats.h"
#include "scriptinterface/JSON.h"
#include "simulation2/components/ICmpGuiInterface.h"
#include "simulation2/helpers/Player.h"
#include "simulation2/helpers/SimulationCommand.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/CmpPtr.h"
#include
#include
/**
* Number of turns between two saved profiler snapshots.
* Keep in sync with source/tools/replayprofile/graph.js
*/
static const int PROFILE_TURN_INTERVAL = 20;
CReplayLogger::CReplayLogger(const ScriptInterface& scriptInterface) :
m_ScriptInterface(scriptInterface), m_Stream(NULL)
{
}
CReplayLogger::~CReplayLogger()
{
delete m_Stream;
}
void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
{
ScriptRequest rq(m_ScriptInterface);
// Add timestamp, since the file-modification-date can change
Script::SetProperty(rq, attribs, "timestamp", (double)std::time(nullptr));
// Add engine version and currently loaded mods for sanity checks when replaying
Script::SetProperty(rq, attribs, "engine_version", engine_version);
JS::RootedValue mods(rq.cx);
Script::ToJSVal(rq, &mods, g_Mods.GetEnabledModsData());
Script::SetProperty(rq, attribs, "mods", mods);
m_Directory = createDateIndexSubdirectory(VisualReplay::GetDirectoryPath());
debug_printf("Writing replay to %s\n", m_Directory.string8().c_str());
m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt").c_str(), std::ofstream::out | std::ofstream::trunc);
*m_Stream << "start " << Script::StringifyJSON(rq, attribs, false) << "\n";
}
void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector& commands)
{
ScriptRequest rq(m_ScriptInterface);
*m_Stream << "turn " << n << " " << turnLength << "\n";
for (SimulationCommand& command : commands)
*m_Stream << "cmd " << command.player << " " << Script::StringifyJSON(rq, &command.data, false) << "\n";
*m_Stream << "end\n";
m_Stream->flush();
}
void CReplayLogger::Hash(const std::string& hash, bool quick)
{
if (quick)
*m_Stream << "hash-quick " << Hexify(hash) << "\n";
else
*m_Stream << "hash " << Hexify(hash) << "\n";
}
void CReplayLogger::SaveMetadata(const CSimulation2& simulation)
{
CmpPtr cmpGuiInterface(simulation, SYSTEM_ENTITY);
if (!cmpGuiInterface)
{
LOGERROR("Could not save replay metadata!");
return;
}
ScriptInterface& scriptInterface = simulation.GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue arg(rq.cx);
JS::RootedValue metadata(rq.cx);
cmpGuiInterface->ScriptCall(INVALID_PLAYER, L"GetReplayMetadata", arg, &metadata);
const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json";
CreateDirectories(fileName.Parent(), 0700);
std::ofstream stream (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc);
stream << Script::StringifyJSON(rq, &metadata, false);
stream.close();
debug_printf("Saved replay metadata to %s\n", fileName.string8().c_str());
}
OsPath CReplayLogger::GetDirectory() const
{
return m_Directory;
}
////////////////////////////////////////////////////////////////
CReplayPlayer::CReplayPlayer() :
m_Stream(NULL)
{
}
CReplayPlayer::~CReplayPlayer()
{
delete m_Stream;
}
void CReplayPlayer::Load(const OsPath& path)
{
ENSURE(!m_Stream);
m_Stream = new std::ifstream(OsString(path).c_str());
ENSURE(m_Stream->good());
}
namespace
{
CStr ModListToString(const std::vector& list)
{
CStr text;
for (const Mod::ModData* data : list)
text += data->m_Pathname + " (" + data->m_Version + ")\n";
return text;
}
void CheckReplayMods(const std::vector& replayMods)
{
std::vector replayData;
replayData.reserve(replayMods.size());
for (const Mod::ModData& data : replayMods)
replayData.push_back(&data);
if (!Mod::AreModsPlayCompatible(g_Mods.GetEnabledModsData(), replayData))
LOGWARNING("Incompatible replay mods detected.\nThe mods of the replay are:\n%s\nThese mods are enabled:\n%s",
ModListToString(replayData), ModListToString(g_Mods.GetEnabledModsData()));
}
} // anonymous namespace
void CReplayPlayer::Replay(const bool serializationtest, const int rejointestturn, const bool ooslog, const bool testHashFull, const bool testHashQuick)
{
ENSURE(m_Stream);
new CProfileViewer;
new CProfileManager;
g_ScriptStatsTable = new CScriptStatsTable;
g_ProfileViewer.AddRootTable(g_ScriptStatsTable);
const int contextSize = 384 * 1024 * 1024;
const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024;
g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger);
std::vector commands;
u32 turn = 0;
u32 turnLength = 0;
{
std::string type;
while ((*m_Stream >> type).good())
{
if (type == "start")
{
std::string attribsStr;
{
ScriptInterface scriptInterface("Engine", "Replay", g_ScriptContext);
ScriptRequest rq(scriptInterface);
std::getline(*m_Stream, attribsStr);
JS::RootedValue attribs(rq.cx);
if (!Script::ParseJSON(rq, attribsStr, &attribs))
{
LOGERROR("Error parsing JSON attributes: %s", attribsStr);
// TODO: do something cleverer than crashing.
ENSURE(false);
}
// Load the mods specified in the replay.
std::vector replayMods;
if (!Script::GetProperty(rq, attribs, "mods", replayMods))
{
LOGERROR("Could not get replay mod information.");
// TODO: do something cleverer than crashing.
ENSURE(false);
}
std::vector mods;
for (const Mod::ModData& data : replayMods)
mods.emplace_back(data.m_Pathname);
// Ignore the return value, we check below.
g_Mods.UpdateAvailableMods(scriptInterface);
g_Mods.EnableMods(mods, false);
CheckReplayMods(replayMods);
MountMods(Paths(g_CmdLineArgs), g_Mods.GetEnabledMods());
}
g_Game = new CGame(false);
if (serializationtest)
g_Game->GetSimulation2()->EnableSerializationTest();
if (rejointestturn >= 0)
g_Game->GetSimulation2()->EnableRejoinTest(rejointestturn);
if (ooslog)
g_Game->GetSimulation2()->EnableOOSLog();
- // Need some stuff for terrain movement costs:
- // (TODO: this ought to be independent of any graphics code)
- new CTerrainTextureManager;
- g_TexMan.LoadTerrainTextures();
-
// Initialise h_mgr so it doesn't crash when emitting sounds
h_mgr_init();
ScriptRequest rq(g_Game->GetSimulation2()->GetScriptInterface());
JS::RootedValue attribs(rq.cx);
ENSURE(Script::ParseJSON(rq, attribsStr, &attribs));
g_Game->StartGame(&attribs, "");
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();
PSRETURN ret = g_Game->ReallyStartGame();
ENSURE(ret == PSRETURN_OK);
}
else if (type == "turn")
{
*m_Stream >> turn >> turnLength;
debug_printf("Turn %u (%u)...\n", turn, turnLength);
}
else if (type == "cmd")
{
player_id_t player;
*m_Stream >> player;
std::string line;
std::getline(*m_Stream, line);
ScriptRequest rq(g_Game->GetSimulation2()->GetScriptInterface());
JS::RootedValue data(rq.cx);
Script::ParseJSON(rq, line, &data);
Script::FreezeObject(rq, data, true);
commands.emplace_back(SimulationCommand(player, rq.cx, data));
}
else if (type == "hash" || type == "hash-quick")
{
std::string replayHash;
*m_Stream >> replayHash;
TestHash(type, replayHash, testHashFull, testHashQuick);
}
else if (type == "end")
{
{
g_Profiler2.RecordFrameStart();
PROFILE2("frame");
g_Profiler2.IncrementFrameNumber();
PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
g_Game->GetSimulation2()->Update(turnLength, commands);
commands.clear();
}
g_Profiler.Frame();
if (turn % PROFILE_TURN_INTERVAL == 0)
g_ProfileViewer.SaveToFile();
}
else
debug_printf("Unrecognised replay token %s\n", type.c_str());
}
}
SAFE_DELETE(m_Stream);
g_Profiler2.SaveToFile();
std::string hash;
bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false);
ENSURE(ok);
debug_printf("# Final state: %s\n", Hexify(hash).c_str());
timer_DisplayClientTotals();
SAFE_DELETE(g_Game);
// Must be explicitly destructed here to avoid callbacks from the JSAPI trying to use g_Profiler2 when
// it's already destructed.
g_ScriptContext.reset();
- // Clean up
- delete &g_TexMan;
-
delete &g_Profiler;
delete &g_ProfileViewer;
SAFE_DELETE(g_ScriptStatsTable);
}
void CReplayPlayer::TestHash(const std::string& hashType, const std::string& replayHash, const bool testHashFull, const bool testHashQuick)
{
bool quick = (hashType == "hash-quick");
if ((quick && !testHashQuick) || (!quick && !testHashFull))
return;
std::string hash;
ENSURE(g_Game->GetSimulation2()->ComputeStateHash(hash, quick));
std::string hexHash = Hexify(hash);
if (hexHash == replayHash)
debug_printf("%s ok (%s)\n", hashType.c_str(), hexHash.c_str());
else
debug_printf("%s MISMATCH (%s != %s)\n", hashType.c_str(), hexHash.c_str(), replayHash.c_str());
}
Index: ps/trunk/source/renderer/Renderer.cpp
===================================================================
--- ps/trunk/source/renderer/Renderer.cpp (revision 26268)
+++ ps/trunk/source/renderer/Renderer.cpp (revision 26269)
@@ -1,834 +1,836 @@
/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "Renderer.h"
#include "graphics/Canvas2D.h"
#include "graphics/CinemaManager.h"
#include "graphics/GameView.h"
#include "graphics/LightEnv.h"
#include "graphics/ModelDef.h"
#include "graphics/TerrainTextureManager.h"
#include "i18n/L10n.h"
#include "lib/allocators/shared_ptr.h"
#include "lib/ogl.h"
#include "lib/tex/tex.h"
#include "gui/GUIManager.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/CStrInternStatic.h"
#include "ps/Game.h"
#include "ps/GameSetup/Config.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/Globals.h"
#include "ps/Loader.h"
#include "ps/Profile.h"
#include "ps/Filesystem.h"
#include "ps/World.h"
#include "ps/ProfileViewer.h"
#include "graphics/Camera.h"
#include "graphics/FontManager.h"
#include "graphics/ShaderManager.h"
#include "graphics/Terrain.h"
#include "graphics/Texture.h"
#include "graphics/TextureManager.h"
#include "ps/Util.h"
#include "ps/VideoMode.h"
#include "renderer/backend/gl/Device.h"
#include "renderer/DebugRenderer.h"
#include "renderer/PostprocManager.h"
#include "renderer/RenderingOptions.h"
#include "renderer/RenderModifiers.h"
#include "renderer/SceneRenderer.h"
#include "renderer/TimeManager.h"
#include "renderer/VertexBufferManager.h"
#include "tools/atlas/GameInterface/GameLoop.h"
#include "tools/atlas/GameInterface/View.h"
#include
namespace
{
size_t g_NextScreenShotNumber = 0;
///////////////////////////////////////////////////////////////////////////////////
// CRendererStatsTable - Profile display of rendering stats
/**
* Class CRendererStatsTable: Implementation of AbstractProfileTable to
* display the renderer stats in-game.
*
* Accesses CRenderer::m_Stats by keeping the reference passed to the
* constructor.
*/
class CRendererStatsTable : public AbstractProfileTable
{
NONCOPYABLE(CRendererStatsTable);
public:
CRendererStatsTable(const CRenderer::Stats& st);
// Implementation of AbstractProfileTable interface
CStr GetName();
CStr GetTitle();
size_t GetNumberRows();
const std::vector& GetColumns();
CStr GetCellText(size_t row, size_t col);
AbstractProfileTable* GetChild(size_t row);
private:
/// Reference to the renderer singleton's stats
const CRenderer::Stats& Stats;
/// Column descriptions
std::vector columnDescriptions;
enum
{
Row_DrawCalls = 0,
Row_TerrainTris,
Row_WaterTris,
Row_ModelTris,
Row_OverlayTris,
Row_BlendSplats,
Row_Particles,
Row_VBReserved,
Row_VBAllocated,
Row_TextureMemory,
Row_ShadersLoaded,
// Must be last to count number of rows
NumberRows
};
};
// Construction
CRendererStatsTable::CRendererStatsTable(const CRenderer::Stats& st)
: Stats(st)
{
columnDescriptions.push_back(ProfileColumn("Name", 230));
columnDescriptions.push_back(ProfileColumn("Value", 100));
}
// Implementation of AbstractProfileTable interface
CStr CRendererStatsTable::GetName()
{
return "renderer";
}
CStr CRendererStatsTable::GetTitle()
{
return "Renderer statistics";
}
size_t CRendererStatsTable::GetNumberRows()
{
return NumberRows;
}
const std::vector& CRendererStatsTable::GetColumns()
{
return columnDescriptions;
}
CStr CRendererStatsTable::GetCellText(size_t row, size_t col)
{
char buf[256];
switch(row)
{
case Row_DrawCalls:
if (col == 0)
return "# draw calls";
sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_DrawCalls);
return buf;
case Row_TerrainTris:
if (col == 0)
return "# terrain tris";
sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_TerrainTris);
return buf;
case Row_WaterTris:
if (col == 0)
return "# water tris";
sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_WaterTris);
return buf;
case Row_ModelTris:
if (col == 0)
return "# model tris";
sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_ModelTris);
return buf;
case Row_OverlayTris:
if (col == 0)
return "# overlay tris";
sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_OverlayTris);
return buf;
case Row_BlendSplats:
if (col == 0)
return "# blend splats";
sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_BlendSplats);
return buf;
case Row_Particles:
if (col == 0)
return "# particles";
sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_Particles);
return buf;
case Row_VBReserved:
if (col == 0)
return "VB reserved";
sprintf_s(buf, sizeof(buf), "%lu kB", (unsigned long)g_VBMan.GetBytesReserved() / 1024);
return buf;
case Row_VBAllocated:
if (col == 0)
return "VB allocated";
sprintf_s(buf, sizeof(buf), "%lu kB", (unsigned long)g_VBMan.GetBytesAllocated() / 1024);
return buf;
case Row_TextureMemory:
if (col == 0)
return "textures uploaded";
sprintf_s(buf, sizeof(buf), "%lu kB", (unsigned long)g_Renderer.GetTextureManager().GetBytesUploaded() / 1024);
return buf;
case Row_ShadersLoaded:
if (col == 0)
return "shader effects loaded";
sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)g_Renderer.GetShaderManager().GetNumEffectsLoaded());
return buf;
default:
return "???";
}
}
AbstractProfileTable* CRendererStatsTable::GetChild(size_t UNUSED(row))
{
return 0;
}
} // anonymous namespace
///////////////////////////////////////////////////////////////////////////////////
// CRenderer implementation
/**
* Struct CRendererInternals: Truly hide data that is supposed to be hidden
* in this structure so it won't even appear in header files.
*/
class CRenderer::Internals
{
NONCOPYABLE(Internals);
public:
/// true if CRenderer::Open has been called
bool IsOpen;
/// true if shaders need to be reloaded
bool ShadersDirty;
/// Table to display renderer stats in-game via profile system
CRendererStatsTable profileTable;
/// Shader manager
CShaderManager shaderManager;
/// Texture manager
CTextureManager textureManager;
/// Time manager
CTimeManager timeManager;
/// Postprocessing effect manager
CPostprocManager postprocManager;
CSceneRenderer sceneRenderer;
CDebugRenderer debugRenderer;
CFontManager fontManager;
std::unique_ptr deviceCommandContext;
Internals() :
IsOpen(false), ShadersDirty(true), profileTable(g_Renderer.m_Stats), textureManager(g_VFS, false, false)
{
}
};
CRenderer::CRenderer()
{
TIMER(L"InitRenderer");
m = std::make_unique();
g_ProfileViewer.AddRootTable(&m->profileTable);
m_Width = 0;
m_Height = 0;
m_Stats.Reset();
// Create terrain related stuff.
new CTerrainTextureManager;
Open(g_xres, g_yres);
// Setup lighting environment. Since the Renderer accesses the
// lighting environment through a pointer, this has to be done before
// the first Frame.
GetSceneRenderer().SetLightEnv(&g_LightEnv);
// I haven't seen the camera affecting GUI rendering and such, but the
// viewport has to be updated according to the video mode
SViewPort vp;
vp.m_X = 0;
vp.m_Y = 0;
vp.m_Width = g_xres;
vp.m_Height = g_yres;
SetViewport(vp);
ModelDefActivateFastImpl();
ColorActivateFastImpl();
ModelRenderer::Init();
}
CRenderer::~CRenderer()
{
+ delete &g_TexMan;
+
// We no longer UnloadWaterTextures here -
// that is the responsibility of the module that asked for
// them to be loaded (i.e. CGameView).
m.reset();
}
void CRenderer::EnumCaps()
{
// assume support for nothing
m_Caps.m_ARBProgram = false;
m_Caps.m_ARBProgramShadow = false;
m_Caps.m_VertexShader = false;
m_Caps.m_FragmentShader = false;
m_Caps.m_Shadows = false;
m_Caps.m_PrettyWater = false;
// now start querying extensions
if (0 == ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL))
{
m_Caps.m_ARBProgram = true;
if (ogl_HaveExtension("GL_ARB_fragment_program_shadow"))
m_Caps.m_ARBProgramShadow = true;
}
// GLSL shaders are in core since GL2.0.
if (ogl_HaveVersion(2, 0))
m_Caps.m_VertexShader = m_Caps.m_FragmentShader = true;
#if CONFIG2_GLES
m_Caps.m_Shadows = true;
#else
if (0 == ogl_HaveExtensions(0, "GL_ARB_shadow", "GL_ARB_depth_texture", NULL))
{
if (ogl_max_tex_units >= 4)
m_Caps.m_Shadows = true;
}
#endif
#if CONFIG2_GLES
m_Caps.m_PrettyWater = true;
#else
if (m_Caps.m_VertexShader && m_Caps.m_FragmentShader)
m_Caps.m_PrettyWater = true;
#endif
}
void CRenderer::ReloadShaders()
{
ENSURE(m->IsOpen);
m->sceneRenderer.ReloadShaders();
m->ShadersDirty = false;
}
bool CRenderer::Open(int width, int height)
{
m->IsOpen = true;
// Must query card capabilities before creating renderers that depend
// on card capabilities.
EnumCaps();
// Dimensions
m_Width = width;
m_Height = height;
// Validate the currently selected render path
SetRenderPath(g_RenderingOptions.GetRenderPath());
m->deviceCommandContext = Renderer::Backend::GL::CDeviceCommandContext::Create();
if (m->postprocManager.IsEnabled())
m->postprocManager.Initialize();
m->sceneRenderer.Initialize();
return true;
}
void CRenderer::Resize(int width, int height)
{
m_Width = width;
m_Height = height;
m->postprocManager.Resize();
m->sceneRenderer.Resize(width, height);
}
void CRenderer::SetRenderPath(RenderPath rp)
{
if (!m->IsOpen)
{
// Delay until Open() is called.
return;
}
// Renderer has been opened, so validate the selected renderpath
if (rp == RenderPath::DEFAULT)
{
if (m_Caps.m_ARBProgram || (m_Caps.m_VertexShader && m_Caps.m_FragmentShader && g_VideoMode.GetBackend() != CVideoMode::Backend::GL_ARB))
rp = RenderPath::SHADER;
else
rp = RenderPath::FIXED;
}
if (rp == RenderPath::SHADER)
{
if (!(m_Caps.m_ARBProgram || (m_Caps.m_VertexShader && m_Caps.m_FragmentShader && g_VideoMode.GetBackend() != CVideoMode::Backend::GL_ARB)))
{
LOGWARNING("Falling back to fixed function\n");
rp = RenderPath::FIXED;
}
}
// TODO: remove this once capabilities have been properly extracted and the above checks have been moved elsewhere.
g_RenderingOptions.m_RenderPath = rp;
MakeShadersDirty();
}
bool CRenderer::ShouldRender() const
{
return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen());
}
void CRenderer::RenderFrame(const bool needsPresent)
{
// Do not render if not focused while in fullscreen or minimised,
// as that triggers a difficult-to-reproduce crash on some graphic cards.
if (!ShouldRender())
return;
if (m_ShouldPreloadResourcesBeforeNextFrame)
{
m_ShouldPreloadResourcesBeforeNextFrame = false;
// We don't meed to render logger for the preload.
RenderFrameImpl(true, false);
}
if (m_ScreenShotType == ScreenShotType::BIG)
{
RenderBigScreenShot(needsPresent);
}
else
{
if (m_ScreenShotType == ScreenShotType::DEFAULT)
RenderScreenShot();
else
RenderFrameImpl(true, true);
m->deviceCommandContext->Flush();
if (needsPresent)
g_VideoMode.GetBackendDevice()->Present();
}
}
void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger)
{
PROFILE3("render");
g_Profiler2.RecordGPUFrameStart();
ogl_WarnIfError();
g_TexMan.UploadResourcesIfNeeded(m->deviceCommandContext.get());
// prepare before starting the renderer frame
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->BeginFrame();
if (g_Game)
m->sceneRenderer.SetSimulation(g_Game->GetSimulation2());
// start new frame
BeginFrame();
ogl_WarnIfError();
if (g_Game && g_Game->IsGameStarted())
{
g_Game->GetView()->Render();
ogl_WarnIfError();
}
m->sceneRenderer.RenderTextOverlays();
// If we're in Atlas game view, render special tools
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
{
g_AtlasGameLoop->view->DrawCinemaPathTool();
ogl_WarnIfError();
}
if (g_Game && g_Game->IsGameStarted())
{
g_Game->GetView()->GetCinema()->Render();
ogl_WarnIfError();
}
glDisable(GL_DEPTH_TEST);
if (renderGUI)
{
OGL_SCOPED_DEBUG_GROUP("Draw GUI");
// All GUI elements are drawn in Z order to render semi-transparent
// objects correctly.
g_GUI->Draw();
ogl_WarnIfError();
}
// If we're in Atlas game view, render special overlays (e.g. editor bandbox).
if (g_AtlasGameLoop && g_AtlasGameLoop->view)
{
CCanvas2D canvas;
g_AtlasGameLoop->view->DrawOverlays(canvas);
ogl_WarnIfError();
}
g_Console->Render();
ogl_WarnIfError();
if (renderLogger)
{
g_Logger->Render();
ogl_WarnIfError();
}
// Profile information
g_ProfileViewer.RenderProfile();
ogl_WarnIfError();
glEnable(GL_DEPTH_TEST);
EndFrame();
const Stats& stats = GetStats();
PROFILE2_ATTR("draw calls: %zu", stats.m_DrawCalls);
PROFILE2_ATTR("terrain tris: %zu", stats.m_TerrainTris);
PROFILE2_ATTR("water tris: %zu", stats.m_WaterTris);
PROFILE2_ATTR("model tris: %zu", stats.m_ModelTris);
PROFILE2_ATTR("overlay tris: %zu", stats.m_OverlayTris);
PROFILE2_ATTR("blend splats: %zu", stats.m_BlendSplats);
PROFILE2_ATTR("particles: %zu", stats.m_Particles);
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameEnd();
ogl_WarnIfError();
}
void CRenderer::RenderScreenShot()
{
m_ScreenShotType = ScreenShotType::NONE;
// get next available numbered filename
// note: %04d -> always 4 digits, so sorting by filename works correctly.
const VfsPath filenameFormat(L"screenshots/screenshot%04d.png");
VfsPath filename;
vfs::NextNumberedFilename(g_VFS, filenameFormat, g_NextScreenShotNumber, filename);
const size_t w = (size_t)g_xres, h = (size_t)g_yres;
const size_t bpp = 24;
GLenum fmt = GL_RGB;
int flags = TEX_BOTTOM_UP;
// Hide log messages and re-render
RenderFrameImpl(true, false);
const size_t img_size = w * h * bpp / 8;
const size_t hdr_size = tex_hdr_size(filename);
std::shared_ptr buf;
AllocateAligned(buf, hdr_size + img_size, maxSectorSize);
GLvoid* img = buf.get() + hdr_size;
Tex t;
if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0)
return;
glReadPixels(0, 0, (GLsizei)w, (GLsizei)h, fmt, GL_UNSIGNED_BYTE, img);
if (tex_write(&t, filename) == INFO::OK)
{
OsPath realPath;
g_VFS->GetRealPath(filename, realPath);
LOGMESSAGERENDER(g_L10n.Translate("Screenshot written to '%s'"), realPath.string8());
debug_printf(
CStr(g_L10n.Translate("Screenshot written to '%s'") + "\n").c_str(),
realPath.string8().c_str());
}
else
LOGERROR("Error writing screenshot to '%s'", filename.string8());
}
void CRenderer::RenderBigScreenShot(const bool needsPresent)
{
m_ScreenShotType = ScreenShotType::NONE;
// If the game hasn't started yet then use WriteScreenshot to generate the image.
if (!g_Game)
return RenderScreenShot();
int tiles = 4, tileWidth = 256, tileHeight = 256;
CFG_GET_VAL("screenshot.tiles", tiles);
CFG_GET_VAL("screenshot.tilewidth", tileWidth);
CFG_GET_VAL("screenshot.tileheight", tileHeight);
if (tiles <= 0 || tileWidth <= 0 || tileHeight <= 0 || tileWidth * tiles % 4 != 0 || tileHeight * tiles % 4 != 0)
{
LOGWARNING("Invalid big screenshot size: tiles=%d tileWidth=%d tileHeight=%d", tiles, tileWidth, tileHeight);
return;
}
// get next available numbered filename
// note: %04d -> always 4 digits, so sorting by filename works correctly.
const VfsPath filenameFormat(L"screenshots/screenshot%04d.bmp");
VfsPath filename;
vfs::NextNumberedFilename(g_VFS, filenameFormat, g_NextScreenShotNumber, filename);
// Slightly ugly and inflexible: Always draw 640*480 tiles onto the screen, and
// hope the screen is actually large enough for that.
ENSURE(g_xres >= tileWidth && g_yres >= tileHeight);
const int imageWidth = tileWidth * tiles, imageHeight = tileHeight * tiles;
const int bpp = 24;
// we want writing BMP to be as fast as possible,
// so read data from OpenGL in BMP format to obviate conversion.
#if CONFIG2_GLES // GLES doesn't support BGR
const GLenum fmt = GL_RGB;
const int flags = TEX_BOTTOM_UP;
#else
const GLenum fmt = GL_BGR;
const int flags = TEX_BOTTOM_UP | TEX_BGR;
#endif
const size_t imageSize = imageWidth * imageHeight * bpp / 8;
const size_t tileSize = tileWidth * tileHeight * bpp / 8;
const size_t headerSize = tex_hdr_size(filename);
void* tileData = malloc(tileSize);
if (!tileData)
{
WARN_IF_ERR(ERR::NO_MEM);
return;
}
std::shared_ptr imageBuffer;
AllocateAligned(imageBuffer, headerSize + imageSize, maxSectorSize);
Tex t;
GLvoid* img = imageBuffer.get() + headerSize;
if (t.wrap(imageWidth, imageHeight, bpp, flags, imageBuffer, headerSize) < 0)
{
free(tileData);
return;
}
ogl_WarnIfError();
CCamera oldCamera = *g_Game->GetView()->GetCamera();
// Resize various things so that the sizes and aspect ratios are correct
{
g_Renderer.Resize(tileWidth, tileHeight);
SViewPort vp = { 0, 0, tileWidth, tileHeight };
g_Game->GetView()->SetViewport(vp);
}
// Render each tile
CMatrix3D projection;
projection.SetIdentity();
const float aspectRatio = 1.0f * tileWidth / tileHeight;
for (int tileY = 0; tileY < tiles; ++tileY)
{
for (int tileX = 0; tileX < tiles; ++tileX)
{
// Adjust the camera to render the appropriate region
if (oldCamera.GetProjectionType() == CCamera::ProjectionType::PERSPECTIVE)
{
projection.SetPerspectiveTile(oldCamera.GetFOV(), aspectRatio, oldCamera.GetNearPlane(), oldCamera.GetFarPlane(), tiles, tileX, tileY);
}
g_Game->GetView()->GetCamera()->SetProjection(projection);
RenderFrameImpl(false, false);
// Copy the tile pixels into the main image
glReadPixels(0, 0, tileWidth, tileHeight, fmt, GL_UNSIGNED_BYTE, tileData);
for (int y = 0; y < tileHeight; ++y)
{
void* dest = static_cast(img) + ((tileY * tileHeight + y) * imageWidth + (tileX * tileWidth)) * bpp / 8;
void* src = static_cast(tileData) + y * tileWidth * bpp / 8;
memcpy(dest, src, tileWidth * bpp / 8);
}
m->deviceCommandContext->Flush();
if (needsPresent)
g_VideoMode.GetBackendDevice()->Present();
}
}
// Restore the viewport settings
{
g_Renderer.Resize(g_xres, g_yres);
SViewPort vp = { 0, 0, g_xres, g_yres };
g_Game->GetView()->SetViewport(vp);
g_Game->GetView()->GetCamera()->SetProjectionFromCamera(oldCamera);
}
if (tex_write(&t, filename) == INFO::OK)
{
OsPath realPath;
g_VFS->GetRealPath(filename, realPath);
LOGMESSAGERENDER(g_L10n.Translate("Screenshot written to '%s'"), realPath.string8());
debug_printf(
CStr(g_L10n.Translate("Screenshot written to '%s'") + "\n").c_str(),
realPath.string8().c_str());
}
else
LOGERROR("Error writing screenshot to '%s'", filename.string8());
free(tileData);
}
void CRenderer::BeginFrame()
{
PROFILE("begin frame");
// Zero out all the per-frame stats.
m_Stats.Reset();
if (m->ShadersDirty)
ReloadShaders();
m->sceneRenderer.BeginFrame();
}
void CRenderer::EndFrame()
{
PROFILE3("end frame");
m->sceneRenderer.EndFrame();
BindTexture(0, 0);
}
void CRenderer::SetViewport(const SViewPort &vp)
{
m_Viewport = vp;
glViewport((GLint)vp.m_X,(GLint)vp.m_Y,(GLsizei)vp.m_Width,(GLsizei)vp.m_Height);
}
SViewPort CRenderer::GetViewport()
{
return m_Viewport;
}
void CRenderer::BindTexture(int unit, GLuint tex)
{
glActiveTextureARB(GL_TEXTURE0+unit);
glBindTexture(GL_TEXTURE_2D, tex);
}
void CRenderer::MakeShadersDirty()
{
m->ShadersDirty = true;
m->sceneRenderer.MakeShadersDirty();
}
CTextureManager& CRenderer::GetTextureManager()
{
return m->textureManager;
}
CShaderManager& CRenderer::GetShaderManager()
{
return m->shaderManager;
}
CTimeManager& CRenderer::GetTimeManager()
{
return m->timeManager;
}
CPostprocManager& CRenderer::GetPostprocManager()
{
return m->postprocManager;
}
CSceneRenderer& CRenderer::GetSceneRenderer()
{
return m->sceneRenderer;
}
CDebugRenderer& CRenderer::GetDebugRenderer()
{
return m->debugRenderer;
}
CFontManager& CRenderer::GetFontManager()
{
return m->fontManager;
}
void CRenderer::PreloadResourcesBeforeNextFrame()
{
m_ShouldPreloadResourcesBeforeNextFrame = true;
}
void CRenderer::MakeScreenShotOnNextFrame(ScreenShotType screenShotType)
{
m_ScreenShotType = screenShotType;
}
Renderer::Backend::GL::CDeviceCommandContext* CRenderer::GetDeviceCommandContext()
{
return m->deviceCommandContext.get();
}
Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.h
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotion.h (revision 26268)
+++ ps/trunk/source/simulation2/components/CCmpUnitMotion.h (revision 26269)
@@ -1,1896 +1,1891 @@
/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_CCMPUNITMOTION
#define INCLUDED_CCMPUNITMOTION
#include "simulation2/system/Component.h"
#include "ICmpUnitMotion.h"
#include "simulation2/components/CCmpUnitMotionManager.h"
#include "simulation2/components/ICmpObstruction.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpOwnership.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpPathfinder.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpValueModificationManager.h"
#include "simulation2/components/ICmpVisual.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Render.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/serialization/SerializedPathfinder.h"
#include "simulation2/serialization/SerializedTypes.h"
#include "graphics/Overlay.h"
#include "maths/FixedVector2D.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
#include "renderer/Scene.h"
#include
// NB: this implementation of ICmpUnitMotion is very tightly coupled with UnitMotionManager.
// As such, both are compiled in the same TU.
// For debugging; units will start going straight to the target
// instead of calling the pathfinder
#define DISABLE_PATHFINDER 0
namespace
{
/**
* Min/Max range to restrict short path queries to. (Larger ranges are (much) slower,
* smaller ranges might miss some legitimate routes around large obstacles.)
* NB: keep the max-range in sync with the vertex pathfinder "move the search space" heuristic.
*/
constexpr entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(12 * Pathfinding::NAVCELL_SIZE_INT);
constexpr entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(56 * Pathfinding::NAVCELL_SIZE_INT);
constexpr entity_pos_t SHORT_PATH_SEARCH_RANGE_INCREMENT = entity_pos_t::FromInt(4 * Pathfinding::NAVCELL_SIZE_INT);
constexpr u8 SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY = 1;
/**
* When using the short-pathfinder to rejoin a long-path waypoint, aim for a circle of this radius around the waypoint.
*/
constexpr entity_pos_t SHORT_PATH_LONG_WAYPOINT_RANGE = entity_pos_t::FromInt(4 * Pathfinding::NAVCELL_SIZE_INT);
/**
* Minimum distance to goal for a long path request
*/
constexpr entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(16 * Pathfinding::NAVCELL_SIZE_INT);
/**
* If we are this close to our target entity/point, then think about heading
* for it in a straight line instead of pathfinding.
*/
constexpr entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(24 * Pathfinding::NAVCELL_SIZE_INT);
/**
* To avoid recomputing paths too often, have some leeway for target range checks
* based on our distance to the target. Increase that incertainty by one navcell
* for every this many tiles of distance.
*/
constexpr entity_pos_t TARGET_UNCERTAINTY_MULTIPLIER = entity_pos_t::FromInt(8 * Pathfinding::NAVCELL_SIZE_INT);
/**
* When following a known imperfect path (i.e. a path that won't take us in range of our goal
* we still recompute a new path every N turn to adapt to moving targets (for example, ships that must pickup
* units may easily end up in this state, they still need to adjust to moving units).
* This is rather arbitrary and mostly for simplicity & optimisation (a better recomputing algorithm
* would not need this).
*/
constexpr u8 KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN = 12;
/**
* When we fail to move this many turns in a row, inform other components that the move will fail.
* Experimentally, this number needs to be somewhat high or moving groups of units will lead to stuck units.
* However, too high means units will look idle for a long time when they are failing to move.
* TODO: if UnitMotion could send differentiated "unreachable" and "currently stuck" failing messages,
* this could probably be lowered.
* TODO: when unit pushing is implemented, this number can probably be lowered.
*/
constexpr u8 MAX_FAILED_MOVEMENTS = 35;
/**
* When computing paths but failing to move, we want to occasionally alternate pathfinder systems
* to avoid getting stuck (the short pathfinder can unstuck the long-range one and vice-versa, depending).
*/
constexpr u8 ALTERNATE_PATH_TYPE_DELAY = 3;
constexpr u8 ALTERNATE_PATH_TYPE_EVERY = 6;
/**
* Units can occasionally get stuck near corners. The cause is a mismatch between CheckMovement and the short pathfinder.
* The problem is the short pathfinder finds an impassable path when units are right on an obstruction edge.
* Fixing this math mismatch is perhaps possible, but fixing it in UM is rather easy: just try backing up a bit
* and that will probably un-stuck the unit. This is the 'failed movement' turn on which to try that.
*/
constexpr u8 BACKUP_HACK_DELAY = 10;
/**
* After this many failed computations, start sending "VERY_OBSTRUCTED" messages instead.
* Should probably be larger than ALTERNATE_PATH_TYPE_DELAY.
*/
constexpr u8 VERY_OBSTRUCTED_THRESHOLD = 10;
const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1);
const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1);
} // anonymous namespace
class CCmpUnitMotion final : public ICmpUnitMotion
{
friend class CCmpUnitMotionManager;
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Create);
componentManager.SubscribeToMessageType(MT_Destroy);
componentManager.SubscribeToMessageType(MT_PathResult);
componentManager.SubscribeToMessageType(MT_OwnershipChanged);
componentManager.SubscribeToMessageType(MT_ValueModification);
componentManager.SubscribeToMessageType(MT_MovementObstructionChanged);
componentManager.SubscribeToMessageType(MT_Deserialized);
}
DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
bool m_DebugOverlayEnabled;
std::vector m_DebugOverlayLongPathLines;
std::vector m_DebugOverlayShortPathLines;
// Template state:
bool m_IsFormationController;
fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier, m_TemplateAcceleration;
pass_class_t m_PassClass;
std::string m_PassClassName;
// Dynamic state:
entity_pos_t m_Clearance;
// cached for efficiency
fixed m_WalkSpeed, m_RunMultiplier;
bool m_FacePointAfterMove;
// Whether the unit participates in pushing.
bool m_Pushing = false;
// Whether the unit blocks movement (& is blocked by movement blockers)
// Cached from ICmpObstruction.
bool m_BlockMovement = false;
// Internal counter used when recovering from obstructed movement.
// Most notably, increases the search range of the vertex pathfinder.
// See HandleObstructedMove() for more details.
u8 m_FailedMovements = 0;
// If > 0, PathingUpdateNeeded returns false always.
// This exists because the goal may be unreachable to the short/long pathfinder.
// In such cases, we would compute inacceptable paths and PathingUpdateNeeded would trigger every turn,
// which would be quite bad for performance.
// To avoid that, when we know the new path is imperfect, treat it as OK and follow it anyways.
// When reaching the end, we'll go through HandleObstructedMove and reset regardless.
// To still recompute now and then (the target may be moving), this is a countdown decremented on each frame.
u8 m_FollowKnownImperfectPathCountdown = 0;
struct Ticket {
u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none
enum Type {
SHORT_PATH,
LONG_PATH
} m_Type = SHORT_PATH; // Pick some default value to avoid UB.
void clear() { m_Ticket = 0; }
} m_ExpectedPathTicket;
struct MoveRequest {
enum Type {
NONE,
POINT,
ENTITY,
OFFSET
} m_Type = NONE;
entity_id_t m_Entity = INVALID_ENTITY;
CFixedVector2D m_Position;
entity_pos_t m_MinRange, m_MaxRange;
// For readability
CFixedVector2D GetOffset() const { return m_Position; };
MoveRequest() = default;
MoveRequest(CFixedVector2D pos, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(POINT), m_Position(pos), m_MinRange(minRange), m_MaxRange(maxRange) {};
MoveRequest(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(ENTITY), m_Entity(target), m_MinRange(minRange), m_MaxRange(maxRange) {};
MoveRequest(entity_id_t target, CFixedVector2D offset) : m_Type(OFFSET), m_Entity(target), m_Position(offset) {};
} m_MoveRequest;
// If this is not INVALID_ENTITY, the unit is a formation member.
entity_id_t m_FormationController = INVALID_ENTITY;
// If the entity moves, it will do so at m_WalkSpeed * m_SpeedMultiplier.
fixed m_SpeedMultiplier;
// This caches the resulting speed from m_WalkSpeed * m_SpeedMultiplier for convenience.
fixed m_Speed;
// Mean speed over the last turn.
fixed m_LastTurnSpeed;
// The speed achieved at the end of the current turn.
fixed m_CurrentSpeed;
fixed m_InstantTurnAngle;
fixed m_Acceleration;
// Currently active paths (storing waypoints in reverse order).
// The last item in each path is the point we're currently heading towards.
WaypointPath m_LongPath;
WaypointPath m_ShortPath;
static std::string GetSchema()
{
return
"Provides the unit with the ability to move around the world by itself."
""
"7.0"
"default"
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
""
"";
}
virtual void Init(const CParamNode& paramNode)
{
m_IsFormationController = paramNode.GetChild("FormationController").ToBool();
m_FacePointAfterMove = true;
m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed();
m_SpeedMultiplier = fixed::FromInt(1);
m_LastTurnSpeed = m_CurrentSpeed = fixed::Zero();
m_RunMultiplier = m_TemplateRunMultiplier = fixed::FromInt(1);
if (paramNode.GetChild("RunMultiplier").IsOk())
m_RunMultiplier = m_TemplateRunMultiplier = paramNode.GetChild("RunMultiplier").ToFixed();
m_InstantTurnAngle = paramNode.GetChild("InstantTurnAngle").ToFixed();
m_Acceleration = m_TemplateAcceleration = paramNode.GetChild("Acceleration").ToFixed();
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
{
m_PassClassName = paramNode.GetChild("PassabilityClass").ToString();
m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName);
m_Clearance = cmpPathfinder->GetClearance(m_PassClass);
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
{
cmpObstruction->SetUnitClearance(m_Clearance);
m_BlockMovement = cmpObstruction->GetBlockMovementFlag(true);
}
}
SetParticipateInPushing(!paramNode.GetChild("DisablePushing").IsOk() || !paramNode.GetChild("DisablePushing").ToBool());
m_DebugOverlayEnabled = false;
}
virtual void Deinit()
{
}
template
void SerializeCommon(S& serialize)
{
serialize.StringASCII("pass class", m_PassClassName, 0, 64);
serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket.m_Ticket);
Serializer(serialize, "ticket type", m_ExpectedPathTicket.m_Type, Ticket::Type::LONG_PATH);
serialize.NumberU8_Unbounded("failed movements", m_FailedMovements);
serialize.NumberU8_Unbounded("followknownimperfectpath", m_FollowKnownImperfectPathCountdown);
Serializer(serialize, "target type", m_MoveRequest.m_Type, MoveRequest::Type::OFFSET);
serialize.NumberU32_Unbounded("target entity", m_MoveRequest.m_Entity);
serialize.NumberFixed_Unbounded("target pos x", m_MoveRequest.m_Position.X);
serialize.NumberFixed_Unbounded("target pos y", m_MoveRequest.m_Position.Y);
serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange);
serialize.NumberFixed_Unbounded("target max range", m_MoveRequest.m_MaxRange);
serialize.NumberU32_Unbounded("formation controller", m_FormationController);
serialize.NumberFixed_Unbounded("speed multiplier", m_SpeedMultiplier);
serialize.NumberFixed_Unbounded("last turn speed", m_LastTurnSpeed);
serialize.NumberFixed_Unbounded("current speed", m_CurrentSpeed);
serialize.NumberFixed_Unbounded("instant turn angle", m_InstantTurnAngle);
serialize.NumberFixed_Unbounded("acceleration", m_Acceleration);
serialize.Bool("facePointAfterMove", m_FacePointAfterMove);
serialize.Bool("pushing", m_Pushing);
Serializer(serialize, "long path", m_LongPath.m_Waypoints);
Serializer(serialize, "short path", m_ShortPath.m_Waypoints);
}
virtual void Serialize(ISerializer& serialize)
{
SerializeCommon(serialize);
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
SerializeCommon(deserialize);
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName);
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
m_BlockMovement = cmpObstruction->GetBlockMovementFlag(false);
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_RenderSubmit:
{
PROFILE("UnitMotion::RenderSubmit");
const CMessageRenderSubmit& msgData = static_cast (msg);
RenderSubmit(msgData.collector);
break;
}
case MT_PathResult:
{
const CMessagePathResult& msgData = static_cast (msg);
PathResult(msgData.ticket, msgData.path);
break;
}
case MT_Create:
{
if (!ENTITY_IS_LOCAL(GetEntityId()))
CmpPtr(GetSystemEntity())->Register(this, GetEntityId(), m_IsFormationController);
break;
}
case MT_Destroy:
{
if (!ENTITY_IS_LOCAL(GetEntityId()))
CmpPtr(GetSystemEntity())->Unregister(GetEntityId());
break;
}
case MT_MovementObstructionChanged:
{
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
m_BlockMovement = cmpObstruction->GetBlockMovementFlag(false);
break;
}
case MT_ValueModification:
{
const CMessageValueModification& msgData = static_cast (msg);
if (msgData.component != L"UnitMotion")
break;
FALLTHROUGH;
}
case MT_OwnershipChanged:
{
OnValueModification();
break;
}
case MT_Deserialized:
{
OnValueModification();
break;
}
}
}
void UpdateMessageSubscriptions()
{
bool needRender = m_DebugOverlayEnabled;
GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender);
}
virtual bool IsMoveRequested() const
{
return m_MoveRequest.m_Type != MoveRequest::NONE;
}
virtual fixed GetSpeedMultiplier() const
{
return m_SpeedMultiplier;
}
virtual void SetSpeedMultiplier(fixed multiplier)
{
m_SpeedMultiplier = std::min(multiplier, m_RunMultiplier);
m_Speed = m_SpeedMultiplier.Multiply(GetWalkSpeed());
}
virtual fixed GetSpeed() const
{
return m_Speed;
}
virtual fixed GetWalkSpeed() const
{
return m_WalkSpeed;
}
virtual fixed GetRunMultiplier() const
{
return m_RunMultiplier;
}
virtual CFixedVector2D EstimateFuturePosition(const fixed dt) const
{
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return CFixedVector2D();
// TODO: formation members should perhaps try to use the controller's position.
CFixedVector2D pos = cmpPosition->GetPosition2D();
entity_angle_t angle = cmpPosition->GetRotation().Y;
fixed speed = m_CurrentSpeed;
// Copy the path so we don't change it.
WaypointPath shortPath = m_ShortPath;
WaypointPath longPath = m_LongPath;
PerformMove(dt, cmpPosition->GetTurnRate(), shortPath, longPath, pos, speed, angle, 0);
return pos;
}
virtual fixed GetAcceleration() const
{
return m_Acceleration;
}
virtual void SetAcceleration(fixed acceleration)
{
m_Acceleration = acceleration;
}
virtual pass_class_t GetPassabilityClass() const
{
return m_PassClass;
}
virtual std::string GetPassabilityClassName() const
{
return m_PassClassName;
}
virtual void SetPassabilityClassName(const std::string& passClassName)
{
m_PassClassName = passClassName;
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName);
}
virtual fixed GetCurrentSpeed() const
{
return m_CurrentSpeed;
}
virtual void SetFacePointAfterMove(bool facePointAfterMove)
{
m_FacePointAfterMove = facePointAfterMove;
}
virtual bool GetFacePointAfterMove() const
{
return m_FacePointAfterMove;
}
virtual void SetDebugOverlay(bool enabled)
{
m_DebugOverlayEnabled = enabled;
UpdateMessageSubscriptions();
}
virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
{
return MoveTo(MoveRequest(CFixedVector2D(x, z), minRange, maxRange));
}
virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
return MoveTo(MoveRequest(target, minRange, maxRange));
}
virtual void MoveToFormationOffset(entity_id_t controller, entity_pos_t x, entity_pos_t z)
{
// Pass the controller to the move request anyways.
MoveTo(MoveRequest(controller, CFixedVector2D(x, z)));
}
virtual void SetMemberOfFormation(entity_id_t controller)
{
m_FormationController = controller;
}
virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z);
/**
* Clears the current MoveRequest - the unit will stop and no longer try and move.
* This should never be called from UnitMotion, since MoveToX orders are given
* by other components - these components should also decide when to stop.
*/
virtual void StopMoving()
{
if (m_FacePointAfterMove)
{
CmpPtr cmpPosition(GetEntityHandle());
if (cmpPosition && cmpPosition->IsInWorld())
{
CFixedVector2D targetPos;
if (ComputeTargetPosition(targetPos))
FaceTowardsPointFromPos(cmpPosition->GetPosition2D(), targetPos.X, targetPos.Y);
}
}
m_MoveRequest = MoveRequest();
m_ExpectedPathTicket.clear();
m_LongPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
}
virtual entity_pos_t GetUnitClearance() const
{
return m_Clearance;
}
private:
bool IsFormationMember() const
{
return m_FormationController != INVALID_ENTITY;
}
bool IsMovingAsFormation() const
{
return IsFormationMember() && m_MoveRequest.m_Type == MoveRequest::OFFSET;
}
bool IsFormationControllerMoving() const
{
CmpPtr cmpControllerMotion(GetSimContext(), m_FormationController);
return cmpControllerMotion && cmpControllerMotion->IsMoveRequested();
}
entity_id_t GetGroup() const
{
return IsFormationMember() ? m_FormationController : GetEntityId();
}
void SetParticipateInPushing(bool pushing)
{
CmpPtr cmpUnitMotionManager(GetSystemEntity());
m_Pushing = pushing && cmpUnitMotionManager->IsPushingActivated();
}
/**
* Warns other components that our current movement will likely fail (e.g. we won't be able to reach our target)
* This should only be called before the actual movement in a given turn, or units might both move and try to do things
* on the same turn, leading to gliding units.
*/
void MoveFailed()
{
// Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time
// if our current offset is unreachable, but we don't want to end up stuck.
// (If the formation controller has stopped moving however, we can safely message).
if (IsFormationMember() && IsFormationControllerMoving())
return;
CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_FAILURE);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
/**
* Warns other components that our current movement is likely over (i.e. we probably reached our destination)
* This should only be called before the actual movement in a given turn, or units might both move and try to do things
* on the same turn, leading to gliding units.
*/
void MoveSucceeded()
{
// Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time
// if our current offset is unreachable, but we don't want to end up stuck.
// (If the formation controller has stopped moving however, we can safely message).
if (IsFormationMember() && IsFormationControllerMoving())
return;
CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_SUCCESS);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
/**
* Warns other components that our current movement was obstructed (i.e. we failed to move this turn).
* This should only be called before the actual movement in a given turn, or units might both move and try to do things
* on the same turn, leading to gliding units.
*/
void MoveObstructed()
{
// Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time
// if our current offset is unreachable, but we don't want to end up stuck.
// (If the formation controller has stopped moving however, we can safely message).
if (IsFormationMember() && IsFormationControllerMoving())
return;
CMessageMotionUpdate msg(m_FailedMovements >= VERY_OBSTRUCTED_THRESHOLD ?
CMessageMotionUpdate::VERY_OBSTRUCTED : CMessageMotionUpdate::OBSTRUCTED);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
/**
* Increment the number of failed movements and notify other components if required.
* @returns true if the failure was notified, false otherwise.
*/
bool IncrementFailedMovementsAndMaybeNotify()
{
m_FailedMovements++;
if (m_FailedMovements >= MAX_FAILED_MOVEMENTS)
{
MoveFailed();
m_FailedMovements = 0;
return true;
}
return false;
}
/**
* If path would take us farther away from the goal than pos currently is, return false, else return true.
*/
bool RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const;
bool ShouldAlternatePathfinder() const
{
return (m_FailedMovements == ALTERNATE_PATH_TYPE_DELAY) || ((MAX_FAILED_MOVEMENTS - ALTERNATE_PATH_TYPE_DELAY) % ALTERNATE_PATH_TYPE_EVERY == 0);
}
bool InShortPathRange(const PathGoal& goal, const CFixedVector2D& pos) const
{
return goal.DistanceToPoint(pos) < LONG_PATH_MIN_DIST;
}
entity_pos_t ShortPathSearchRange() const
{
u8 multiple = m_FailedMovements < SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY ? 0 : m_FailedMovements - SHORT_PATH_SEARCH_RANGE_INCREASE_DELAY;
fixed searchRange = SHORT_PATH_MIN_SEARCH_RANGE + SHORT_PATH_SEARCH_RANGE_INCREMENT * multiple;
if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE)
searchRange = SHORT_PATH_MAX_SEARCH_RANGE;
return searchRange;
}
/**
* Handle the result of an asynchronous path query.
*/
void PathResult(u32 ticket, const WaypointPath& path);
void OnValueModification()
{
CmpPtr cmpValueModificationManager(GetSystemEntity());
if (!cmpValueModificationManager)
return;
m_WalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId());
m_RunMultiplier = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateRunMultiplier, GetEntityId());
// For MT_Deserialize compute m_Speed from the serialized m_SpeedMultiplier.
// For MT_ValueModification and MT_OwnershipChanged, adjust m_SpeedMultiplier if needed
// (in case then new m_RunMultiplier value is lower than the old).
SetSpeedMultiplier(m_SpeedMultiplier);
}
/**
* Check if we are at destination early in the turn, this both lets units react faster
* and ensure that distance comparisons are done while units are not being moved
* (otherwise they won't be commutative).
*/
void OnTurnStart();
void PreMove(CCmpUnitMotionManager::MotionState& state);
void Move(CCmpUnitMotionManager::MotionState& state, fixed dt);
void PostMove(CCmpUnitMotionManager::MotionState& state, fixed dt);
/**
* Returns true if we are possibly at our destination.
* Since the concept of being at destination is dependent on why the move was requested,
* UnitMotion can only ever hint about this, hence the conditional tone.
*/
bool PossiblyAtDestination() const;
/**
* Process the move the unit will do this turn.
* This does not send actually change the position.
* @returns true if the move was obstructed.
*/
bool PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle, uint8_t pushingPressure) const;
/**
* Update other components on our speed.
* (For performance, this should try to avoid sending messages).
*/
void UpdateMovementState(entity_pos_t speed, entity_pos_t meanSpeed);
/**
* React if our move was obstructed.
* @param moved - true if the unit still managed to move.
* @returns true if the obstruction required handling, false otherwise.
*/
bool HandleObstructedMove(bool moved);
/**
* Returns true if the target position is valid. False otherwise.
* (this may indicate that the target is e.g. out of the world/dead).
* NB: for code-writing convenience, if we have no target, this returns true.
*/
bool TargetHasValidPosition(const MoveRequest& moveRequest) const;
bool TargetHasValidPosition() const
{
return TargetHasValidPosition(m_MoveRequest);
}
/**
* Computes the current location of our target entity (plus offset).
* Returns false if no target entity or no valid position.
*/
bool ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const;
bool ComputeTargetPosition(CFixedVector2D& out) const
{
return ComputeTargetPosition(out, m_MoveRequest);
}
/**
* Attempts to replace the current path with a straight line to the target,
* if it's close enough and the route is not obstructed.
*/
bool TryGoingStraightToTarget(const CFixedVector2D& from, bool updatePaths);
/**
* Returns whether our we need to recompute a path to reach our target.
*/
bool PathingUpdateNeeded(const CFixedVector2D& from) const;
/**
* Rotate to face towards the target point, given the current pos
*/
void FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z);
/**
* Units in 'pushing' mode are marked as 'moving' in the obstruction manager.
* Units in 'pushing' mode should skip them in checkMovement (to enable pushing).
* However, units for which pushing is deactivated should collide against everyone.
* Units that don't block movement never participate in pushing, but they also
* shouldn't collide with pushing units.
*/
bool ShouldCollideWithMovingUnits() const
{
return !m_Pushing && m_BlockMovement;
}
/**
* Returns an appropriate obstruction filter for use with path requests.
*/
ControlGroupMovementObstructionFilter GetObstructionFilter() const
{
return ControlGroupMovementObstructionFilter(ShouldCollideWithMovingUnits(), GetGroup());
}
/**
* Filter a specific tag on top of the existing control groups.
*/
SkipTagAndControlGroupObstructionFilter GetObstructionFilter(const ICmpObstructionManager::tag_t& tag) const
{
return SkipTagAndControlGroupObstructionFilter(tag, ShouldCollideWithMovingUnits(), GetGroup());
}
/**
* Decide whether to approximate the given range from a square target as a circle,
* rather than as a square.
*/
bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const;
/**
* Create a PathGoal from a move request.
* @returns true if the goal was successfully created.
*/
bool ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const;
/**
* Compute a path to the given goal from the given position.
* Might go in a straight line immediately, or might start an asynchronous path request.
*/
void ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal);
/**
* Start an asynchronous long path query.
*/
void RequestLongPath(const CFixedVector2D& from, const PathGoal& goal);
/**
* Start an asynchronous short path query.
* @param extendRange - if true, extend the search range to at least the distance to the goal.
*/
void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool extendRange);
/**
* General handler for MoveTo interface functions.
*/
bool MoveTo(MoveRequest request);
/**
* Convert a path into a renderable list of lines
*/
void RenderPath(const WaypointPath& path, std::vector& lines, CColor color);
void RenderSubmit(SceneCollector& collector);
};
REGISTER_COMPONENT_TYPE(UnitMotion)
bool CCmpUnitMotion::RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const
{
if (path.m_Waypoints.empty())
return false;
// Reject the new path if it does not lead us closer to the target's position.
if (goal.DistanceToPoint(pos) <= goal.DistanceToPoint(CFixedVector2D(path.m_Waypoints.front().x, path.m_Waypoints.front().z)))
return true;
return false;
}
void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path)
{
// Ignore obsolete path requests
if (ticket != m_ExpectedPathTicket.m_Ticket || m_MoveRequest.m_Type == MoveRequest::NONE)
return;
Ticket::Type ticketType = m_ExpectedPathTicket.m_Type;
m_ExpectedPathTicket.clear();
// If we not longer have a position, we won't be able to do much.
// Fail in the next Move() call.
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return;
CFixedVector2D pos = cmpPosition->GetPosition2D();
// Assume all long paths were towards the goal, and assume short paths were if there are no long waypoints.
bool pathedTowardsGoal = ticketType == Ticket::LONG_PATH || m_LongPath.m_Waypoints.empty();
// Check if we need to run the short-path hack (warning: tricky control flow).
bool shortPathHack = false;
if (path.m_Waypoints.empty())
{
// No waypoints means pathing failed. If this was a long-path, try the short-path hack.
if (!pathedTowardsGoal)
return;
shortPathHack = ticketType == Ticket::LONG_PATH;
}
else if (PathGoal goal; pathedTowardsGoal && ComputeGoal(goal, m_MoveRequest) && RejectFartherPaths(goal, path, pos))
{
// Reject paths that would take the unit further away from the goal.
// This assumes that we prefer being closer 'as the crow flies' to unreachable goals.
// This is a hack of sorts around units 'dancing' between two positions (see e.g. #3144),
// but never actually failing to move, ergo never actually informing unitAI that it succeeds/fails.
// (for short paths, only do so if aiming directly for the goal
// as sub-goals may be farther than we are).
// If this was a long-path and we no longer have waypoints, try the short-path hack.
if (!m_LongPath.m_Waypoints.empty())
return;
shortPathHack = ticketType == Ticket::LONG_PATH;
}
// Short-path hack: if the long-range pathfinder doesn't find an acceptable path, push a fake waypoint at the goal.
// This means HandleObstructedMove will use the short-pathfinder to try and reach it,
// and that may find a path as the vertex pathfinder is more precise.
if (shortPathHack)
{
// If we're resorting to the short-path hack, the situation is dire. Most likely, the goal is unreachable.
// We want to find a path or fail fast. Bump failed movements so the short pathfinder will run at max-range
// right away. This is safe from a performance PoV because it can only happen if the target is unreachable to
// the long-range pathfinder, which is rare, and since the entity will fail to move if the goal is actually unreachable,
// the failed movements will be increased to MAX anyways, so just shortcut.
m_FailedMovements = MAX_FAILED_MOVEMENTS - 2;
CFixedVector2D targetPos;
if (ComputeTargetPosition(targetPos))
m_LongPath.m_Waypoints.emplace_back(Waypoint{ targetPos.X, targetPos.Y });
return;
}
if (ticketType == Ticket::LONG_PATH)
{
m_LongPath = path;
// Long paths don't properly follow diagonals because of JPS/the grid. Since units now take time turning,
// they can actually slow down substantially if they have to do a one navcell diagonal movement,
// which is somewhat common at the beginning of a new path.
// For that reason, if the first waypoint is really close, check if we can't go directly to the second.
if (m_LongPath.m_Waypoints.size() >= 2)
{
const Waypoint& firstWpt = m_LongPath.m_Waypoints.back();
if (CFixedVector2D(firstWpt.x - pos.X, firstWpt.z - pos.Y).CompareLength(Pathfinding::NAVCELL_SIZE * 4) <= 0)
{
CmpPtr cmpPathfinder(GetSystemEntity());
ENSURE(cmpPathfinder);
const Waypoint& secondWpt = m_LongPath.m_Waypoints[m_LongPath.m_Waypoints.size() - 2];
if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, secondWpt.x, secondWpt.z, m_Clearance, m_PassClass))
m_LongPath.m_Waypoints.pop_back();
}
}
}
else
m_ShortPath = path;
m_FollowKnownImperfectPathCountdown = 0;
if (!pathedTowardsGoal)
return;
// Performance hack: If we were pathing towards the goal and this new path won't put us in range,
// it's highly likely that we are going somewhere unreachable.
// However, Move() will try to recompute the path every turn, which can be quite slow.
// To avoid this, act as if our current path leads us to the correct destination.
// NB: for short-paths, the problem might be that the search space is too small
// but we'll still follow this path until the en and try again then.
// Because we reject farther paths, it works out.
if (PathingUpdateNeeded(pos))
{
// Inform other components early, as they might have better behaviour than waiting for the path to carry out.
// Send OBSTRUCTED at first - moveFailed is likely to trigger path recomputation and we might end up
// recomputing too often for nothing.
if (!IncrementFailedMovementsAndMaybeNotify())
MoveObstructed();
// We'll automatically recompute a path when this reaches 0, as a way to improve behaviour.
// (See D665 - this is needed because the target may be moving, and we should adjust to that).
m_FollowKnownImperfectPathCountdown = KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN;
}
}
void CCmpUnitMotion::OnTurnStart()
{
if (PossiblyAtDestination())
MoveSucceeded();
else if (!TargetHasValidPosition())
{
// Scrap waypoints - we don't know where to go.
// If the move request remains unchanged and the target again has a valid position later on,
// moving will be resumed.
// Units may want to move to move to the target's last known position,
// but that should be decided by UnitAI (handling MoveFailed), not UnitMotion.
m_LongPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
MoveFailed();
}
}
void CCmpUnitMotion::PreMove(CCmpUnitMotionManager::MotionState& state)
{
state.ignore = !m_Pushing || !m_BlockMovement;
state.wasObstructed = false;
state.wentStraight = false;
// If we were idle and will still be, no need for an update.
state.needUpdate = state.cmpPosition->IsInWorld() &&
(m_CurrentSpeed != fixed::Zero() || m_LastTurnSpeed != fixed::Zero() || m_MoveRequest.m_Type != MoveRequest::NONE);
if (!m_BlockMovement)
return;
state.controlGroup = IsFormationMember() ? m_FormationController : INVALID_ENTITY;
// Update moving flag, this is an internal construct used for pushing,
// so it does not really reflect whether the unit is actually moving or not.
state.isMoving = m_Pushing && m_MoveRequest.m_Type != MoveRequest::NONE;
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
cmpObstruction->SetMovingFlag(state.isMoving);
}
void CCmpUnitMotion::Move(CCmpUnitMotionManager::MotionState& state, fixed dt)
{
PROFILE("Move");
// If we're chasing a potentially-moving unit and are currently close
// enough to its current position, and we can head in a straight line
// to it, then throw away our current path and go straight to it.
state.wentStraight = TryGoingStraightToTarget(state.initialPos, true);
state.wasObstructed = PerformMove(dt, state.cmpPosition->GetTurnRate(), m_ShortPath, m_LongPath, state.pos, state.speed, state.angle, state.pushingPressure);
}
void CCmpUnitMotion::PostMove(CCmpUnitMotionManager::MotionState& state, fixed dt)
{
// Update our speed over this turn so that the visual actor shows the correct animation.
if (state.pos == state.initialPos)
{
if (state.angle != state.initialAngle)
state.cmpPosition->TurnTo(state.angle);
UpdateMovementState(fixed::Zero(), fixed::Zero());
}
else
{
// Update the Position component after our movement (if we actually moved anywhere)
CFixedVector2D offset = state.pos - state.initialPos;
state.cmpPosition->MoveAndTurnTo(state.pos.X, state.pos.Y, state.angle);
// Calculate the mean speed over this past turn.
UpdateMovementState(state.speed, offset.Length() / dt);
}
if (state.wasObstructed && HandleObstructedMove(state.pos != state.initialPos))
return;
else if (!state.wasObstructed && state.pos != state.initialPos)
m_FailedMovements = 0;
// If we moved straight, and didn't quite finish the path, reset - we'll update it next turn if still OK.
if (state.wentStraight && !state.wasObstructed)
m_ShortPath.m_Waypoints.clear();
// We may need to recompute our path sometimes (e.g. if our target moves).
// Since we request paths asynchronously anyways, this does not need to be done before moving.
if (!state.wentStraight && PathingUpdateNeeded(state.pos))
{
PathGoal goal;
if (ComputeGoal(goal, m_MoveRequest))
ComputePathToGoal(state.pos, goal);
}
else if (m_FollowKnownImperfectPathCountdown > 0)
--m_FollowKnownImperfectPathCountdown;
}
bool CCmpUnitMotion::PossiblyAtDestination() const
{
if (m_MoveRequest.m_Type == MoveRequest::NONE)
return false;
CmpPtr cmpObstructionManager(GetSystemEntity());
ENSURE(cmpObstructionManager);
if (m_MoveRequest.m_Type == MoveRequest::POINT)
return cmpObstructionManager->IsInPointRange(GetEntityId(), m_MoveRequest.m_Position.X, m_MoveRequest.m_Position.Y, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false);
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
return cmpObstructionManager->IsInTargetRange(GetEntityId(), m_MoveRequest.m_Entity, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false);
if (m_MoveRequest.m_Type == MoveRequest::OFFSET)
{
CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity);
if (cmpControllerMotion && cmpControllerMotion->IsMoveRequested())
return false;
// In formation, return a match only if we are exactly at the target position.
// Otherwise, units can go in an infinite "walzting" loop when the Idle formation timer
// reforms them.
CFixedVector2D targetPos;
ComputeTargetPosition(targetPos);
CmpPtr cmpPosition(GetEntityHandle());
return (targetPos-cmpPosition->GetPosition2D()).CompareLength(fixed::Zero()) <= 0;
}
return false;
}
bool CCmpUnitMotion::PerformMove(fixed dt, const fixed& turnRate, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos, fixed& speed, entity_angle_t& angle, uint8_t pushingPressure) const
{
// If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it.
if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty())
return true;
// Wrap the angle to (-Pi, Pi].
while (angle > entity_angle_t::Pi())
angle -= entity_angle_t::Pi() * 2;
while (angle < -entity_angle_t::Pi())
angle += entity_angle_t::Pi() * 2;
CmpPtr cmpPathfinder(GetSystemEntity());
ENSURE(cmpPathfinder);
fixed basicSpeed = m_Speed;
// If in formation, run to keep up; otherwise just walk.
if (IsMovingAsFormation())
basicSpeed = m_Speed.Multiply(m_RunMultiplier);
// If pushing pressure is applied, slow the unit down.
if (pushingPressure)
{
// Values below this pressure don't slow the unit down (avoids slowing groups down).
constexpr int pressureMinThreshold = 10;
// Lower speed up to a floor to prevent units from getting stopped.
// This helped pushing particularly for fast units, since they'll end up slowing down.
constexpr int maxPressure = CCmpUnitMotionManager::MAX_PRESSURE - pressureMinThreshold - 80;
constexpr entity_pos_t floorSpeed = entity_pos_t::FromFraction(3, 2);
static_assert(maxPressure > 0);
uint8_t slowdown = maxPressure - std::min(maxPressure, std::max(0, pushingPressure - pressureMinThreshold));
basicSpeed = basicSpeed.Multiply(fixed::FromInt(slowdown) / maxPressure);
// NB: lowering this too much will make the units behave a lot like viscous fluid
// when the density becomes extreme. While perhaps realistic (and kind of neat),
// it's not very helpful for gameplay. Empirically, a value of 1.5 avoids most of the effect
// while still slowing down movement significantly, and seems like a good balance.
// Min with the template speed to allow units that are explicitly absurdly slow.
basicSpeed = std::max(std::min(m_TemplateWalkSpeed, floorSpeed), basicSpeed);
}
- // Find the speed factor of the underlying terrain.
- // (We only care about the tile we start on - it doesn't matter if we're moving
- // partially onto a much slower/faster tile).
- // TODO: Terrain-dependent speeds are not currently supported.
- fixed terrainSpeed = fixed::FromInt(1);
-
- fixed maxSpeed = basicSpeed.Multiply(terrainSpeed);
+ // TODO: would be nice to support terrain-dependent speed again.
+ fixed maxSpeed = basicSpeed;
fixed timeLeft = dt;
fixed zero = fixed::Zero();
ICmpObstructionManager::tag_t specificIgnore;
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
{
CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
if (cmpTargetObstruction)
specificIgnore = cmpTargetObstruction->GetObstruction();
}
while (timeLeft > zero)
{
// If we ran out of path, we have to stop.
if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty())
break;
CFixedVector2D target;
if (shortPath.m_Waypoints.empty())
target = CFixedVector2D(longPath.m_Waypoints.back().x, longPath.m_Waypoints.back().z);
else
target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z);
CFixedVector2D offset = target - pos;
if (turnRate > zero && !offset.IsZero())
{
fixed angleDiff = angle - atan2_approx(offset.X, offset.Y);
fixed absoluteAngleDiff = angleDiff.Absolute();
if (absoluteAngleDiff > entity_angle_t::Pi())
absoluteAngleDiff = entity_angle_t::Pi() * 2 - absoluteAngleDiff;
// We only rotate to the instantTurnAngle angle. The rest we rotate during movement.
if (absoluteAngleDiff > m_InstantTurnAngle)
{
// Stop moving when rotating this far.
speed = zero;
fixed maxRotation = turnRate.Multiply(timeLeft);
// Figure out whether rotating will increase or decrease the angle, and how far we need to rotate in that direction.
int direction = (entity_angle_t::Zero() < angleDiff && angleDiff <= entity_angle_t::Pi()) || angleDiff < -entity_angle_t::Pi() ? -1 : 1;
// Can't rotate far enough, just rotate in the correct direction.
if (absoluteAngleDiff - m_InstantTurnAngle > maxRotation)
{
angle += maxRotation * direction;
if (angle * direction > entity_angle_t::Pi())
angle -= entity_angle_t::Pi() * 2 * direction;
break;
}
// Rotate towards the next waypoint and continue moving.
angle = atan2_approx(offset.X, offset.Y);
timeLeft = std::min(maxRotation, maxRotation - absoluteAngleDiff + m_InstantTurnAngle) / turnRate;
}
else
{
// Modify the speed depending on the angle difference.
fixed sin, cos;
sincos_approx(angleDiff, sin, cos);
speed = speed.Multiply(cos);
angle = atan2_approx(offset.X, offset.Y);
}
}
// Work out how far we can travel in timeLeft.
fixed accelTime = std::min(timeLeft, (maxSpeed - speed) / m_Acceleration);
fixed accelDist = speed.Multiply(accelTime) + accelTime.Square().Multiply(m_Acceleration) / 2;
fixed maxdist = accelDist + maxSpeed.Multiply(timeLeft - accelTime);
// If the target is close, we can move there directly.
fixed offsetLength = offset.Length();
if (offsetLength <= maxdist)
{
if (cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass))
{
pos = target;
// Spend the rest of the time heading towards the next waypoint.
// Either we still need to accelerate after, or we have reached maxSpeed.
// The former is much less likely than the latter: usually we can reach
// maxSpeed within one waypoint. So the Sqrt is not too bad.
if (offsetLength <= accelDist)
{
fixed requiredTime = (-speed + (speed.Square() + offsetLength.Multiply(m_Acceleration).Multiply(fixed::FromInt(2))).Sqrt()) / m_Acceleration;
timeLeft -= requiredTime;
speed += m_Acceleration.Multiply(requiredTime);
}
else
{
timeLeft -= accelTime + (offsetLength - accelDist) / maxSpeed;
speed = maxSpeed;
}
if (shortPath.m_Waypoints.empty())
longPath.m_Waypoints.pop_back();
else
shortPath.m_Waypoints.pop_back();
continue;
}
else
{
// Error - path was obstructed.
return true;
}
}
else
{
// Not close enough, so just move in the right direction.
offset.Normalize(maxdist);
target = pos + offset;
speed = std::min(maxSpeed, speed + m_Acceleration.Multiply(timeLeft));
if (cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass))
pos = target;
else
return true;
break;
}
}
return false;
}
void CCmpUnitMotion::UpdateMovementState(entity_pos_t speed, entity_pos_t meanSpeed)
{
CmpPtr cmpVisual(GetEntityHandle());
if (cmpVisual)
{
if (meanSpeed == fixed::Zero())
cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1));
else
cmpVisual->SelectMovementAnimation(meanSpeed > (m_WalkSpeed / 2).Multiply(m_RunMultiplier + fixed::FromInt(1)) ? "run" : "walk", meanSpeed);
}
m_LastTurnSpeed = meanSpeed;
m_CurrentSpeed = speed;
}
bool CCmpUnitMotion::HandleObstructedMove(bool moved)
{
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return false;
// We failed to move, inform other components as they might handle it.
// (don't send messages on the first failure, as that would be too noisy).
// Also don't increment above the initial MoveObstructed message if we actually manage to move a little.
if (!moved || m_FailedMovements < 2)
{
if (!IncrementFailedMovementsAndMaybeNotify() && m_FailedMovements >= 2)
MoveObstructed();
}
PathGoal goal;
if (!ComputeGoal(goal, m_MoveRequest))
return false;
// At this point we have a position in the world since ComputeGoal checked for that.
CFixedVector2D pos = cmpPosition->GetPosition2D();
// Assume that we are merely obstructed and the long path is salvageable, so try going around the obstruction.
// This could be a separate function, but it doesn't really make sense to call it outside of here, and I can't find a name.
// I use an IIFE to have nice 'return' semantics still.
if ([&]() -> bool {
// If the goal is close enough, we should ignore any remaining long waypoint and just
// short path there directly, as that improves behaviour in general - see D2095).
if (InShortPathRange(goal, pos))
return false;
// On rare occasions, when following a short path, we can end up in a position where
// the short pathfinder thinks we are inside an obstruction (and can leave)
// but the CheckMovement logic doesn't. I believe the cause is a small numerical difference
// in their calculation, but haven't been able to pinpoint it precisely.
// In those cases, the solution is to back away to prevent the short-pathfinder from being confused.
// TODO: this should only be done if we're obstructed by a static entity.
if (!m_ShortPath.m_Waypoints.empty() && m_FailedMovements == BACKUP_HACK_DELAY)
{
Waypoint next = m_ShortPath.m_Waypoints.back();
CFixedVector2D backUp(pos.X - next.x, pos.Y - next.z);
backUp.Normalize();
next.x = pos.X + backUp.X;
next.z = pos.Y + backUp.Y;
m_ShortPath.m_Waypoints.push_back(next);
return true;
}
// Delete the next waypoint if it's reasonably close,
// because it might be blocked by units and thus unreachable.
// NB: this number is tricky. Make it too high, and units start going down dead ends, which looks odd (#5795)
// Make it too low, and they might get stuck behind other obstructed entities.
// It also has performance implications because it calls the short-pathfinder.
fixed skipbeyond = std::max(ShortPathSearchRange() / 3, Pathfinding::NAVCELL_SIZE * 8);
if (m_LongPath.m_Waypoints.size() > 1 &&
(pos - CFixedVector2D(m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z)).CompareLength(skipbeyond) < 0)
{
m_LongPath.m_Waypoints.pop_back();
}
else if (ShouldAlternatePathfinder())
{
// Recompute the whole thing occasionally, in case we got stuck in a dead end from removing long waypoints.
RequestLongPath(pos, goal);
return true;
}
if (m_LongPath.m_Waypoints.empty())
return false;
// Compute a short path in the general vicinity of the next waypoint, to help pathfinding in crowds.
// The goal here is to manage to move in the general direction of our target, not to be super accurate.
fixed radius = Clamp(skipbeyond/3, Pathfinding::NAVCELL_SIZE * 4, Pathfinding::NAVCELL_SIZE * 12);
PathGoal subgoal = { PathGoal::CIRCLE, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z, radius };
RequestShortPath(pos, subgoal, false);
return true;
}()) return true;
// If we couldn't use a workaround, try recomputing the entire path.
ComputePathToGoal(pos, goal);
return true;
}
bool CCmpUnitMotion::TargetHasValidPosition(const MoveRequest& moveRequest) const
{
if (moveRequest.m_Type != MoveRequest::ENTITY)
return true;
CmpPtr cmpPosition(GetSimContext(), moveRequest.m_Entity);
return cmpPosition && cmpPosition->IsInWorld();
}
bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const
{
if (moveRequest.m_Type == MoveRequest::POINT)
{
out = moveRequest.m_Position;
return true;
}
CmpPtr cmpTargetPosition(GetSimContext(), moveRequest.m_Entity);
if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld())
return false;
if (moveRequest.m_Type == MoveRequest::OFFSET)
{
// There is an offset, so compute it relative to orientation
entity_angle_t angle = cmpTargetPosition->GetRotation().Y;
CFixedVector2D offset = moveRequest.GetOffset().Rotate(angle);
out = cmpTargetPosition->GetPosition2D() + offset;
}
else
{
out = cmpTargetPosition->GetPosition2D();
// Position is only updated after all units have moved & pushed.
// Therefore, we may need to interpolate the target position, depending on when this call takes place during the turn:
// - On "Turn Start", we'll check positions directly without interpolation.
// - During movement, we'll call this for direct-pathing & we need to interpolate
// (this way, we move where the unit will end up at the end of _this_ turn, making it match on next turn start).
// - After movement, we'll call this to request paths & we need to interpolate
// (this way, we'll move where the unit ends up in the end of _next_ turn, making it a match in 2 turns).
// TODO: This does not really aim many turns in advance, with orthogonal trajectories it probably should.
CmpPtr cmpUnitMotion(GetSimContext(), moveRequest.m_Entity);
CmpPtr cmpUnitMotionManager(GetSystemEntity());
bool needInterpolation = cmpUnitMotion && cmpUnitMotion->IsMoveRequested() && cmpUnitMotionManager->ComputingMotion();
if (needInterpolation)
{
// Add predicted movement.
CFixedVector2D tempPos = out + (out - cmpTargetPosition->GetPreviousPosition2D());
out = tempPos;
}
}
return true;
}
bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from, bool updatePaths)
{
// Assume if we have short paths we want to follow them.
// Exception: offset movement (formations) generally have very short deltas
// and to look good we need them to walk-straight most of the time.
if (!IsFormationMember() && !m_ShortPath.m_Waypoints.empty())
return false;
CFixedVector2D targetPos;
if (!ComputeTargetPosition(targetPos))
return false;
CmpPtr cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return false;
// Move the goal to match the target entity's new position
PathGoal goal;
if (!ComputeGoal(goal, m_MoveRequest))
return false;
goal.x = targetPos.X;
goal.z = targetPos.Y;
// (we ignore changes to the target's rotation, since only buildings are
// square and buildings don't move)
// Find the point on the goal shape that we should head towards
CFixedVector2D goalPos = goal.NearestPointOnGoal(from);
// Fail if the target is too far away
if ((goalPos - from).CompareLength(DIRECT_PATH_RANGE) > 0)
return false;
// Check if there's any collisions on that route.
// For entity goals, skip only the specific obstruction tag or with e.g. walls we might ignore too many entities.
ICmpObstructionManager::tag_t specificIgnore;
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
{
CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
if (cmpTargetObstruction)
specificIgnore = cmpTargetObstruction->GetObstruction();
}
// Check movement against units - we want to use the short pathfinder to walk around those if needed.
if (specificIgnore.valid())
{
if (!cmpPathfinder->CheckMovement(GetObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
return false;
}
else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass))
return false;
if (!updatePaths)
return true;
// That route is okay, so update our path
m_LongPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y });
return true;
}
bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const
{
if (m_MoveRequest.m_Type == MoveRequest::NONE)
return false;
CFixedVector2D targetPos;
if (!ComputeTargetPosition(targetPos))
return false;
if (m_FollowKnownImperfectPathCountdown > 0 && (!m_LongPath.m_Waypoints.empty() || !m_ShortPath.m_Waypoints.empty()))
return false;
if (PossiblyAtDestination())
return false;
// Get the obstruction shape and translate it where we estimate the target to be.
ICmpObstructionManager::ObstructionSquare estimatedTargetShape;
if (m_MoveRequest.m_Type == MoveRequest::ENTITY)
{
CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity);
if (cmpTargetObstruction)
cmpTargetObstruction->GetObstructionSquare(estimatedTargetShape);
}
estimatedTargetShape.x = targetPos.X;
estimatedTargetShape.z = targetPos.Y;
CmpPtr cmpObstruction(GetEntityHandle());
ICmpObstructionManager::ObstructionSquare shape;
if (cmpObstruction)
cmpObstruction->GetObstructionSquare(shape);
// Translate our own obstruction shape to our last waypoint or our current position, lacking that.
if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty())
{
shape.x = from.X;
shape.z = from.Y;
}
else
{
const Waypoint& lastWaypoint = m_LongPath.m_Waypoints.empty() ? m_ShortPath.m_Waypoints.front() : m_LongPath.m_Waypoints.front();
shape.x = lastWaypoint.x;
shape.z = lastWaypoint.z;
}
CmpPtr cmpObstructionManager(GetSystemEntity());
ENSURE(cmpObstructionManager);
// Increase the ranges with distance, to avoid recomputing every turn against units that are moving and far-away for example.
entity_pos_t distance = (from - CFixedVector2D(estimatedTargetShape.x, estimatedTargetShape.z)).Length();
// TODO: it could be worth computing this based on time to collision instead of linear distance.
entity_pos_t minRange = std::max(m_MoveRequest.m_MinRange - distance / TARGET_UNCERTAINTY_MULTIPLIER, entity_pos_t::Zero());
entity_pos_t maxRange = m_MoveRequest.m_MaxRange < entity_pos_t::Zero() ? m_MoveRequest.m_MaxRange :
m_MoveRequest.m_MaxRange + distance / TARGET_UNCERTAINTY_MULTIPLIER;
if (cmpObstructionManager->AreShapesInRange(shape, estimatedTargetShape, minRange, maxRange, false))
return false;
return true;
}
void CCmpUnitMotion::FaceTowardsPoint(entity_pos_t x, entity_pos_t z)
{
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return;
CFixedVector2D pos = cmpPosition->GetPosition2D();
FaceTowardsPointFromPos(pos, x, z);
}
void CCmpUnitMotion::FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z)
{
CFixedVector2D target(x, z);
CFixedVector2D offset = target - pos;
if (!offset.IsZero())
{
entity_angle_t angle = atan2_approx(offset.X, offset.Y);
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition)
return;
cmpPosition->TurnTo(angle);
}
}
// The pathfinder cannot go to "rounded rectangles" goals, which are what happens with square targets and a non-null range.
// Depending on what the best approximation is, we either pretend the target is a circle or a square.
// One needs to be careful that the approximated geometry will be in the range.
bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const
{
// Given a square, plus a target range we should reach, the shape at that distance
// is a round-cornered square which we can approximate as either a circle or as a square.
// Previously, we used the shape that minimized the worst-case error.
// However that is unsage in some situations. So let's be less clever and
// just check if our range is at least three times bigger than the circleradius
return (range > circleRadius*3);
}
bool CCmpUnitMotion::ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const
{
if (moveRequest.m_Type == MoveRequest::NONE)
return false;
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return false;
CFixedVector2D pos = cmpPosition->GetPosition2D();
CFixedVector2D targetPosition;
if (!ComputeTargetPosition(targetPosition, moveRequest))
return false;
ICmpObstructionManager::ObstructionSquare targetObstruction;
if (moveRequest.m_Type == MoveRequest::ENTITY)
{
CmpPtr cmpTargetObstruction(GetSimContext(), moveRequest.m_Entity);
if (cmpTargetObstruction)
cmpTargetObstruction->GetObstructionSquare(targetObstruction);
}
targetObstruction.x = targetPosition.X;
targetObstruction.z = targetPosition.Y;
ICmpObstructionManager::ObstructionSquare obstruction;
CmpPtr cmpObstruction(GetEntityHandle());
if (cmpObstruction)
cmpObstruction->GetObstructionSquare(obstruction);
else
{
obstruction.x = pos.X;
obstruction.z = pos.Y;
}
CmpPtr cmpObstructionManager(GetSystemEntity());
ENSURE(cmpObstructionManager);
entity_pos_t distance = cmpObstructionManager->DistanceBetweenShapes(obstruction, targetObstruction);
out.x = targetObstruction.x;
out.z = targetObstruction.z;
out.hw = targetObstruction.hw;
out.hh = targetObstruction.hh;
out.u = targetObstruction.u;
out.v = targetObstruction.v;
if (moveRequest.m_MinRange > fixed::Zero() || moveRequest.m_MaxRange > fixed::Zero() ||
targetObstruction.hw > fixed::Zero())
out.type = PathGoal::SQUARE;
else
{
out.type = PathGoal::POINT;
return true;
}
entity_pos_t circleRadius = CFixedVector2D(targetObstruction.hw, targetObstruction.hh).Length();
// TODO: because we cannot move to rounded rectangles, we have to make conservative approximations.
// This means we might end up in a situation where cons(max-range) < min range < max range < cons(min-range)
// When going outside of the min-range or inside the max-range, the unit will still go through the correct range
// but if it moves fast enough, this might not be picked up by PossiblyAtDestination().
// Fixing this involves moving to rounded rectangles, or checking more often in PerformMove().
// In the meantime, one should avoid that 'Speed over a turn' > MaxRange - MinRange, in case where
// min-range is not 0 and max-range is not infinity.
if (distance < moveRequest.m_MinRange)
{
// Distance checks are nearest edge to nearest edge, so we need to account for our clearance
// and we must make sure diagonals also fit so multiply by slightly more than sqrt(2)
entity_pos_t goalDistance = moveRequest.m_MinRange + m_Clearance * 3 / 2;
if (ShouldTreatTargetAsCircle(moveRequest.m_MinRange, circleRadius))
{
// We are safely away from the obstruction itself if we are away from the circumscribing circle
out.type = PathGoal::INVERTED_CIRCLE;
out.hw = circleRadius + goalDistance;
}
else
{
out.type = PathGoal::INVERTED_SQUARE;
out.hw = targetObstruction.hw + goalDistance;
out.hh = targetObstruction.hh + goalDistance;
}
}
else if (moveRequest.m_MaxRange >= fixed::Zero() && distance > moveRequest.m_MaxRange)
{
if (ShouldTreatTargetAsCircle(moveRequest.m_MaxRange, circleRadius))
{
entity_pos_t goalDistance = moveRequest.m_MaxRange;
// We must go in-range of the inscribed circle, not the circumscribing circle.
circleRadius = std::min(targetObstruction.hw, targetObstruction.hh);
out.type = PathGoal::CIRCLE;
out.hw = circleRadius + goalDistance;
}
else
{
// The target is large relative to our range, so treat it as a square and
// get close enough that the diagonals come within range
entity_pos_t goalDistance = moveRequest.m_MaxRange * 2 / 3; // multiply by slightly less than 1/sqrt(2)
out.type = PathGoal::SQUARE;
entity_pos_t delta = std::max(goalDistance, m_Clearance + entity_pos_t::FromInt(4)/16); // ensure it's far enough to not intersect the building itself
out.hw = targetObstruction.hw + delta;
out.hh = targetObstruction.hh + delta;
}
}
// Do nothing in particular in case we are already in range.
return true;
}
void CCmpUnitMotion::ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal)
{
#if DISABLE_PATHFINDER
{
CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from);
m_LongPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y });
return;
}
#endif
// If the target is close enough, hope that we'll be able to go straight next turn.
if (!ShouldAlternatePathfinder() && TryGoingStraightToTarget(from, false))
{
// NB: since we may fail to move straight next turn, we should edge our bets.
// Since the 'go straight' logic currently fires only if there's no short path,
// we'll compute a long path regardless to make sure _that_ stays up to date.
// (it's also extremely likely to be very fast to compute, so no big deal).
m_ShortPath.m_Waypoints.clear();
RequestLongPath(from, goal);
return;
}
// Otherwise we need to compute a path.
// If it's close then just do a short path, not a long path
// TODO: If it's close on the opposite side of a river then we really
// need a long path, so we shouldn't simply check linear distance
// the check is arbitrary but should be a reasonably small distance.
// We want to occasionally compute a long path if we're computing short-paths, because the short path domain
// is bounded and thus it can't around very large static obstacles.
// Likewise, we want to compile a short-path occasionally when the target is far because we might be stuck
// on a navcell surrounded by impassable navcells, but the short-pathfinder could move us out of there.
bool shortPath = InShortPathRange(goal, from);
if (ShouldAlternatePathfinder())
shortPath = !shortPath;
if (shortPath)
{
m_LongPath.m_Waypoints.clear();
// Extend the range so that our first path is probably valid.
RequestShortPath(from, goal, true);
}
else
{
m_ShortPath.m_Waypoints.clear();
RequestLongPath(from, goal);
}
}
void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal)
{
CmpPtr cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
// this is by how much our waypoints will be apart at most.
// this value here seems sensible enough.
PathGoal improvedGoal = goal;
improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1);
cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass);
m_ExpectedPathTicket.m_Type = Ticket::LONG_PATH;
m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId());
}
void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool extendRange)
{
CmpPtr cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
entity_pos_t searchRange = ShortPathSearchRange();
if (extendRange)
{
CFixedVector2D dist(from.X - goal.x, from.Y - goal.z);
if (dist.CompareLength(searchRange - entity_pos_t::FromInt(1)) >= 0)
{
searchRange = dist.Length() + fixed::FromInt(1);
if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE)
searchRange = SHORT_PATH_MAX_SEARCH_RANGE;
}
}
m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH;
m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, ShouldCollideWithMovingUnits(), GetGroup(), GetEntityId());
}
bool CCmpUnitMotion::MoveTo(MoveRequest request)
{
PROFILE("MoveTo");
if (request.m_MinRange == request.m_MaxRange && !request.m_MinRange.IsZero())
LOGWARNING("MaxRange must be larger than MinRange; See CCmpUnitMotion.cpp for more information");
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return false;
PathGoal goal;
if (!ComputeGoal(goal, request))
return false;
m_MoveRequest = request;
m_FailedMovements = 0;
m_FollowKnownImperfectPathCountdown = 0;
ComputePathToGoal(cmpPosition->GetPosition2D(), goal);
return true;
}
bool CCmpUnitMotion::IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
CmpPtr cmpPosition(GetEntityHandle());
if (!cmpPosition || !cmpPosition->IsInWorld())
return false;
MoveRequest request(target, minRange, maxRange);
PathGoal goal;
if (!ComputeGoal(goal, request))
return false;
CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
CFixedVector2D pos = cmpPosition->GetPosition2D();
return cmpPathfinder->IsGoalReachable(pos.X, pos.Y, goal, m_PassClass);
}
void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color)
{
bool floating = false;
CmpPtr cmpPosition(GetEntityHandle());
if (cmpPosition)
floating = cmpPosition->CanFloat();
lines.clear();
std::vector waypointCoords;
for (size_t i = 0; i < path.m_Waypoints.size(); ++i)
{
float x = path.m_Waypoints[i].x.ToFloat();
float z = path.m_Waypoints[i].z.ToFloat();
waypointCoords.push_back(x);
waypointCoords.push_back(z);
lines.push_back(SOverlayLine());
lines.back().m_Color = color;
SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating);
}
float x = cmpPosition->GetPosition2D().X.ToFloat();
float z = cmpPosition->GetPosition2D().Y.ToFloat();
waypointCoords.push_back(x);
waypointCoords.push_back(z);
lines.push_back(SOverlayLine());
lines.back().m_Color = color;
SimRender::ConstructLineOnGround(GetSimContext(), waypointCoords, lines.back(), floating);
}
void CCmpUnitMotion::RenderSubmit(SceneCollector& collector)
{
if (!m_DebugOverlayEnabled)
return;
RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH);
RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH);
for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i)
collector.Submit(&m_DebugOverlayLongPathLines[i]);
for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i)
collector.Submit(&m_DebugOverlayShortPathLines[i]);
}
#endif // INCLUDED_CCMPUNITMOTION
Index: ps/trunk/source/simulation2/components/tests/test_Pathfinder.h
===================================================================
--- ps/trunk/source/simulation2/components/tests/test_Pathfinder.h (revision 26268)
+++ ps/trunk/source/simulation2/components/tests/test_Pathfinder.h (revision 26269)
@@ -1,435 +1,429 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "simulation2/system/ComponentTest.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpPathfinder.h"
#include "simulation2/helpers/Grid.h"
#include "graphics/MapReader.h"
#include "graphics/Terrain.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/timer.h"
#include "lib/tex/tex.h"
#include "ps/Loader.h"
#include "ps/Pyrogenesis.h"
#include "scriptinterface/ScriptContext.h"
#include "simulation2/Simulation2.h"
#include
#include
class TestCmpPathfinder : public CxxTest::TestSuite
{
public:
void setUp()
{
g_VFS = CreateVfs();
g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST);
g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors
TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY));
CXeromyces::Startup();
-
- // Need some stuff for terrain movement costs:
- // (TODO: this ought to be independent of any graphics code)
- new CTerrainTextureManager;
- g_TexMan.LoadTerrainTextures();
}
void tearDown()
{
- delete &g_TexMan;
CXeromyces::Terminate();
g_VFS.reset();
DeleteDirectory(DataDir()/"_testcache");
}
void test_namespace()
{
// Check that Pathfinding::NAVCELL_SIZE is actually an integer and that the definitions
// of Pathfinding::NAVCELL_SIZE_INT and Pathfinding::NAVCELL_SIZE_LOG2 match
TS_ASSERT_EQUALS(Pathfinding::NAVCELL_SIZE.ToInt_RoundToNegInfinity(), Pathfinding::NAVCELL_SIZE.ToInt_RoundToInfinity());
TS_ASSERT_EQUALS(Pathfinding::NAVCELL_SIZE.ToInt_RoundToNearest(), Pathfinding::NAVCELL_SIZE_INT);
TS_ASSERT_EQUALS((Pathfinding::NAVCELL_SIZE >> 1).ToInt_RoundToZero(), Pathfinding::NAVCELL_SIZE_LOG2);
}
void test_pathgoal_nearest_distance()
{
entity_pos_t i = Pathfinding::NAVCELL_SIZE;
CFixedVector2D u(i*1, i*0);
CFixedVector2D v(i*0, i*1);
{
PathGoal goal = { PathGoal::POINT, i*8, i*6 };
TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*6);
TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*2);
TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*8 + v*6);
TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*10);
TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*12, i*9));
TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6));
TS_ASSERT(goal.RectContainsGoal(i*8, i*6, i*12, i*9));
TS_ASSERT(!goal.RectContainsGoal(i*4, i*3, i*7, i*5));
TS_ASSERT(!goal.RectContainsGoal(i*9, i*7, i*13, i*15));
}
{
PathGoal goal = { PathGoal::CIRCLE, i*8, i*6, i*5 };
TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*4);
TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*0);
TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*4 + v*3);
TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*5);
TS_ASSERT(goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside
TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside
TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // partially inside
TS_ASSERT(goal.RectContainsGoal(i*4, i*0, i*12, i*1)); // touching the edge
}
{
PathGoal goal = { PathGoal::INVERTED_CIRCLE, i*8, i*6, i*5 };
TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*1);
TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*3);
TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*0 + v*0);
TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*0);
TS_ASSERT(!goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside
TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside
TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // partially inside
TS_ASSERT(goal.RectContainsGoal(i*4, i*0, i*12, i*1)); // touching the edge
}
{
PathGoal goal = { PathGoal::SQUARE, i*8, i*6, i*4, i*3, u, v };
TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*4);
TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*0);
TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*4 + v*3);
TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*5);
TS_ASSERT(goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside
TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside
TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // partially inside
TS_ASSERT(goal.RectContainsGoal(i*4, i*2, i*12, i*3)); // touching the edge
TS_ASSERT(goal.RectContainsGoal(i*3, i*0, i*4, i*10)); // touching the edge
}
{
PathGoal goal = { PathGoal::INVERTED_SQUARE, i*8, i*6, i*4, i*3, u, v };
TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*3);
TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*1);
TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*0 + v*0);
TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*0);
TS_ASSERT(!goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside
TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside
TS_ASSERT(!goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // inside, touching (should fail)
TS_ASSERT(goal.RectContainsGoal(i*4, i*2, i*12, i*3)); // touching the edge
TS_ASSERT(goal.RectContainsGoal(i*3, i*0, i*4, i*10)); // touching the edge
}
}
void test_performance_DISABLED()
{
CTerrain terrain;
CSimulation2 sim2(NULL, g_ScriptContext, &terrain);
sim2.LoadDefaultScripts();
sim2.ResetState();
std::unique_ptr mapReader = std::make_unique();
LDR_BeginRegistering();
mapReader->LoadMap(L"maps/skirmishes/Median Oasis (2).pmp",
*sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue,
&terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
&sim2, &sim2.GetSimContext(), -1, false);
LDR_EndRegistering();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
sim2.PreInitGame();
sim2.InitGame();
sim2.Update(0);
CmpPtr cmp(sim2, SYSTEM_ENTITY);
#if 0
entity_pos_t x0 = entity_pos_t::FromInt(10);
entity_pos_t z0 = entity_pos_t::FromInt(495);
entity_pos_t x1 = entity_pos_t::FromInt(500);
entity_pos_t z1 = entity_pos_t::FromInt(495);
ICmpPathfinder::Goal goal = { ICmpPathfinder::Goal::POINT, x1, z1 };
WaypointPath path;
cmp->ComputePath(x0, z0, goal, cmp->GetPassabilityClass("default"), path);
for (size_t i = 0; i < path.m_Waypoints.size(); ++i)
printf("%d: %f %f\n", (int)i, path.m_Waypoints[i].x.ToDouble(), path.m_Waypoints[i].z.ToDouble());
#endif
double t = timer_Time();
std::mt19937 engine(42);
std::uniform_int_distribution distribution511(0, 511);
std::uniform_int_distribution distribution63(0, 63);
for (size_t j = 0; j < 1024*2; ++j)
{
entity_pos_t x0 = entity_pos_t::FromInt(distribution511(engine));
entity_pos_t z0 = entity_pos_t::FromInt(distribution511(engine));
entity_pos_t x1 = x0 + entity_pos_t::FromInt(distribution63(engine));
entity_pos_t z1 = z0 + entity_pos_t::FromInt(distribution63(engine));
PathGoal goal = { PathGoal::POINT, x1, z1 };
WaypointPath path;
cmp->ComputePathImmediate(x0, z0, goal, cmp->GetPassabilityClass("default"), path);
}
t = timer_Time() - t;
printf("[%f]", t);
}
void test_performance_short_DISABLED()
{
CTerrain terrain;
terrain.Initialize(5, NULL);
CSimulation2 sim2(NULL, g_ScriptContext, &terrain);
sim2.LoadDefaultScripts();
sim2.ResetState();
const entity_pos_t range = entity_pos_t::FromInt(48);
CmpPtr cmpObstructionMan(sim2, SYSTEM_ENTITY);
CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY);
std::mt19937 engine(42);
std::uniform_real_distribution distribution01(0.0f, std::nextafter(1.0f, 2.0f));
for (size_t i = 0; i < 200; ++i)
{
fixed x = fixed::FromFloat(1.5f*range.ToFloat() * distribution01(engine));
fixed z = fixed::FromFloat(1.5f*range.ToFloat() * distribution01(engine));
// printf("# %f %f\n", x.ToFloat(), z.ToFloat());
cmpObstructionMan->AddUnitShape(INVALID_ENTITY, x, z, fixed::FromInt(2), 0, INVALID_ENTITY);
}
PathGoal goal = { PathGoal::POINT, range, range };
WaypointPath path = cmpPathfinder->ComputeShortPathImmediate(ShortPathRequest{ 0, range/3, range/3, fixed::FromInt(2), range, goal, 0, false, 0, 0 });
for (size_t i = 0; i < path.m_Waypoints.size(); ++i)
printf("# %d: %f %f\n", (int)i, path.m_Waypoints[i].x.ToFloat(), path.m_Waypoints[i].z.ToFloat());
}
template
void DumpGrid(std::ostream& stream, const Grid& grid, int mask)
{
for (u16 j = 0; j < grid.m_H; ++j)
{
for (u16 i = 0; i < grid.m_W; )
{
if (!(grid.get(i, j) & mask))
{
i++;
continue;
}
u16 i0 = i;
for (i = i0+1; ; ++i)
{
if (i >= grid.m_W || !(grid.get(i, j) & mask))
{
stream << " \n";
break;
}
}
}
}
}
void test_perf2_DISABLED()
{
CTerrain terrain;
CSimulation2 sim2(NULL, g_ScriptContext, &terrain);
sim2.LoadDefaultScripts();
sim2.ResetState();
std::unique_ptr mapReader = std::make_unique();
LDR_BeginRegistering();
mapReader->LoadMap(L"maps/scenarios/Peloponnese.pmp",
*sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue,
&terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
&sim2, &sim2.GetSimContext(), -1, false);
LDR_EndRegistering();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
sim2.PreInitGame();
sim2.InitGame();
sim2.Update(0);
std::ofstream stream(OsString("perf2.html").c_str(), std::ofstream::out | std::ofstream::trunc);
CmpPtr cmpObstructionManager(sim2, SYSTEM_ENTITY);
CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY);
pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default");
const Grid& obstructions = cmpPathfinder->GetPassabilityGrid();
int scale = 1;
stream << "\n";
stream << "\n";
stream << "\n";
}
void test_perf3_DISABLED()
{
CTerrain terrain;
CSimulation2 sim2(NULL, g_ScriptContext, &terrain);
sim2.LoadDefaultScripts();
sim2.ResetState();
std::unique_ptr mapReader = std::make_unique();
LDR_BeginRegistering();
mapReader->LoadMap(L"maps/scenarios/Peloponnese.pmp",
*sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue,
&terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
&sim2, &sim2.GetSimContext(), -1, false);
LDR_EndRegistering();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
sim2.PreInitGame();
sim2.InitGame();
sim2.Update(0);
std::ofstream stream(OsString("perf3.html").c_str(), std::ofstream::out | std::ofstream::trunc);
CmpPtr cmpObstructionManager(sim2, SYSTEM_ENTITY);
CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY);
pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default");
const Grid& obstructions = cmpPathfinder->GetPassabilityGrid();
int scale = 31;
stream << "\n";
stream << "\n";
stream << "\n";
}
void DumpPath(std::ostream& stream, int i0, int j0, int i1, int j1, CmpPtr& cmpPathfinder)
{
entity_pos_t x0 = entity_pos_t::FromInt(i0);
entity_pos_t z0 = entity_pos_t::FromInt(j0);
entity_pos_t x1 = entity_pos_t::FromInt(i1);
entity_pos_t z1 = entity_pos_t::FromInt(j1);
PathGoal goal = { PathGoal::POINT, x1, z1 };
WaypointPath path;
cmpPathfinder->ComputePathImmediate(x0, z0, goal, cmpPathfinder->GetPassabilityClass("default"), path);
u32 debugSteps;
double debugTime;
Grid debugGrid;
cmpPathfinder->GetDebugData(debugSteps, debugTime, debugGrid);
// stream << " \n";
stream << " \n";
// stream << " \n";
// DumpGrid(stream, debugGrid, 1);
// stream << " \n";
// stream << " \n";
// DumpGrid(stream, debugGrid, 2);
// stream << " \n";
// stream << " \n";
// DumpGrid(stream, debugGrid, 3);
// stream << " \n";
stream << " \n";
stream << " \n";
}
void RepeatPath(int n, int i0, int j0, int i1, int j1, CmpPtr& cmpPathfinder)
{
entity_pos_t x0 = entity_pos_t::FromInt(i0);
entity_pos_t z0 = entity_pos_t::FromInt(j0);
entity_pos_t x1 = entity_pos_t::FromInt(i1);
entity_pos_t z1 = entity_pos_t::FromInt(j1);
PathGoal goal = { PathGoal::POINT, x1, z1 };
double t = timer_Time();
for (int i = 0; i < n; ++i)
{
WaypointPath path;
cmpPathfinder->ComputePathImmediate(x0, z0, goal, cmpPathfinder->GetPassabilityClass("default"), path);
}
t = timer_Time() - t;
debug_printf("### RepeatPath %fms each (%fs total)\n", 1000*t / n, t);
}
};
Index: ps/trunk/source/simulation2/helpers/Pathfinding.h
===================================================================
--- ps/trunk/source/simulation2/helpers/Pathfinding.h (revision 26268)
+++ ps/trunk/source/simulation2/helpers/Pathfinding.h (revision 26269)
@@ -1,244 +1,243 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_PATHFINDING
#define INCLUDED_PATHFINDING
#include "graphics/Terrain.h"
#include "maths/MathUtil.h"
#include "simulation2/system/Entity.h"
#include "PathGoal.h"
class CParamNode;
typedef u16 pass_class_t;
template
class Grid;
struct LongPathRequest
{
u32 ticket;
entity_pos_t x0;
entity_pos_t z0;
PathGoal goal;
pass_class_t passClass;
entity_id_t notify;
};
struct ShortPathRequest
{
u32 ticket;
entity_pos_t x0;
entity_pos_t z0;
entity_pos_t clearance;
entity_pos_t range;
PathGoal goal;
pass_class_t passClass;
bool avoidMovingUnits;
entity_id_t group;
entity_id_t notify;
};
struct Waypoint
{
entity_pos_t x, z;
};
/**
* Returned path.
* Waypoints are in *reverse* order (the earliest is at the back of the list)
*/
struct WaypointPath
{
std::vector m_Waypoints;
};
/**
* Represents the cost of a path consisting of horizontal/vertical and
* diagonal movements over a uniform-cost grid.
* Maximum path length before overflow is about 45K steps.
*/
struct PathCost
{
PathCost() : data(0) { }
/// Construct from a number of horizontal/vertical and diagonal steps
PathCost(u16 hv, u16 d)
: data(hv * 65536 + d * 92682) // 2^16 * sqrt(2) == 92681.9
{
}
/// Construct for horizontal/vertical movement of given number of steps
static PathCost horizvert(u16 n)
{
return PathCost(n, 0);
}
/// Construct for diagonal movement of given number of steps
static PathCost diag(u16 n)
{
return PathCost(0, n);
}
PathCost operator+(const PathCost& a) const
{
PathCost c;
c.data = data + a.data;
return c;
}
PathCost& operator+=(const PathCost& a)
{
data += a.data;
return *this;
}
bool operator<=(const PathCost& b) const { return data <= b.data; }
bool operator< (const PathCost& b) const { return data < b.data; }
bool operator>=(const PathCost& b) const { return data >= b.data; }
bool operator>(const PathCost& b) const { return data > b.data; }
u32 ToInt()
{
return data;
}
private:
u32 data;
};
inline constexpr int PASS_CLASS_BITS = 16;
typedef u16 NavcellData; // 1 bit per passability class (up to PASS_CLASS_BITS)
#define IS_PASSABLE(item, classmask) (((item) & (classmask)) == 0)
#define PASS_CLASS_MASK_FROM_INDEX(id) ((pass_class_t)(1u << id))
#define SPECIAL_PASS_CLASS PASS_CLASS_MASK_FROM_INDEX((PASS_CLASS_BITS-1)) // 16th bit, used for special in-place computations
namespace Pathfinding
{
/**
* The long-range pathfinder operates primarily over a navigation grid (a uniform-cost
* 2D passability grid, with horizontal/vertical (not diagonal) connectivity).
* This is based on the terrain tile passability, plus the rasterized shapes of
* obstructions, all expanded outwards by the radius of the units.
* Since units are much smaller than terrain tiles, the nav grid should be
* higher resolution than the tiles.
* We therefore split each the world into NxN "nav cells" (for some integer N,
* preferably a power of two).
*/
inline constexpr fixed NAVCELL_SIZE = fixed::FromInt(1);
inline constexpr int NAVCELL_SIZE_INT = 1;
inline constexpr int NAVCELL_SIZE_LOG2 = 0;
/**
* The terrain grid is coarser, and it is often convenient to convert from one to the other.
*/
inline constexpr int NAVCELLS_PER_TERRAIN_TILE = TERRAIN_TILE_SIZE / NAVCELL_SIZE_INT;
static_assert(TERRAIN_TILE_SIZE % NAVCELL_SIZE_INT == 0, "Terrain tile size is not a multiple of navcell size");
/**
* To make sure the long-range pathfinder is more strict than the short-range one,
* we need to slightly over-rasterize. So we extend the clearance radius by 1.
*/
inline constexpr entity_pos_t CLEARANCE_EXTENSION_RADIUS = fixed::FromInt(1);
/**
* Compute the navcell indexes on the grid nearest to a given point
* w, h are the grid dimensions, i.e. the number of navcells per side
*/
inline void NearestNavcell(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h)
{
// Use NAVCELL_SIZE_INT to save the cost of dividing by a fixed
i = static_cast(Clamp((x / NAVCELL_SIZE_INT).ToInt_RoundToNegInfinity(), 0, w - 1));
j = static_cast(Clamp((z / NAVCELL_SIZE_INT).ToInt_RoundToNegInfinity(), 0, h - 1));
}
/**
* Returns the position of the center of the given terrain tile
*/
inline void TerrainTileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
{
static_assert(TERRAIN_TILE_SIZE % 2 == 0);
x = entity_pos_t::FromInt(i*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE / 2);
z = entity_pos_t::FromInt(j*(int)TERRAIN_TILE_SIZE + (int)TERRAIN_TILE_SIZE / 2);
}
inline void NavcellCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z)
{
x = entity_pos_t::FromInt(i * 2 + 1).Multiply(NAVCELL_SIZE / 2);
z = entity_pos_t::FromInt(j * 2 + 1).Multiply(NAVCELL_SIZE / 2);
}
/*
* Checks that the line (x0,z0)-(x1,z1) does not intersect any impassable navcells.
*/
bool CheckLineMovement(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1,
pass_class_t passClass, const Grid& grid);
}
/*
* For efficient pathfinding we want to try hard to minimise the per-tile search cost,
- * so we precompute the tile passability flags and movement costs for the various different
- * types of unit.
+ * so we precompute the tile passability flags for the various different types of unit.
* We also want to minimise memory usage (there can easily be 100K tiles so we don't want
* to store many bytes for each).
*
* To handle passability efficiently, we have a small number of passability classes
* (e.g. "infantry", "ship"). Each unit belongs to a single passability class, and
* uses that for all its pathfinding.
* Passability is determined by water depth, terrain slope, forestness, buildingness.
* We need at least one bit per class per tile to represent passability.
*
* Not all pass classes are used for actual pathfinding. The pathfinder calls
* CCmpObstructionManager's Rasterize() to add shapes onto the passability grid.
* Which shapes are rasterized depend on the value of the m_Obstructions of each passability
* class.
*
* Passabilities not used for unit pathfinding should not use the Clearance attribute, and
* will get a zero clearance value.
*/
class PathfinderPassability
{
public:
PathfinderPassability(pass_class_t mask, const CParamNode& node);
bool IsPassable(fixed waterdepth, fixed steepness, fixed shoredist) const
{
return ((m_MinDepth <= waterdepth && waterdepth <= m_MaxDepth) && (steepness < m_MaxSlope) && (m_MinShore <= shoredist && shoredist <= m_MaxShore));
}
pass_class_t m_Mask;
fixed m_Clearance; // min distance from static obstructions
enum ObstructionHandling
{
NONE,
PATHFINDING,
FOUNDATION
};
ObstructionHandling m_Obstructions;
private:
fixed m_MinDepth;
fixed m_MaxDepth;
fixed m_MaxSlope;
fixed m_MinShore;
fixed m_MaxShore;
};
#endif // INCLUDED_PATHFINDING
Index: ps/trunk/source/simulation2/tests/test_Serializer.h
===================================================================
--- ps/trunk/source/simulation2/tests/test_Serializer.h (revision 26268)
+++ ps/trunk/source/simulation2/tests/test_Serializer.h (revision 26269)
@@ -1,947 +1,941 @@
-/* Copyright (C) 2021 Wildfire Games.
+/* Copyright (C) 2022 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "lib/self_test.h"
#include "simulation2/serialization/DebugSerializer.h"
#include "simulation2/serialization/HashSerializer.h"
#include "simulation2/serialization/StdSerializer.h"
#include "simulation2/serialization/StdDeserializer.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include "graphics/MapReader.h"
#include "graphics/Terrain.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/config2.h"
#include "lib/timer.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Loader.h"
#include "ps/XML/Xeromyces.h"
#include "simulation2/Simulation2.h"
#if CONFIG2_VALGRIND
# include "callgrind.h"
#endif
#include
#define TS_ASSERT_STREAM(stream, len, buffer) \
TS_ASSERT_EQUALS(stream.str().length(), (size_t)len); \
TS_ASSERT_SAME_DATA(stream.str().data(), buffer, len)
#define TSM_ASSERT_STREAM(m, stream, len, buffer) \
TSM_ASSERT_EQUALS(m, stream.str().length(), (size_t)len); \
TSM_ASSERT_SAME_DATA(m, stream.str().data(), buffer, len)
class TestSerializer : public CxxTest::TestSuite
{
public:
void serialize_types(ISerializer& serialize)
{
serialize.NumberI8_Unbounded("i8", (signed char)-123);
serialize.NumberU8_Unbounded("u8", (unsigned char)255);
serialize.NumberI16_Unbounded("i16", -12345);
serialize.NumberU16_Unbounded("u16", 56789);
serialize.NumberI32_Unbounded("i32", -123);
serialize.NumberU32_Unbounded("u32", (unsigned)-123);
serialize.NumberFloat_Unbounded("float", 1e+30f);
serialize.NumberDouble_Unbounded("double", 1e+300);
serialize.NumberFixed_Unbounded("fixed", fixed::FromFloat(1234.5f));
serialize.Bool("t", true);
serialize.Bool("f", false);
serialize.StringASCII("string", "example", 0, 255);
serialize.StringASCII("string 2", "example\"\\\"", 0, 255);
serialize.StringASCII("string 3", "example\n\ntest", 0, 255);
wchar_t testw[] = { 't', 0xEA, 's', 't', 0 };
serialize.String("string 4", testw, 0, 255);
serialize.RawBytes("raw bytes", (const u8*)"\0\1\2\3\x0f\x10", 6);
}
void test_Debug_basic()
{
ScriptInterface script("Test", "Test", g_ScriptContext);
std::stringstream stream;
CDebugSerializer serialize(script, stream);
serialize.NumberI32_Unbounded("x", -123);
serialize.NumberU32_Unbounded("y", 1234);
serialize.NumberI32("z", 12345, 0, 65535);
TS_ASSERT_STR_EQUALS(stream.str(), "x: -123\ny: 1234\nz: 12345\n");
}
void test_Debug_floats()
{
ScriptInterface script("Test", "Test", g_ScriptContext);
std::stringstream stream;
CDebugSerializer serialize(script, stream);
serialize.NumberFloat_Unbounded("x", 1e4f);
serialize.NumberFloat_Unbounded("x", 1e-4f);
serialize.NumberFloat_Unbounded("x", 1e5f);
serialize.NumberFloat_Unbounded("x", 1e-5f);
serialize.NumberFloat_Unbounded("x", 1e6f);
serialize.NumberFloat_Unbounded("x", 1e-6f);
serialize.NumberFloat_Unbounded("x", 1e10f);
serialize.NumberFloat_Unbounded("x", 1e-10f);
serialize.NumberDouble_Unbounded("x", 1e4);
serialize.NumberDouble_Unbounded("x", 1e-4);
serialize.NumberDouble_Unbounded("x", 1e5);
serialize.NumberDouble_Unbounded("x", 1e-5);
serialize.NumberDouble_Unbounded("x", 1e6);
serialize.NumberDouble_Unbounded("x", 1e-6);
serialize.NumberDouble_Unbounded("x", 1e10);
serialize.NumberDouble_Unbounded("x", 1e-10);
serialize.NumberDouble_Unbounded("x", 1e100);
serialize.NumberDouble_Unbounded("x", 1e-100);
serialize.NumberFixed_Unbounded("x", fixed::FromDouble(1e4));
TS_ASSERT_STR_EQUALS(stream.str(),
"x: 10000\nx: 9.9999997e-05\nx: 100000\nx: 9.9999997e-06\nx: 1000000\nx: 1e-06\nx: 1e+10\nx: 1e-10\n"
"x: 10000\nx: 0.0001\nx: 100000\nx: 1.0000000000000001e-05\nx: 1000000\nx: 9.9999999999999995e-07\nx: 10000000000\nx: 1e-10\nx: 1e+100\nx: 1e-100\n"
"x: 10000\n"
);
}
void test_Debug_types()
{
ScriptInterface script("Test", "Test", g_ScriptContext);
std::stringstream stream;
CDebugSerializer serialize(script, stream);
serialize.Comment("comment");
serialize_types(serialize);
TS_ASSERT_STR_EQUALS(stream.str(),
"# comment\n"
"i8: -123\n"
"u8: 255\n"
"i16: -12345\n"
"u16: 56789\n"
"i32: -123\n"
"u32: 4294967173\n"
"float: 1e+30\n"
"double: 1.0000000000000001e+300\n"
"fixed: 1234.5\n"
"t: true\n"
"f: false\n"
"string: \"example\"\n"
"string 2: \"example\\\"\\\\\\\"\"\n" // C-escaped form of: "example\"\\\""
"string 3: \"example\\n\\ntest\"\n"
"string 4: \"t\xC3\xAAst\"\n"
"raw bytes: (6 bytes) 00 01 02 03 0f 10\n"
);
}
void test_Std_basic()
{
ScriptInterface script("Test", "Test", g_ScriptContext);
std::stringstream stream;
CStdSerializer serialize(script, stream);
serialize.NumberI32_Unbounded("x", -123);
serialize.NumberU32_Unbounded("y", 1234);
serialize.NumberI32("z", 12345, 0, 65535);
TS_ASSERT_STREAM(stream, 12, "\x85\xff\xff\xff" "\xd2\x04\x00\x00" "\x39\x30\x00\x00");
CStdDeserializer deserialize(script, stream);
int32_t n;
deserialize.NumberI32_Unbounded("x", n);
TS_ASSERT_EQUALS(n, -123);
deserialize.NumberI32_Unbounded("y", n);
TS_ASSERT_EQUALS(n, 1234);
deserialize.NumberI32("z", n, 0, 65535);
TS_ASSERT_EQUALS(n, 12345);
// NOTE: Don't use good() here - it fails due to a bug in older libc++ versions
TS_ASSERT(!stream.bad() && !stream.fail());
TS_ASSERT_EQUALS(stream.peek(), EOF);
}
void test_Std_types()
{
ScriptInterface script("Test", "Test", g_ScriptContext);
std::stringstream stream;
CStdSerializer serialize(script, stream);
serialize_types(serialize);
CStdDeserializer deserialize(script, stream);
int8_t i8v;
uint8_t u8v;
int16_t i16v;
uint16_t u16v;
int32_t i32v;
uint32_t u32v;
float flt;
double dbl;
fixed fxd;
bool bl;
std::string str;
std::wstring wstr;
u8 cbuf[256];
deserialize.NumberI8_Unbounded("i8", i8v);
TS_ASSERT_EQUALS(i8v, -123);
deserialize.NumberU8_Unbounded("u8", u8v);
TS_ASSERT_EQUALS(u8v, 255);
deserialize.NumberI16_Unbounded("i16", i16v);
TS_ASSERT_EQUALS(i16v, -12345);
deserialize.NumberU16_Unbounded("u16", u16v);
TS_ASSERT_EQUALS(u16v, 56789);
deserialize.NumberI32_Unbounded("i32", i32v);
TS_ASSERT_EQUALS(i32v, -123);
deserialize.NumberU32_Unbounded("u32", u32v);
TS_ASSERT_EQUALS(u32v, 4294967173u);
deserialize.NumberFloat_Unbounded("float", flt);
TS_ASSERT_EQUALS(flt, 1e+30f);
deserialize.NumberDouble_Unbounded("double", dbl);
TS_ASSERT_EQUALS(dbl, 1e+300);
deserialize.NumberFixed_Unbounded("fixed", fxd);
TS_ASSERT_EQUALS(fxd.ToDouble(), 1234.5);
deserialize.Bool("t", bl);
TS_ASSERT_EQUALS(bl, true);
deserialize.Bool("f", bl);
TS_ASSERT_EQUALS(bl, false);
deserialize.StringASCII("string", str, 0, 255);
TS_ASSERT_STR_EQUALS(str, "example");
deserialize.StringASCII("string 2", str, 0, 255);
TS_ASSERT_STR_EQUALS(str, "example\"\\\"");
deserialize.StringASCII("string 3", str, 0, 255);
TS_ASSERT_STR_EQUALS(str, "example\n\ntest");
wchar_t testw[] = { 't', 0xEA, 's', 't', 0 };
deserialize.String("string 4", wstr, 0, 255);
TS_ASSERT_WSTR_EQUALS(wstr, testw);
cbuf[6] = 0x42; // sentinel
deserialize.RawBytes("raw bytes", cbuf, 6);
TS_ASSERT_SAME_DATA(cbuf, (const u8*)"\0\1\2\3\x0f\x10\x42", 7);
// NOTE: Don't use good() here - it fails due to a bug in older libc++ versions
TS_ASSERT(!stream.bad() && !stream.fail());
TS_ASSERT_EQUALS(stream.peek(), EOF);
}
void test_Hash_basic()
{
ScriptInterface script("Test", "Test", g_ScriptContext);
CHashSerializer serialize(script);
serialize.NumberI32_Unbounded("x", -123);
serialize.NumberU32_Unbounded("y", 1234);
serialize.NumberI32("z", 12345, 0, 65535);
TS_ASSERT_EQUALS(serialize.GetHashLength(), (size_t)16);
TS_ASSERT_SAME_DATA(serialize.ComputeHash(), "\xa0\x3a\xe5\x3e\x9b\xd7\xfb\x11\x88\x35\xc6\xfb\xb9\x94\xa9\x72", 16);
// echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g'
}
void test_Hash_stream()
{
ScriptInterface script("Test", "Test", g_ScriptContext);
CHashSerializer hashSerialize(script);
hashSerialize.NumberI32_Unbounded("x", -123);
hashSerialize.NumberU32_Unbounded("y", 1234);
hashSerialize.NumberI32("z", 12345, 0, 65535);
ISerializer& serialize = hashSerialize;
{
CStdSerializer streamSerialize(script, serialize.GetStream());
streamSerialize.NumberI32_Unbounded("x2", -456);
streamSerialize.NumberU32_Unbounded("y2", 5678);
streamSerialize.NumberI32("z2", 45678, 0, 65535);
}
TS_ASSERT_EQUALS(hashSerialize.GetHashLength(), (size_t)16);
TS_ASSERT_SAME_DATA(hashSerialize.ComputeHash(), "\x5c\xff\x33\xd1\x72\xdd\x6d\x77\xa8\xd4\xa1\xf6\x84\xcc\xaa\x10", 16);
// echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00\x38\xfe\xff\xff\x2e\x16\x00\x00\x6e\xb2\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g'
}
void test_bounds()
{
ScriptInterface script("Test", "Test", g_ScriptContext);
std::stringstream stream;
CDebugSerializer serialize(script, stream);
serialize.NumberI32("x", 16, -16, 16);
serialize.NumberI32("x", -16, -16, 16);
TS_ASSERT_THROWS(serialize.NumberI32("x", 99, -16, 16), const PSERROR_Serialize_OutOfBounds&);
TS_ASSERT_THROWS(serialize.NumberI32("x", -17, -16, 16), const PSERROR_Serialize_OutOfBounds&);
}
// TODO: test exceptions more thoroughly
void helper_script_roundtrip(const char* msg, const char* input, const char* expected, size_t expstreamlen = 0, const char* expstream = NULL, const char* debug = NULL)
{
ScriptInterface script("Test", "Test", g_ScriptContext);
ScriptRequest rq(script);
JS::RootedValue obj(rq.cx);
TSM_ASSERT(msg, script.Eval(input, &obj));
if (debug)
{
std::stringstream dbgstream;
CDebugSerializer serialize(script, dbgstream);
serialize.ScriptVal("script", &obj);
TS_ASSERT_STR_EQUALS(dbgstream.str(), debug);
}
std::stringstream stream;
CStdSerializer serialize(script, stream);
serialize.ScriptVal("script", &obj);
if (expstream)
{
TSM_ASSERT_STREAM(msg, stream, expstreamlen, expstream);
}
CStdDeserializer deserialize(script, stream);
JS::RootedValue newobj(rq.cx);
deserialize.ScriptVal("script", &newobj);
// NOTE: Don't use good() here - it fails due to a bug in older libc++ versions
TSM_ASSERT(msg, !stream.bad() && !stream.fail());
TSM_ASSERT_EQUALS(msg, stream.peek(), EOF);
std::stringstream stream2;
CStdSerializer serialize2(script, stream2);
CStdDeserializer deserialize2(script, stream2);
// Round-trip the deserialized value again. This helps ensure prototypes are correctly deserialized.
serialize2.ScriptVal("script2", &newobj);
deserialize2.ScriptVal("script2", &newobj);
std::string source;
TSM_ASSERT(msg, ScriptFunction::Call(rq, newobj, "toSource", source));
TS_ASSERT_STR_EQUALS(source, expected);
}
void test_script_basic()
{
helper_script_roundtrip("Object",
"({'x': 123, 'y': [1, 1.5, '2', 'test', undefined, null, true, false]})",
/* expected: */
"({x:123, y:[1, 1.5, \"2\", \"test\", (void 0), null, true, false]})",
/* expected stream: */
116,
"\x03" // SCRIPT_TYPE_OBJECT
"\x02\0\0\0" // num props
"\x01\x01\0\0\0" "x" // "x"
"\x05" // SCRIPT_TYPE_INT
"\x7b\0\0\0" // 123
"\x01\x01\0\0\0" "y" // "y"
"\x02" // SCRIPT_TYPE_ARRAY
"\x08\0\0\0" // array length
"\x08\0\0\0" // num props
"\x01\x01\0\0\0" "0" // "0"
"\x05" "\x01\0\0\0" // SCRIPT_TYPE_INT 1
"\x01\x01\0\0\0" "1" // "1"
"\x06" "\0\0\0\0\0\0\xf8\x3f" // SCRIPT_TYPE_DOUBLE 1.5
"\x01\x01\0\0\0" "2" // "2"
"\x04" "\x01\x01\0\0\0" "2" // SCRIPT_TYPE_STRING "2"
"\x01\x01\0\0\0" "3" // "3"
"\x04" "\x01\x04\0\0\0" "test" // SCRIPT_TYPE_STRING "test"
"\x01\x01\0\0\0" "4" // "4"
"\x00" // SCRIPT_TYPE_VOID
"\x01\x01\0\0\0" "5" // "5"
"\x01" // SCRIPT_TYPE_NULL
"\x01\x01\0\0\0" "6" // "6"
"\x07" "\x01" // SCRIPT_TYPE_BOOLEAN true
"\x01\x01\0\0\0" "7" // "7"
"\x07" "\x00", // SCRIPT_TYPE_BOOLEAN false
/* expected debug: */
"script: {\n"
" \"x\": 123,\n"
" \"y\": [\n"
" 1,\n"
" 1.5,\n"
" \"2\",\n"
" \"test\",\n"
" null,\n"
" null,\n"
" true,\n"
" false\n"
" ]\n"
"}\n"
);
}
void test_script_unicode()
{
helper_script_roundtrip("unicode", "({"
"'x': \"\\x01\\x80\\xff\\u0100\\ud7ff\", "
"'y': \"\\ue000\\ufffd\""
"})",
/* expected: */
"({"
"x:\"\\x01\\x80\\xFF\\u0100\\uD7FF\", "
"y:\"\\uE000\\uFFFD\""
"})");
// Disabled since we no longer do the UTF-8 conversion that rejects invalid characters
// TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 1", "(\"\\ud7ff\\ud800\")", "..."), PSERROR_Serialize_InvalidCharInString);
// TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 2", "(\"\\udfff\")", "..."), PSERROR_Serialize_InvalidCharInString);
// TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 3", "(\"\\uffff\")", "..."), PSERROR_Serialize_InvalidCharInString);
// TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 4", "(\"\\ud800\\udc00\")" /* U+10000 */, "..."), PSERROR_Serialize_InvalidCharInString);
helper_script_roundtrip("unicode", "\"\\ud800\\uffff\"", "(new String(\"\\uD800\\uFFFF\"))");
}
void test_script_objects()
{
helper_script_roundtrip("Number", "[1, new Number('2.0'), 3]", "[1, (new Number(2)), 3]");
helper_script_roundtrip("Number with props", "var n=new Number('2.0'); n.foo='bar'; n", "(new Number(2))");
helper_script_roundtrip("String", "['test1', new String('test2'), 'test3']", "[\"test1\", (new String(\"test2\")), \"test3\"]");
helper_script_roundtrip("String with props", "var s=new String('test'); s.foo='bar'; s", "(new String(\"test\"))");
helper_script_roundtrip("Boolean", "[new Boolean('true'), false]", "[(new Boolean(true)), false]");
helper_script_roundtrip("Boolean with props", "var b=new Boolean('true'); b.foo='bar'; b", "(new Boolean(true))");
}
void test_script_fancy_objects()
{
// This asserts that objects are deserialized with their correct prototypes.
helper_script_roundtrip("Custom Object", ""
"function customObj() { this.a = this.customFunc.name; };"
"customObj.prototype.customFunc = function customFunc(){};"
"new customObj();", "({a:\"customFunc\"})");
helper_script_roundtrip("Custom Class", ""
"class customObj {"
" constructor() { this.a = this.customFunc.name; }"
" customFunc(){};"
"}; new customObj();", "({a:\"customFunc\"})");
helper_script_roundtrip("Custom Class with Serialize/Deserialize()", ""
"class customObj {"
" constructor() { this.a = this.customFunc.name; }"
" Serialize() { return { 'foo': 'bar' }; }"
" Deserialize(data) { this.foo = data.foo; }"
" customFunc(){};"
"}; new customObj();", "({a:\"customFunc\", foo:\"bar\"})");
helper_script_roundtrip("Custom Class with null serialize & deserialize()", ""
"class customObj {"
" constructor() { this.a = this.customFunc.name; }"
" Deserialize(data) { this.test = 'test'; };"
" customFunc(){};"
"}; customObj.prototype.Serialize=null;"
"new customObj();", "({a:\"customFunc\", test:\"test\"})");
helper_script_roundtrip("Custom Class with arguments but still works", ""
"class customObj {"
" constructor(test) { this.a = test; }"
" Serialize() { return { 'data': this.a }; };"
" Deserialize(data) { this.a = data.data; };"
"}; new customObj(4);", "({a:4})");
}
void test_script_objects_properties()
{
helper_script_roundtrip("Object with null in prop name", "({\"foo\\0bar\":1})", "({\'foo\\x00bar\':1})");
}
void test_script_typed_arrays_simple()
{
helper_script_roundtrip("Int8Array",
"var arr=new Int8Array(8);"
"for(var i=0; iMount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST));
TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY));
- // Need some stuff for terrain movement costs:
- // (TODO: this ought to be independent of any graphics code)
- new CTerrainTextureManager;
- g_TexMan.LoadTerrainTextures();
-
CTerrain terrain;
CSimulation2 sim2(NULL, g_ScriptContext, &terrain);
sim2.LoadDefaultScripts();
sim2.ResetState();
std::unique_ptr mapReader = std::make_unique();
LDR_BeginRegistering();
mapReader->LoadMap(L"maps/skirmishes/Greek Acropolis (2).pmp",
*sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue,
&terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
&sim2, &sim2.GetSimContext(), -1, false);
LDR_EndRegistering();
TS_ASSERT_OK(LDR_NonprogressiveLoad());
sim2.Update(0);
{
std::stringstream str;
std::string hash;
sim2.SerializeState(str);
sim2.ComputeStateHash(hash, false);
debug_printf("\n");
debug_printf("# size = %d\n", (int)str.str().length());
debug_printf("# hash = ");
for (size_t i = 0; i < hash.size(); ++i)
debug_printf("%02x", (unsigned int)(u8)hash[i]);
debug_printf("\n");
}
double t = timer_Time();
#if CONFIG2_VALGRIND
CALLGRIND_START_INSTRUMENTATION;
#endif
size_t reps = 128;
for (size_t i = 0; i < reps; ++i)
{
std::string hash;
sim2.ComputeStateHash(hash, false);
}
#if CONFIG2_VALGRIND
CALLGRIND_STOP_INSTRUMENTATION;
#endif
t = timer_Time() - t;
debug_printf("# time = %f (%f/%d)\n", t/reps, t, (int)reps);
// Shut down the world
- delete &g_TexMan;
g_VFS.reset();
DeleteDirectory(DataDir()/"_testcache");
CXeromyces::Terminate();
}
};