Index: ps/trunk/source/graphics/MapReader.cpp
===================================================================
--- ps/trunk/source/graphics/MapReader.cpp (revision 24267)
+++ ps/trunk/source/graphics/MapReader.cpp (revision 24268)
@@ -1,1611 +1,1617 @@
/* Copyright (C) 2020 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/ScriptContext.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);
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);
ScriptInterface::CreateObject(rq, ret);
if (m_ScriptSettings.empty())
return;
JS::RootedValue scriptSettingsVal(rq.cx);
scriptInterface.ParseJSON(m_ScriptSettings, &scriptSettingsVal);
scriptInterface.SetProperty(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, at_y, at_z;
+ 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.GetElementString(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.GetElementString(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(terrainambientcolor);
EL(unitsambientcolor);
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_terrainambientcolor)
{
m_MapReader.m_LightEnv.m_TerrainAmbientColor = RGBColor(
attrs.GetNamedItem(at_r).ToFloat(),
attrs.GetNamedItem(at_g).ToFloat(),
attrs.GetNamedItem(at_b).ToFloat());
}
else if (element_name == el_unitsambientcolor)
{
m_MapReader.m_LightEnv.m_UnitsAmbientColor = 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 element_name = fog.GetNodeName();
- if (element_name == el_fogcolor)
+ int fog_element_name = fog.GetNodeName();
+ if (fog_element_name == el_fogcolor)
{
- XMBAttributeList attrs = fog.GetAttributes();
+ XMBAttributeList fogAttributes = fog.GetAttributes();
m_MapReader.m_LightEnv.m_FogColor = RGBColor(
- attrs.GetNamedItem(at_r).ToFloat(),
- attrs.GetNamedItem(at_g).ToFloat(),
- attrs.GetNamedItem(at_b).ToFloat());
+ fogAttributes.GetNamedItem(at_r).ToFloat(),
+ fogAttributes.GetNamedItem(at_g).ToFloat(),
+ fogAttributes.GetNamedItem(at_b).ToFloat());
}
- else if (element_name == el_fogfactor)
+ else if (fog_element_name == el_fogfactor)
{
m_MapReader.m_LightEnv.m_FogFactor = fog.GetText().ToFloat();
}
- else if (element_name == el_fogthickness)
+ 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 element_name = postproc.GetNodeName();
- if (element_name == el_brightness)
+ int post_element_name = postproc.GetNodeName();
+ if (post_element_name == el_brightness)
{
m_MapReader.m_LightEnv.m_Brightness = postproc.GetText().ToFloat();
}
- else if (element_name == el_contrast)
+ else if (post_element_name == el_contrast)
{
m_MapReader.m_LightEnv.m_Contrast = postproc.GetText().ToFloat();
}
- else if (element_name == el_saturation)
+ else if (post_element_name == el_saturation)
{
m_MapReader.m_LightEnv.m_Saturation = postproc.GetText().ToFloat();
}
- else if (element_name == el_bloom)
+ else if (post_element_name == el_bloom)
{
m_MapReader.m_LightEnv.m_Bloom = postproc.GetText().ToFloat();
}
- else if (element_name == el_posteffect)
+ 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 element_name = waterelement.GetNodeName();
- if (element_name == el_height)
+ 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 (element_name == el_type)
+ 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 (element_name == el) \
+ else if (water_element_name == el) \
{ \
- XMBAttributeList attrs = waterelement.GetAttributes(); \
+ XMBAttributeList colorAttrs = waterelement.GetAttributes(); \
out = CColor( \
- attrs.GetNamedItem(at_r).ToFloat(), \
- attrs.GetNamedItem(at_g).ToFloat(), \
- attrs.GetNamedItem(at_b).ToFloat(), \
+ colorAttrs.GetNamedItem(at_r).ToFloat(), \
+ colorAttrs.GetNamedItem(at_g).ToFloat(), \
+ colorAttrs.GetNamedItem(at_b).ToFloat(), \
1.f); \
}
#define READ_FLOAT(el, out) \
- else if (element_name == el) \
+ 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 attrs = setting.GetAttributes();
+ XMBAttributeList positionAttrs = setting.GetAttributes();
Position = CFixedVector3D(
- fixed::FromString(attrs.GetNamedItem(at_x)),
- fixed::FromString(attrs.GetNamedItem(at_y)),
- fixed::FromString(attrs.GetNamedItem(at_z)));
+ 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 attrs = setting.GetAttributes();
+ XMBAttributeList orientationAttrs = setting.GetAttributes();
Orientation = CFixedVector3D(
- fixed::FromString(attrs.GetNamedItem(at_x)),
- fixed::FromString(attrs.GetNamedItem(at_y)),
- fixed::FromString(attrs.GetNamedItem(at_z)));
+ 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 attrs = setting.GetAttributes();
- ControlGroup = attrs.GetNamedItem(at_group).ToInt();
- ControlGroup2 = attrs.GetNamedItem(at_group2).ToInt();
+ 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 attrs = garr_ent.GetAttributes();
- Garrison.push_back(attrs.GetNamedItem(at_uid).ToInt());
+ 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 attrs = turretPoint.GetAttributes();
+ XMBAttributeList turretAttrs = turretPoint.GetAttributes();
Turrets.push_back(std::make_pair(
- attrs.GetNamedItem(at_turret),
- attrs.GetNamedItem(at_uid).ToInt()
+ 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
}
CmpPtr cmpOwnership(sim, ent);
if (cmpOwnership)
cmpOwnership->SetOwner(PlayerID);
if (!Garrison.empty())
{
CmpPtr cmpGarrisonHolder(sim, ent);
if (cmpGarrisonHolder)
cmpGarrisonHolder->SetInitEntities(Garrison);
else
LOGERROR("CXMLMapReader::ReadEntities() entity '%d' of player '%d' has no GarrisonHolder component and thus cannot garrison units.", ent, PlayerID);
Garrison.clear();
}
if (!Turrets.empty())
{
CmpPtr cmpTurretHolder(sim, ent);
if (cmpTurretHolder)
cmpTurretHolder->SetInitEntities(Turrets);
else
LOGERROR("CXMLMapReader::ReadEntities() entity '%d' of player '%d' has no TurretHolder component and thus cannot use turrets.", ent, PlayerID);
Turrets.clear();
}
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 = pSimulation2->GetScriptInterface().StringifyJSON(&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
ScriptInterface::StructuredClone results = m_MapGen->GetResults();
// Parse data into simulation context
JS::RootedValue data(rq.cx);
pSimulation2->GetScriptInterface().ReadStructuredClone(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 (!pSimulation2->GetScriptInterface().GetProperty(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);
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 (!pSimulation2->GetScriptInterface().GetProperty(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 (!pSimulation2->GetScriptInterface().GetProperty(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 terrainAmbientColor;
GET_ENVIRONMENT_PROPERTY(envObj, TerrainAmbientColor, terrainAmbientColor)
m_LightEnv.m_TerrainAmbientColor = RGBColor(terrainAmbientColor.r, terrainAmbientColor.g, terrainAmbientColor.b);
CColor unitsAmbientColor;
GET_ENVIRONMENT_PROPERTY(envObj, UnitsAmbientColor, unitsAmbientColor)
m_LightEnv.m_UnitsAmbientColor = RGBColor(unitsAmbientColor.r, unitsAmbientColor.g, unitsAmbientColor.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 (!pSimulation2->GetScriptInterface().GetProperty(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/ParticleEmitter.cpp
===================================================================
--- ps/trunk/source/graphics/ParticleEmitter.cpp (revision 24267)
+++ ps/trunk/source/graphics/ParticleEmitter.cpp (revision 24268)
@@ -1,299 +1,299 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* Copyright (C) 2020 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 "ParticleEmitter.h"
#include "graphics/LightEnv.h"
#include "graphics/LOSTexture.h"
#include "graphics/ParticleEmitterType.h"
#include "graphics/ParticleManager.h"
#include "graphics/ShaderProgram.h"
#include "graphics/TextureManager.h"
#include "renderer/Renderer.h"
CParticleEmitter::CParticleEmitter(const CParticleEmitterTypePtr& type) :
m_Type(type), m_Active(true), m_NextParticleIdx(0), m_EmissionRoundingError(0.f),
m_LastUpdateTime(type->m_Manager.GetCurrentTime()),
m_IndexArray(GL_STATIC_DRAW),
m_VertexArray(GL_DYNAMIC_DRAW),
m_LastFrameNumber(-1)
{
// If we should start with particles fully emitted, pretend that we
// were created in the past so the first update will produce lots of
// particles.
// TODO: instead of this, maybe it would make more sense to do a full
// lifetime-length update of all emitters when the game first starts
// (so that e.g. buildings constructed later on won't have fully-started
// emitters, but those at the start will)?
if (m_Type->m_StartFull)
m_LastUpdateTime -= m_Type->m_MaxLifetime;
m_Particles.reserve(m_Type->m_MaxParticles);
m_AttributePos.type = GL_FLOAT;
m_AttributePos.elems = 3;
m_VertexArray.AddAttribute(&m_AttributePos);
m_AttributeAxis.type = GL_FLOAT;
m_AttributeAxis.elems = 2;
m_VertexArray.AddAttribute(&m_AttributeAxis);
m_AttributeUV.type = GL_FLOAT;
m_AttributeUV.elems = 2;
m_VertexArray.AddAttribute(&m_AttributeUV);
m_AttributeColor.type = GL_UNSIGNED_BYTE;
m_AttributeColor.elems = 4;
m_VertexArray.AddAttribute(&m_AttributeColor);
m_VertexArray.SetNumVertices(m_Type->m_MaxParticles * 4);
m_VertexArray.Layout();
m_IndexArray.SetNumVertices(m_Type->m_MaxParticles * 6);
m_IndexArray.Layout();
VertexArrayIterator index = m_IndexArray.GetIterator();
- for (size_t i = 0; i < m_Type->m_MaxParticles; ++i)
+ for (u16 i = 0; i < m_Type->m_MaxParticles; ++i)
{
*index++ = i*4 + 0;
*index++ = i*4 + 1;
*index++ = i*4 + 2;
*index++ = i*4 + 2;
*index++ = i*4 + 3;
*index++ = i*4 + 0;
}
m_IndexArray.Upload();
m_IndexArray.FreeBackingStore();
}
void CParticleEmitter::UpdateArrayData(int frameNumber)
{
if (m_LastFrameNumber == frameNumber)
return;
m_LastFrameNumber = frameNumber;
// Update m_Particles
m_Type->UpdateEmitter(*this, m_Type->m_Manager.GetCurrentTime() - m_LastUpdateTime);
m_LastUpdateTime = m_Type->m_Manager.GetCurrentTime();
// Regenerate the vertex array data:
VertexArrayIterator attrPos = m_AttributePos.GetIterator();
VertexArrayIterator attrAxis = m_AttributeAxis.GetIterator();
VertexArrayIterator attrUV = m_AttributeUV.GetIterator();
VertexArrayIterator attrColor = m_AttributeColor.GetIterator();
ENSURE(m_Particles.size() <= m_Type->m_MaxParticles);
CBoundingBoxAligned bounds;
for (size_t i = 0; i < m_Particles.size(); ++i)
{
// TODO: for more efficient rendering, maybe we should replace this with
// a degenerate quad if alpha is 0
bounds += m_Particles[i].pos;
*attrPos++ = m_Particles[i].pos;
*attrPos++ = m_Particles[i].pos;
*attrPos++ = m_Particles[i].pos;
*attrPos++ = m_Particles[i].pos;
// Compute corner offsets, split into sin/cos components so the vertex
// shader can multiply by the camera-right (or left?) and camera-up vectors
// to get rotating billboards:
float s = sin(m_Particles[i].angle) * m_Particles[i].size/2.f;
float c = cos(m_Particles[i].angle) * m_Particles[i].size/2.f;
(*attrAxis)[0] = c;
(*attrAxis)[1] = s;
++attrAxis;
(*attrAxis)[0] = s;
(*attrAxis)[1] = -c;
++attrAxis;
(*attrAxis)[0] = -c;
(*attrAxis)[1] = -s;
++attrAxis;
(*attrAxis)[0] = -s;
(*attrAxis)[1] = c;
++attrAxis;
(*attrUV)[0] = 1;
(*attrUV)[1] = 0;
++attrUV;
(*attrUV)[0] = 0;
(*attrUV)[1] = 0;
++attrUV;
(*attrUV)[0] = 0;
(*attrUV)[1] = 1;
++attrUV;
(*attrUV)[0] = 1;
(*attrUV)[1] = 1;
++attrUV;
SColor4ub color = m_Particles[i].color;
// Special case: If the blending depends on the source color, not the source alpha,
// then pre-multiply by the alpha. (This is kind of a hack.)
if (m_Type->m_BlendFuncDst == GL_ONE_MINUS_SRC_COLOR)
{
color.R = (color.R * color.A) / 255;
color.G = (color.G * color.A) / 255;
color.B = (color.B * color.A) / 255;
}
*attrColor++ = color;
*attrColor++ = color;
*attrColor++ = color;
*attrColor++ = color;
}
m_ParticleBounds = bounds;
m_VertexArray.Upload();
}
void CParticleEmitter::PrepareForRendering()
{
m_VertexArray.PrepareForRendering();
}
void CParticleEmitter::Bind(const CShaderProgramPtr& shader)
{
CLOSTexture& los = g_Renderer.GetScene().GetLOSTexture();
shader->BindTexture(str_losTex, los.GetTextureSmooth());
shader->Uniform(str_losTransform, los.GetTextureMatrix()[0], los.GetTextureMatrix()[12], 0.f, 0.f);
const CLightEnv& lightEnv = g_Renderer.GetLightEnv();
shader->Uniform(str_sunColor, lightEnv.m_SunColor);
shader->Uniform(str_fogColor, lightEnv.m_FogColor);
shader->Uniform(str_fogParams, lightEnv.m_FogFactor, lightEnv.m_FogMax, 0.f, 0.f);
shader->BindTexture(str_baseTex, m_Type->m_Texture);
pglBlendEquationEXT(m_Type->m_BlendEquation);
glBlendFunc(m_Type->m_BlendFuncSrc, m_Type->m_BlendFuncDst);
}
void CParticleEmitter::RenderArray(const CShaderProgramPtr& shader)
{
// Some drivers apparently don't like count=0 in glDrawArrays here,
// so skip all drawing in that case
if (m_Particles.empty())
return;
u8* indexBase = m_IndexArray.Bind();
u8* base = m_VertexArray.Bind();
GLsizei stride = (GLsizei)m_VertexArray.GetStride();
shader->VertexPointer(3, GL_FLOAT, stride, base + m_AttributePos.offset);
// Pass the sin/cos axis components as texcoords for no particular reason
// other than that they fit. (Maybe this should be glVertexAttrib* instead?)
shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, stride, base + m_AttributeUV.offset);
shader->TexCoordPointer(GL_TEXTURE1, 2, GL_FLOAT, stride, base + m_AttributeAxis.offset);
shader->ColorPointer(4, GL_UNSIGNED_BYTE, stride, base + m_AttributeColor.offset);
shader->AssertPointersBound();
glDrawElements(GL_TRIANGLES, (GLsizei)(m_Particles.size() * 6), GL_UNSIGNED_SHORT, indexBase);
g_Renderer.GetStats().m_DrawCalls++;
g_Renderer.GetStats().m_Particles += m_Particles.size();
}
void CParticleEmitter::Unattach(const CParticleEmitterPtr& self)
{
m_Active = false;
m_Type->m_Manager.AddUnattachedEmitter(self);
}
void CParticleEmitter::AddParticle(const SParticle& particle)
{
if (m_NextParticleIdx >= m_Particles.size())
m_Particles.push_back(particle);
else
m_Particles[m_NextParticleIdx] = particle;
m_NextParticleIdx = (m_NextParticleIdx + 1) % m_Type->m_MaxParticles;
}
void CParticleEmitter::SetEntityVariable(const std::string& name, float value)
{
m_EntityVariables[name] = value;
}
CModelParticleEmitter::CModelParticleEmitter(const CParticleEmitterTypePtr& type) :
m_Type(type)
{
m_Emitter = CParticleEmitterPtr(new CParticleEmitter(m_Type));
}
CModelParticleEmitter::~CModelParticleEmitter()
{
m_Emitter->Unattach(m_Emitter);
}
void CModelParticleEmitter::SetEntityVariable(const std::string& name, float value)
{
m_Emitter->SetEntityVariable(name, value);
}
CModelAbstract* CModelParticleEmitter::Clone() const
{
return new CModelParticleEmitter(m_Type);
}
void CModelParticleEmitter::CalcBounds()
{
// TODO: we ought to compute sensible bounds here, probably based on the
// current computed particle positions plus the emitter type's largest
// potential bounding box at the current position
m_WorldBounds = m_Type->CalculateBounds(m_Emitter->GetPosition(), m_Emitter->GetParticleBounds());
}
void CModelParticleEmitter::ValidatePosition()
{
// TODO: do we need to do anything here?
// This is a convenient (though possibly not particularly appropriate) place
// to invalidate bounds so they'll be recomputed from the recent particle data
InvalidateBounds();
}
void CModelParticleEmitter::InvalidatePosition()
{
}
void CModelParticleEmitter::SetTransform(const CMatrix3D& transform)
{
if (m_Transform == transform)
return;
m_Emitter->SetPosition(transform.GetTranslation());
m_Emitter->SetRotation(transform.GetRotation());
// call base class to set transform on this object
CRenderableObject::SetTransform(transform);
}
Index: ps/trunk/source/graphics/ParticleEmitterType.h
===================================================================
--- ps/trunk/source/graphics/ParticleEmitterType.h (revision 24267)
+++ ps/trunk/source/graphics/ParticleEmitterType.h (revision 24268)
@@ -1,117 +1,117 @@
-/* Copyright (C) 2011 Wildfire Games.
+/* Copyright (C) 2020 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_PARTICLEEMITTERTYPE
#define INCLUDED_PARTICLEEMITTERTYPE
#include "graphics/Texture.h"
#include "lib/ogl.h"
#include "lib/file/vfs/vfs_path.h"
#include "maths/BoundingBoxAligned.h"
class CVector3D;
class CParticleEmitter;
class CParticleManager;
class IParticleVar;
class IParticleEffector;
/**
* Particle emitter type - stores the common state data for all emitters of that
* type, and uses that data to update the emitter states.
*
* The data is initialised from XML files.
*
* Most of the emitter type data is represented as subclasses of IParticleVar,
* which will typically return a constant value or a random value each time it's
* evaluated. New subclasses can be added to support different random distributions,
* etc.
*/
class CParticleEmitterType
{
NONCOPYABLE(CParticleEmitterType); // reference member
public:
CParticleEmitterType(const VfsPath& path, CParticleManager& manager);
private:
friend class CModelParticleEmitter;
friend class CParticleEmitter;
friend class CParticleVarConstant;
friend class CParticleVarUniform;
friend class CParticleVarCopy;
friend class ParticleRenderer;
enum
{
VAR_EMISSIONRATE,
VAR_LIFETIME,
VAR_POSITION_X,
VAR_POSITION_Y,
VAR_POSITION_Z,
VAR_ANGLE,
VAR_VELOCITY_X,
VAR_VELOCITY_Y,
VAR_VELOCITY_Z,
VAR_VELOCITY_ANGLE,
VAR_SIZE,
VAR_SIZE_GROWTHRATE,
VAR_COLOR_R,
VAR_COLOR_G,
VAR_COLOR_B,
VAR__MAX
};
int GetVariableID(const std::string& name);
bool LoadXML(const VfsPath& path);
/**
* Update the state of an emitter's particles, by a potentially long time @p dt.
*/
void UpdateEmitter(CParticleEmitter& emitter, float dt);
/**
* Update the state of an emitter's particles, by a short time @p dt that can
* be computed in a single step.
*/
void UpdateEmitterStep(CParticleEmitter& emitter, float dt);
CBoundingBoxAligned CalculateBounds(CVector3D emitterPos, CBoundingBoxAligned emittedBounds);
CTexturePtr m_Texture;
GLenum m_BlendEquation;
GLenum m_BlendFuncSrc;
GLenum m_BlendFuncDst;
bool m_StartFull;
bool m_UseRelativeVelocity;
float m_MaxLifetime;
- size_t m_MaxParticles;
+ u16 m_MaxParticles;
CBoundingBoxAligned m_MaxBounds;
typedef shared_ptr IParticleVarPtr;
std::vector m_Variables;
typedef shared_ptr IParticleEffectorPtr;
std::vector m_Effectors;
CParticleManager& m_Manager;
};
typedef shared_ptr CParticleEmitterTypePtr;
#endif // INCLUDED_PARTICLEEMITTERTYPE
Index: ps/trunk/source/graphics/tests/test_Camera.h
===================================================================
--- ps/trunk/source/graphics/tests/test_Camera.h (revision 24267)
+++ ps/trunk/source/graphics/tests/test_Camera.h (revision 24268)
@@ -1,176 +1,176 @@
/* Copyright (C) 2020 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/Camera.h"
#include "maths/MathUtil.h"
#include "maths/Vector3D.h"
#include
#include
class TestCamera : public CxxTest::TestSuite
{
public:
void test_frustum_perspective()
{
SViewPort viewPort;
viewPort.m_X = 0;
viewPort.m_Y = 0;
viewPort.m_Width = 512;
viewPort.m_Height = 512;
CCamera camera;
camera.SetViewPort(viewPort);
camera.LookAlong(
CVector3D(0.0f, 0.0f, 0.0f),
CVector3D(0.0f, 0.0f, 1.0f),
CVector3D(0.0f, 1.0f, 0.0f)
);
camera.SetPerspectiveProjection(1.0f, 101.0f, DEGTORAD(90.0f));
TS_ASSERT_EQUALS(camera.GetProjectionType(), CCamera::PERSPECTIVE);
camera.UpdateFrustum();
const float sqrt2 = sqrtf(2.0f) / 2.0f;
const std::vector expectedPlanes = {
CVector4D(sqrt2, 0.0f, sqrt2, 0.0f),
CVector4D(-sqrt2, 0.0f, sqrt2, 0.0f),
CVector4D(0.0f, sqrt2, sqrt2, 0.0f),
CVector4D(0.0f, -sqrt2, sqrt2, 0.0f),
CVector4D(0.0f, 0.0f, -1.0f, 101.0f),
CVector4D(0.0f, 0.0f, 1.0f, -1.0f),
};
CheckFrustumPlanes(camera.GetFrustum(), expectedPlanes);
}
void test_frustum_ortho()
{
SViewPort viewPort;
viewPort.m_X = 0;
viewPort.m_Y = 0;
viewPort.m_Width = 512;
viewPort.m_Height = 512;
CCamera camera;
camera.SetViewPort(viewPort);
camera.LookAlong(
CVector3D(0.0f, 0.0f, 0.0f),
CVector3D(0.0f, 0.0f, 1.0f),
CVector3D(0.0f, 1.0f, 0.0f)
);
CMatrix3D projection;
projection.SetOrtho(-10.0f, 10.0f, -10.0f, 10.0f, -10.0f, 10.0f);
camera.SetProjection(projection);
TS_ASSERT_EQUALS(camera.GetProjectionType(), CCamera::CUSTOM);
camera.UpdateFrustum();
const std::vector expectedPlanes = {
CVector4D(1.0f, 0.0f, 0.0f, 10.0f),
CVector4D(-1.0f, 0.0f, 0.0f, 10.0f),
CVector4D(0.0f, 1.0f, 0.0f, 10.0f),
CVector4D(0.0f, -1.0f, 0.0f, 10.0f),
CVector4D(0.0f, 0.0f, 1.0f, 10.0f),
CVector4D(0.0f, 0.0f, -1.0f, 10.0f)
};
CheckFrustumPlanes(camera.GetFrustum(), expectedPlanes);
}
// Order of planes is unknown. So use interactive checker.
void CheckFrustumPlanes(const CFrustum& frustum, const std::vector& expectedPlanes)
{
TS_ASSERT_EQUALS(frustum.GetNumPlanes(), expectedPlanes.size());
std::set indices;
for (size_t i = 0; i < expectedPlanes.size(); ++i)
indices.insert(i);
for (size_t i = 0; i < frustum.GetNumPlanes(); ++i)
{
bool found = false;
for (size_t j : indices)
{
if (EqualPlanes(frustum[i], expectedPlanes[j]))
{
found = true;
indices.erase(j);
break;
}
}
if (!found)
TS_FAIL(frustum[i]);
}
}
bool EqualPlanes(const CPlane& p1, const CPlane& p2) const
{
const float EPS = 1e-3f;
if (std::fabs(p1.m_Dist - p2.m_Dist) >= EPS)
return false;
return
std::fabs(p1.m_Norm.X - p2.m_Norm.X) < EPS &&
std::fabs(p1.m_Norm.Y - p2.m_Norm.Y) < EPS &&
std::fabs(p1.m_Norm.Z - p2.m_Norm.Z) < EPS;
}
void CompareQuads(const CCamera::Quad& quad, const CCamera::Quad& expected_quad)
{
- const float EPS = 1e-4;
+ const float EPS = 1e-4f;
for (size_t index = 0; index < expected_quad.size(); ++index)
{
TS_ASSERT_DELTA(quad[index].X, expected_quad[index].X, EPS);
TS_ASSERT_DELTA(quad[index].Y, expected_quad[index].Y, EPS);
TS_ASSERT_DELTA(quad[index].Z, expected_quad[index].Z, EPS);
}
}
void test_persepctive_plane_points()
{
SViewPort viewPort;
viewPort.m_X = 0;
viewPort.m_Y = 0;
viewPort.m_Width = 512;
viewPort.m_Height = 512;
CCamera camera;
camera.SetViewPort(viewPort);
camera.LookAlong(
CVector3D(0.0f, 0.0f, 0.0f),
CVector3D(0.0f, 0.0f, 1.0f),
CVector3D(0.0f, 1.0f, 0.0f)
);
camera.m_Orientation.SetTranslation(CVector3D(1.0f, 2.0f, 3.0f));
camera.SetPerspectiveProjection(1.0f, 101.0f, DEGTORAD(90.0f));
CCamera::Quad quad;
// Zero distance point is the origin of all camera rays,
// so all plane points should be stay there.
camera.GetViewQuad(0.0f, quad);
for (const CVector3D& point : quad)
TS_ASSERT_EQUALS(point, CVector3D(0.0f, 0.0f, 0.0f));
// Points lying on the far plane.
CCamera::Quad expectedFarQuad = {
CVector3D(-101.0f, -101.0f, 101.0f),
CVector3D(101.0f, -101.0f, 101.0f),
CVector3D(101.0f, 101.0f, 101.0f),
CVector3D(-101.0f, 101.0f, 101.0f)
};
camera.GetViewQuad(camera.GetFarPlane(), quad);
CompareQuads(quad, expectedFarQuad);
}
};
Index: ps/trunk/source/graphics/tests/test_Terrain.h
===================================================================
--- ps/trunk/source/graphics/tests/test_Terrain.h (revision 24267)
+++ ps/trunk/source/graphics/tests/test_Terrain.h (revision 24268)
@@ -1,473 +1,473 @@
/* Copyright (C) 2020 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/Terrain.h"
#include "graphics/Patch.h"
#include "graphics/RenderableObject.h"
#include "maths/Fixed.h"
#include "maths/FixedVector3D.h"
#include
#include
class TestTerrain : public CxxTest::TestSuite
{
void SetVertex(CTerrain& terrain, ssize_t i, ssize_t j, u16 height)
{
terrain.GetHeightMap()[j*terrain.GetVerticesPerSide() + i] = height;
terrain.MakeDirty(RENDERDATA_UPDATE_VERTICES);
}
u16 GetVertex(CTerrain& terrain, ssize_t i, ssize_t j)
{
return terrain.GetHeightMap()[j*terrain.GetVerticesPerSide() + i];
}
void Set45Slope(CTerrain& terrain)
{
SetVertex(terrain, 0, 0, 100);
SetVertex(terrain, 0, 1, 100);
SetVertex(terrain, 0, 2, 100);
SetVertex(terrain, 1, 0, 100 + TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 1, 1, 100 + TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 1, 2, 100 + TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 2, 0, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 2, 1, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 2, 2, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 3, 0, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 3, 1, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 3, 2, 100 + 2*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
}
void SetHighPlateau(CTerrain& terrain, int height)
{
SetVertex(terrain, 4, 0, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 4, 1, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 4, 2, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 5, 0, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 5, 1, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
SetVertex(terrain, 5, 2, 100 + height*TERRAIN_TILE_SIZE*HEIGHT_UNITS_PER_METRE);
}
public:
void test_GetExactGroundLevel()
{
CTerrain terrain;
terrain.Initialize(4, NULL);
Set45Slope(terrain);
SetHighPlateau(terrain, 20);
float ground;
ground = terrain.GetExactGroundLevel(0.f, 1.5f*TERRAIN_TILE_SIZE);
TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE, 0.01f);
ground = terrain.GetExactGroundLevel(0.5f*TERRAIN_TILE_SIZE, 1.5f*TERRAIN_TILE_SIZE);
TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE+0.5f*TERRAIN_TILE_SIZE, 0.01f);
ground = terrain.GetExactGroundLevel(1.5f*TERRAIN_TILE_SIZE, 1.5f*TERRAIN_TILE_SIZE);
TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE+1.5f*TERRAIN_TILE_SIZE, 0.01f);
ground = terrain.GetExactGroundLevel(2.5f*TERRAIN_TILE_SIZE, 1.5f*TERRAIN_TILE_SIZE);
TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE+2.f*TERRAIN_TILE_SIZE, 0.01f);
ground = terrain.GetExactGroundLevel(3.5f*TERRAIN_TILE_SIZE, 1.5f*TERRAIN_TILE_SIZE);
TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE+11.f*TERRAIN_TILE_SIZE, 0.01f);
ground = terrain.GetExactGroundLevel(4.5f*TERRAIN_TILE_SIZE, 1.5f*TERRAIN_TILE_SIZE);
TS_ASSERT_DELTA(ground, 100.f/HEIGHT_UNITS_PER_METRE+20.f*TERRAIN_TILE_SIZE, 0.01f);
}
void test_GetExactGroundLevelFixed()
{
CTerrain terrain;
terrain.Initialize(4, NULL);
Set45Slope(terrain);
SetHighPlateau(terrain, 20);
const double maxDelta = 0.0001;
fixed ground;
ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(0.f), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE));
TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE, maxDelta);
ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(0.5f*TERRAIN_TILE_SIZE), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE));
TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE+0.5*TERRAIN_TILE_SIZE, maxDelta);
ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE));
TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE+1.5*TERRAIN_TILE_SIZE, maxDelta);
ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(2.5f*TERRAIN_TILE_SIZE), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE));
TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE+2.0*TERRAIN_TILE_SIZE, maxDelta);
ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(3.5f*TERRAIN_TILE_SIZE), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE));
TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE+11.0*TERRAIN_TILE_SIZE, maxDelta);
ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(4.5f*TERRAIN_TILE_SIZE), fixed::FromFloat(1.5f*TERRAIN_TILE_SIZE));
TS_ASSERT_DELTA(ground.ToDouble(), 100.0/HEIGHT_UNITS_PER_METRE+20.0*TERRAIN_TILE_SIZE, maxDelta);
}
void test_GetExactGroundLevelFixed_max()
{
CTerrain terrain;
terrain.Initialize(4, NULL);
SetVertex(terrain, 0, 0, 65535);
SetVertex(terrain, 0, 1, 65535);
SetVertex(terrain, 1, 0, 65535);
SetVertex(terrain, 1, 1, 65535);
const double maxDelta = 0.024;
int p = 255;
for (int zi = 0; zi < p; ++zi)
{
for (int xi = 0; xi < p; ++xi)
{
fixed ground = terrain.GetExactGroundLevelFixed(fixed::FromFloat(xi/(float)p*TERRAIN_TILE_SIZE), fixed::FromFloat(zi/(float)p*TERRAIN_TILE_SIZE));
TS_ASSERT_DELTA(ground.ToDouble(), 65535.0/HEIGHT_UNITS_PER_METRE, maxDelta);
}
}
}
void test_CalcNormal()
{
CTerrain terrain;
terrain.Initialize(4, NULL);
Set45Slope(terrain);
CVector3D vec;
terrain.CalcNormal(1, 1, vec);
TS_ASSERT_DELTA(vec.X, -1.f/sqrt(2.f), 0.01f);
TS_ASSERT_DELTA(vec.Y, 1.f/sqrt(2.f), 0.01f);
TS_ASSERT_EQUALS(vec.Z, 0.f);
terrain.CalcNormal(2, 1, vec);
TS_ASSERT_DELTA(vec.X, (-1.f/sqrt(2.f)) / sqrt(2.f+sqrt(2.f)), 0.01f);
TS_ASSERT_DELTA(vec.Y, (1.f+1.f/sqrt(2.f)) / sqrt(2.f+sqrt(2.f)), 0.01f);
TS_ASSERT_EQUALS(vec.Z, 0);
terrain.CalcNormal(5, 1, vec);
TS_ASSERT_EQUALS(vec.X, 0.f);
TS_ASSERT_EQUALS(vec.Y, 1.f);
TS_ASSERT_EQUALS(vec.Z, 0.f);
}
void test_CalcNormalFixed()
{
CTerrain terrain;
terrain.Initialize(4, NULL);
Set45Slope(terrain);
CFixedVector3D vec;
terrain.CalcNormalFixed(1, 1, vec);
TS_ASSERT_DELTA(vec.X.ToFloat(), -1.f/sqrt(2.f), 0.01f);
TS_ASSERT_DELTA(vec.Y.ToFloat(), 1.f/sqrt(2.f), 0.01f);
TS_ASSERT_EQUALS(vec.Z.ToFloat(), 0.f);
terrain.CalcNormalFixed(2, 1, vec);
TS_ASSERT_DELTA(vec.X.ToFloat(), (-1.f/sqrt(2.f)) / sqrt(2.f+sqrt(2.f)), 0.01f);
TS_ASSERT_DELTA(vec.Y.ToFloat(), (1.f+1.f/sqrt(2.f)) / sqrt(2.f+sqrt(2.f)), 0.01f);
TS_ASSERT_EQUALS(vec.Z.ToFloat(), 0);
terrain.CalcNormalFixed(5, 1, vec);
TS_ASSERT_EQUALS(vec.X.ToFloat(), 0.f);
TS_ASSERT_EQUALS(vec.Y.ToFloat(), 1.f);
TS_ASSERT_EQUALS(vec.Z.ToFloat(), 0.f);
}
void test_Resize()
{
// We do resize by size in patches, so it doesn't make sense to
// fill each vertex with a different value. Instead we use a single
// value per a patch.
struct ResizeTestCase
{
ssize_t horizontalOffset, verticalOffset;
std::vector> sourcePatches;
std::vector> expectedPatches;
};
const ResizeTestCase testCases[] = {
// Without offset.
{
0, 0,
{
{42}
},
{
{42}
}
},
{
0, 0,
{
{1, 2},
{3, 4}
},
{
{1, 2},
{3, 4}
}
},
{
0, 0,
{
{1, 2},
{3, 4}
},
{
{1, 1, 2, 2},
{1, 1, 2, 2},
{3, 3, 4, 4},
{3, 3, 4, 4}
}
},
{
0, 0,
{
{ 1, 2 , 3, 4},
{ 5, 6 , 7, 8},
{ 9, 10, 11, 12},
{13, 14, 15, 16}
},
{
{ 6, 7},
{10, 11},
}
},
// With offset.
{
-2, -2,
{
{1, 2},
{3, 4}
},
{
{0, 0},
{0, 0}
}
},
{
-2, 0,
{
{1, 2},
{3, 4}
},
{
{0, 0},
{0, 0}
}
},
{
4, 4,
{
{1, 2},
{3, 4}
},
{
{0, 0},
{0, 0}
}
},
{
1, 1,
{
{ 1, 2 , 3, 4},
{ 5, 6 , 7, 8},
{ 9, 10, 11, 12},
{13, 14, 15, 16}
},
{
{ 6 , 7, 8, 8},
{10, 11, 12, 12},
{14, 15, 16, 16},
{14, 15, 16, 16}
}
},
{
1, 1,
{
{1, 2},
{3, 4}
},
{
{4, 4},
{4, 4}
}
},
{
-2, 0,
{
{ 1, 2 , 3, 4},
{ 5, 6 , 7, 8},
{ 9, 10, 11, 12},
{13, 14, 15, 16}
},
{
{5, 5},
{9, 9}
}
},
{
2, -2,
{
{ 1, 2 , 3, 4},
{ 5, 6 , 7, 8},
{ 9, 10, 11, 12},
{13, 14, 15, 16}
},
{
{4, 4},
{4, 4}
}
},
{
3, -1,
{
{ 1, 2 , 3, 4},
{ 5, 6 , 7, 8},
{ 9, 10, 11, 12},
{13, 14, 15, 16}
},
{
{0, 0},
{0, 0}
}
},
{
-2, -1,
{
{1, 2},
{3, 4}
},
{
{1, 1, 1, 1},
{1, 1, 1, 1},
{1, 1, 1, 1},
{3, 3, 3, 3}
}
},
};
for (const ResizeTestCase& testCase : testCases)
{
const ssize_t sourceSize = testCase.sourcePatches.size();
const ssize_t expectedSize = testCase.expectedPatches.size();
TS_ASSERT_LESS_THAN(0, sourceSize);
TS_ASSERT_LESS_THAN(0, expectedSize);
const ssize_t sourceMapSize = sourceSize * PATCH_SIZE + 1;
const ssize_t expectedMapSize = expectedSize * PATCH_SIZE + 1;
CTerrain terrain;
{
std::vector heightmap(sourceMapSize * sourceMapSize);
for (ssize_t jTile = 0; jTile < sourceSize; ++jTile)
{
TS_ASSERT_EQUALS(sourceSize, testCase.sourcePatches[jTile].size());
for (ssize_t iTile = 0; iTile < sourceSize; ++iTile)
{
for (ssize_t j = 0; j < PATCH_SIZE; ++j)
for (ssize_t i = 0; i < PATCH_SIZE; ++i)
{
const ssize_t idx =
(jTile * PATCH_SIZE + j) * sourceMapSize +
iTile * PATCH_SIZE + i;
heightmap[idx] = testCase.sourcePatches[jTile][iTile];
}
}
}
terrain.Initialize(sourceSize, heightmap.data());
}
terrain.ResizeAndOffset(expectedSize, testCase.horizontalOffset, testCase.verticalOffset);
TS_ASSERT_EQUALS(expectedMapSize, terrain.GetVerticesPerSide());
TS_ASSERT_EQUALS(expectedSize, terrain.GetPatchesPerSide());
for (ssize_t jTile = 0; jTile < expectedSize; ++jTile)
{
TS_ASSERT_EQUALS(
expectedSize, testCase.expectedPatches[jTile].size());
for (ssize_t iTile = 0; iTile < expectedSize; ++iTile)
{
for (ssize_t j = 0; j < PATCH_SIZE; ++j)
for (ssize_t i = 0; i < PATCH_SIZE; ++i)
{
// The whole patch should have the same height,
// since we resize by patches.
if (GetVertex(terrain, iTile * PATCH_SIZE,
jTile * PATCH_SIZE) ==
GetVertex(terrain, iTile * PATCH_SIZE + i,
jTile * PATCH_SIZE + j))
continue;
TS_FAIL("The whole patch should have the same height");
std::stringstream ss;
ss << "iTile=" << iTile << " jTile=" << jTile
<< " i=" << i << " j=" << j;
TS_WARN(ss.str());
ss.str(std::string());
ss << "found=" << GetVertex(terrain, iTile * PATCH_SIZE + i,
jTile * PATCH_SIZE + j)
<< " expected=" << GetVertex(terrain, iTile * PATCH_SIZE,
jTile * PATCH_SIZE);
TS_WARN(ss.str());
return;
}
if (testCase.expectedPatches[jTile][iTile] ==
GetVertex(terrain, iTile * PATCH_SIZE,
jTile * PATCH_SIZE))
continue;
std::stringstream ss;
ss << "The patch has wrong height"
<< " (i=" << iTile << " j=" << jTile << "):"
<< " found=" << GetVertex(terrain, iTile * PATCH_SIZE,
jTile * PATCH_SIZE)
<< " expected=" << testCase.expectedPatches[jTile][iTile];
TS_FAIL(ss.str());
ss.str(std::string());
ss << "Terrain (" << terrain.GetPatchesPerSide()
<< "x" << terrain.GetPatchesPerSide() << "):";
TS_WARN(ss.str());
- for (ssize_t jTile = 0; jTile < expectedSize; ++jTile)
+ for (ssize_t jj = 0; jj < expectedSize; ++jj)
{
ss.str(std::string());
ss << "[";
- for (ssize_t iTile = 0; iTile < expectedSize; ++iTile)
+ for (ssize_t ii = 0; ii < expectedSize; ++ii)
{
- if (iTile)
+ if (ii)
ss << ", ";
- ss << GetVertex(terrain, iTile * PATCH_SIZE,
- jTile * PATCH_SIZE);
+ ss << GetVertex(terrain, ii * PATCH_SIZE,
+ jj * PATCH_SIZE);
}
ss << "]";
TS_WARN(ss.str());
}
return;
}
}
}
}
};
Index: ps/trunk/source/gui/GUIManager.cpp
===================================================================
--- ps/trunk/source/gui/GUIManager.cpp (revision 24267)
+++ ps/trunk/source/gui/GUIManager.cpp (revision 24268)
@@ -1,405 +1,405 @@
/* Copyright (C) 2020 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 "GUIManager.h"
#include "gui/CGUI.h"
#include "lib/timer.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/GameSetup/Config.h"
#include "ps/Profile.h"
#include "ps/XML/Xeromyces.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
CGUIManager* g_GUI = nullptr;
const CStr CGUIManager::EventNameWindowResized = "WindowResized";
// General TODOs:
//
// A lot of the CGUI data could (and should) be shared between
// multiple pages, instead of treating them as completely independent, to save
// memory and loading time.
// called from main loop when (input) events are received.
// event is passed to other handlers if false is returned.
// trampoline: we don't want to make the HandleEvent implementation static
InReaction gui_handler(const SDL_Event_* ev)
{
if (!g_GUI)
return IN_PASS;
PROFILE("GUI event handler");
return g_GUI->HandleEvent(ev);
}
static Status ReloadChangedFileCB(void* param, const VfsPath& path)
{
return static_cast(param)->ReloadChangedFile(path);
}
CGUIManager::CGUIManager()
{
m_ScriptContext = g_ScriptContext;
m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIManager", m_ScriptContext));
m_ScriptInterface->SetCallbackData(this);
m_ScriptInterface->LoadGlobalScripts();
if (!CXeromyces::AddValidator(g_VFS, "gui_page", "gui/gui_page.rng"))
LOGERROR("CGUIManager: failed to load GUI page grammar file 'gui/gui_page.rng'");
if (!CXeromyces::AddValidator(g_VFS, "gui", "gui/gui.rng"))
LOGERROR("CGUIManager: failed to load GUI XML grammar file 'gui/gui.rng'");
RegisterFileReloadFunc(ReloadChangedFileCB, this);
}
CGUIManager::~CGUIManager()
{
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
}
size_t CGUIManager::GetPageCount() const
{
return m_PageStack.size();
}
void CGUIManager::SwitchPage(const CStrW& pageName, ScriptInterface* srcScriptInterface, JS::HandleValue initData)
{
// The page stack is cleared (including the script context where initData came from),
// therefore we have to clone initData.
ScriptInterface::StructuredClone initDataClone;
if (!initData.isUndefined())
initDataClone = srcScriptInterface->WriteStructuredClone(initData, true);
m_PageStack.clear();
PushPage(pageName, initDataClone, JS::UndefinedHandleValue);
}
void CGUIManager::PushPage(const CStrW& pageName, ScriptInterface::StructuredClone initData, JS::HandleValue callbackFunction)
{
// Store the callback handler in the current GUI page before opening the new one
if (!m_PageStack.empty() && !callbackFunction.isUndefined())
m_PageStack.back().SetCallbackFunction(*m_ScriptInterface, callbackFunction);
// Push the page prior to loading its contents, because that may push
// another GUI page on init which should be pushed on top of this new page.
m_PageStack.emplace_back(pageName, initData);
m_PageStack.back().LoadPage(m_ScriptContext);
}
void CGUIManager::PopPage(ScriptInterface::StructuredClone args)
{
if (m_PageStack.size() < 2)
{
debug_warn(L"Tried to pop GUI page when there's < 2 in the stack");
return;
}
m_PageStack.pop_back();
m_PageStack.back().PerformCallbackFunction(args);
}
CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const ScriptInterface::StructuredClone initData)
- : name(pageName), initData(initData), inputs(), gui(), callbackFunction()
+ : m_Name(pageName), initData(initData), inputs(), gui(), callbackFunction()
{
}
void CGUIManager::SGUIPage::LoadPage(shared_ptr scriptContext)
{
// If we're hotloading then try to grab some data from the previous page
ScriptInterface::StructuredClone hotloadData;
if (gui)
{
shared_ptr scriptInterface = gui->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue global(rq.cx, rq.globalValue());
JS::RootedValue hotloadDataVal(rq.cx);
scriptInterface->CallFunction(global, "getHotloadData", &hotloadDataVal);
hotloadData = scriptInterface->WriteStructuredClone(hotloadDataVal, true);
}
g_CursorName = g_DefaultCursor;
inputs.clear();
gui.reset(new CGUI(scriptContext));
gui->AddObjectTypes();
- VfsPath path = VfsPath("gui") / name;
+ VfsPath path = VfsPath("gui") / m_Name;
inputs.insert(path);
CXeromyces xero;
if (xero.Load(g_VFS, path, "gui_page") != PSRETURN_OK)
// Fail silently (Xeromyces reported the error)
return;
int elmt_page = xero.GetElementID("page");
int elmt_include = xero.GetElementID("include");
XMBElement root = xero.GetRoot();
if (root.GetNodeName() != elmt_page)
{
- LOGERROR("GUI page '%s' must have root element ", utf8_from_wstring(name));
+ LOGERROR("GUI page '%s' must have root element ", utf8_from_wstring(m_Name));
return;
}
XERO_ITER_EL(root, node)
{
if (node.GetNodeName() != elmt_include)
{
- LOGERROR("GUI page '%s' must only have elements inside ", utf8_from_wstring(name));
+ LOGERROR("GUI page '%s' must only have elements inside ", utf8_from_wstring(m_Name));
continue;
}
- std::string name = node.GetText();
- CStrW nameW (node.GetText().FromUTF8());
+ CStr8 name = node.GetText();
+ CStrW nameW = node.GetText().FromUTF8();
PROFILE2("load gui xml");
PROFILE2_ATTR("name: %s", name.c_str());
TIMER(nameW.c_str());
if (name.back() == '/')
{
- VfsPath directory = VfsPath("gui") / nameW;
- VfsPaths pathnames;
- vfs::GetPathnames(g_VFS, directory, L"*.xml", pathnames);
- for (const VfsPath& path : pathnames)
- gui->LoadXmlFile(path, inputs);
+ VfsPath currentDirectory = VfsPath("gui") / nameW;
+ VfsPaths directories;
+ vfs::GetPathnames(g_VFS, currentDirectory, L"*.xml", directories);
+ for (const VfsPath& directory : directories)
+ gui->LoadXmlFile(directory, inputs);
}
else
{
- VfsPath path = VfsPath("gui") / nameW;
- gui->LoadXmlFile(path, inputs);
+ VfsPath directory = VfsPath("gui") / nameW;
+ gui->LoadXmlFile(directory, inputs);
}
}
gui->LoadedXmlFiles();
shared_ptr scriptInterface = gui->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue initDataVal(rq.cx);
JS::RootedValue hotloadDataVal(rq.cx);
JS::RootedValue global(rq.cx, rq.globalValue());
if (initData)
scriptInterface->ReadStructuredClone(initData, &initDataVal);
if (hotloadData)
scriptInterface->ReadStructuredClone(hotloadData, &hotloadDataVal);
if (scriptInterface->HasProperty(global, "init") &&
!scriptInterface->CallFunctionVoid(global, "init", initDataVal, hotloadDataVal))
- LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(name));
+ LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(m_Name));
}
void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc)
{
if (!callbackFunc.isObject())
{
LOGERROR("Given callback handler is not an object!");
return;
}
ScriptRequest rq(scriptInterface);
if (!JS_ObjectIsFunction(rq.cx, &callbackFunc.toObject()))
{
LOGERROR("Given callback handler is not a function!");
return;
}
callbackFunction = std::make_shared(scriptInterface.GetGeneralJSContext(), callbackFunc);
}
void CGUIManager::SGUIPage::PerformCallbackFunction(ScriptInterface::StructuredClone args)
{
if (!callbackFunction)
return;
shared_ptr scriptInterface = gui->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedObject globalObj(rq.cx, rq.glob);
JS::RootedValue funcVal(rq.cx, *callbackFunction);
// Delete the callback function, so that it is not called again
callbackFunction.reset();
JS::RootedValue argVal(rq.cx);
if (args)
scriptInterface->ReadStructuredClone(args, &argVal);
JS::AutoValueVector paramData(rq.cx);
DISCARD paramData.append(argVal);
JS::RootedValue result(rq.cx);
if(!JS_CallFunctionValue(rq.cx, globalObj, funcVal, paramData, &result))
ScriptException::CatchPending(rq);
}
Status CGUIManager::ReloadChangedFile(const VfsPath& path)
{
for (SGUIPage& p : m_PageStack)
if (p.inputs.find(path) != p.inputs.end())
{
- LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.name));
+ LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.m_Name));
p.LoadPage(m_ScriptContext);
// TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators
}
return INFO::OK;
}
Status CGUIManager::ReloadAllPages()
{
// TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators
for (SGUIPage& p : m_PageStack)
p.LoadPage(m_ScriptContext);
return INFO::OK;
}
InReaction CGUIManager::HandleEvent(const SDL_Event_* ev)
{
// We want scripts to have access to the raw input events, so they can do complex
// processing when necessary (e.g. for unit selection and camera movement).
// Sometimes they'll want to be layered behind the GUI widgets (e.g. to detect mousedowns on the
// visible game area), sometimes they'll want to intercepts events before the GUI (e.g.
// to capture all mouse events until a mouseup after dragging).
// So we call two separate handler functions:
bool handled = false;
{
PROFILE("handleInputBeforeGui");
ScriptRequest rq(*top()->GetScriptInterface());
JS::RootedValue global(rq.cx, rq.globalValue());
if (top()->GetScriptInterface()->CallFunction(global, "handleInputBeforeGui", handled, *ev, top()->FindObjectUnderMouse()))
if (handled)
return IN_HANDLED;
}
{
PROFILE("handle event in native GUI");
InReaction r = top()->HandleEvent(ev);
if (r != IN_PASS)
return r;
}
{
// We can't take the following lines out of this scope because top() may be another gui page than it was when calling handleInputBeforeGui!
ScriptRequest rq(*top()->GetScriptInterface());
JS::RootedValue global(rq.cx, rq.globalValue());
PROFILE("handleInputAfterGui");
if (top()->GetScriptInterface()->CallFunction(global, "handleInputAfterGui", handled, *ev))
if (handled)
return IN_HANDLED;
}
return IN_PASS;
}
void CGUIManager::SendEventToAll(const CStr& eventName) const
{
// Save an immutable copy so iterators aren't invalidated by handlers
PageStackType pageStack = m_PageStack;
for (const SGUIPage& p : pageStack)
p.gui->SendEventToAll(eventName);
}
void CGUIManager::SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const
{
// Save an immutable copy so iterators aren't invalidated by handlers
PageStackType pageStack = m_PageStack;
for (const SGUIPage& p : pageStack)
p.gui->SendEventToAll(eventName, paramData);
}
void CGUIManager::TickObjects()
{
PROFILE3("gui tick");
// We share the script context with everything else that runs in the same thread.
// This call makes sure we trigger GC regularly even if the simulation is not running.
m_ScriptInterface->GetContext()->MaybeIncrementalGC(1.0f);
// Save an immutable copy so iterators aren't invalidated by tick handlers
PageStackType pageStack = m_PageStack;
for (const SGUIPage& p : pageStack)
p.gui->TickObjects();
}
void CGUIManager::Draw() const
{
PROFILE3_GPU("gui");
for (const SGUIPage& p : m_PageStack)
p.gui->Draw();
}
void CGUIManager::UpdateResolution()
{
// Save an immutable copy so iterators aren't invalidated by event handlers
PageStackType pageStack = m_PageStack;
for (const SGUIPage& p : pageStack)
{
p.gui->UpdateResolution();
p.gui->SendEventToAll(EventNameWindowResized);
}
}
bool CGUIManager::TemplateExists(const std::string& templateName) const
{
return m_TemplateLoader.TemplateExists(templateName);
}
const CParamNode& CGUIManager::GetTemplate(const std::string& templateName)
{
const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity");
if (!templateRoot.IsOk())
LOGERROR("Invalid template found for '%s'", templateName.c_str());
return templateRoot;
}
// This returns a shared_ptr to make sure the CGUI doesn't get deallocated
// while we're in the middle of calling a function on it (e.g. if a GUI script
// calls SwitchPage)
shared_ptr CGUIManager::top() const
{
ENSURE(m_PageStack.size());
return m_PageStack.back().gui;
}
Index: ps/trunk/source/gui/GUIManager.h
===================================================================
--- ps/trunk/source/gui/GUIManager.h (revision 24267)
+++ ps/trunk/source/gui/GUIManager.h (revision 24268)
@@ -1,181 +1,181 @@
/* Copyright (C) 2020 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_GUIMANAGER
#define INCLUDED_GUIMANAGER
#include "lib/file/vfs/vfs_path.h"
#include "lib/input.h"
#include "ps/CStr.h"
#include "ps/TemplateLoader.h"
#include "scriptinterface/ScriptInterface.h"
#include
#include
#include
class CGUI;
/**
* External interface to the GUI system.
*
* The GUI consists of a set of pages. Each page is constructed from a
* series of XML files, and is independent from any other page.
* Only one page is active at a time. All events and render requests etc
* will go to the active page. This lets the GUI switch between pre-game menu
* and in-game UI.
*/
class CGUIManager
{
NONCOPYABLE(CGUIManager);
public:
CGUIManager();
~CGUIManager();
shared_ptr GetScriptInterface()
{
return m_ScriptInterface;
}
shared_ptr GetContext() { return m_ScriptContext; }
shared_ptr GetActiveGUI() { return top(); }
/**
* Returns the number of currently open GUI pages.
*/
size_t GetPageCount() const;
/**
* Load a new GUI page and make it active. All current pages will be destroyed.
*/
void SwitchPage(const CStrW& name, ScriptInterface* srcScriptInterface, JS::HandleValue initData);
/**
* Load a new GUI page and make it active. All current pages will be retained,
* and will still be drawn and receive tick events, but will not receive
* user inputs.
* If given, the callbackHandler function will be executed once this page is closed.
*/
void PushPage(const CStrW& pageName, ScriptInterface::StructuredClone initData, JS::HandleValue callbackFunc);
/**
* Unload the currently active GUI page, and make the previous page active.
* (There must be at least two pages when you call this.)
*/
void PopPage(ScriptInterface::StructuredClone args);
/**
* Called when a file has been modified, to hotload changes.
*/
Status ReloadChangedFile(const VfsPath& path);
/**
* Called when we should reload all pages (e.g. translation hotloading update).
*/
Status ReloadAllPages();
/**
* Pass input events to the currently active GUI page.
*/
InReaction HandleEvent(const SDL_Event_* ev);
/**
* See CGUI::SendEventToAll; applies to the currently active page.
*/
void SendEventToAll(const CStr& eventName) const;
void SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const;
/**
* See CGUI::TickObjects; applies to @em all loaded pages.
*/
void TickObjects();
/**
* See CGUI::Draw; applies to @em all loaded pages.
*/
void Draw() const;
/**
* See CGUI::UpdateResolution; applies to @em all loaded pages.
*/
void UpdateResolution();
/**
* Check if a template with this name exists
*/
bool TemplateExists(const std::string& templateName) const;
/**
* Retrieve the requested template, used for displaying faction specificities.
*/
const CParamNode& GetTemplate(const std::string& templateName);
private:
struct SGUIPage
{
// COPYABLE, because event handlers may invalidate page stack iterators by open or close pages,
// and event handlers need to be called for the entire stack.
/**
* Initializes the data that will be used to create the CGUI page one or multiple times (hotloading).
*/
SGUIPage(const CStrW& pageName, const ScriptInterface::StructuredClone initData);
/**
* Create the CGUI with it's own ScriptInterface. Deletes the previous CGUI if it existed.
*/
void LoadPage(shared_ptr scriptContext);
/**
* Sets the callback handler when a new page is opened that will be performed when the page is closed.
*/
void SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc);
/**
* Execute the stored callback function with the given arguments.
*/
void PerformCallbackFunction(ScriptInterface::StructuredClone args);
- CStrW name;
+ CStrW m_Name;
std::unordered_set inputs; // for hotloading
ScriptInterface::StructuredClone initData; // data to be passed to the init() function
shared_ptr gui; // the actual GUI page
/**
* Function executed by this parent GUI page when the child GUI page it pushed is popped.
* Notice that storing it in the SGUIPage instead of CGUI means that it will survive the hotloading CGUI reset.
*/
shared_ptr callbackFunction;
};
const static CStr EventNameWindowResized;
shared_ptr top() const;
shared_ptr m_ScriptContext;
shared_ptr m_ScriptInterface;
using PageStackType = std::vector;
PageStackType m_PageStack;
CTemplateLoader m_TemplateLoader;
};
extern CGUIManager* g_GUI;
extern InReaction gui_handler(const SDL_Event_* ev);
#endif // INCLUDED_GUIMANAGER
Index: ps/trunk/source/gui/ObjectTypes/CChart.cpp
===================================================================
--- ps/trunk/source/gui/ObjectTypes/CChart.cpp (revision 24267)
+++ ps/trunk/source/gui/ObjectTypes/CChart.cpp (revision 24268)
@@ -1,322 +1,316 @@
/* Copyright (C) 2020 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 "CChart.h"
#include "graphics/ShaderManager.h"
#include "gui/GUIMatrix.h"
#include "gui/SettingTypes/CGUIList.h"
#include "gui/SettingTypes/CGUISeries.h"
#include "gui/SettingTypes/CGUIString.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
#include "renderer/Renderer.h"
#include
CChart::CChart(CGUI& pGUI)
: IGUIObject(pGUI),
IGUITextOwner(*static_cast(this)),
m_AxisColor(),
m_AxisWidth(),
m_BufferZone(),
m_Font(),
m_FormatX(),
m_FormatY(),
m_SeriesColor(),
m_SeriesSetting(),
m_TextAlign()
{
RegisterSetting("axis_color", m_AxisColor);
RegisterSetting("axis_width", m_AxisWidth);
RegisterSetting("buffer_zone", m_BufferZone);
RegisterSetting("font", m_Font);
RegisterSetting("format_x", m_FormatX);
RegisterSetting("format_y", m_FormatY);
RegisterSetting("series_color", m_SeriesColor);
RegisterSetting("series", m_SeriesSetting);
RegisterSetting("text_align", m_TextAlign);
}
CChart::~CChart()
{
}
void CChart::UpdateCachedSize()
{
IGUIObject::UpdateCachedSize();
IGUITextOwner::UpdateCachedSize();
}
void CChart::HandleMessage(SGUIMessage& Message)
{
IGUIObject::HandleMessage(Message);
// IGUITextOwner::HandleMessage(Message); performed in UpdateSeries
// TODO: implement zoom
- switch (Message.type)
- {
- case GUIM_SETTINGS_UPDATED:
- {
+ if(Message.type == GUIM_SETTINGS_UPDATED)
UpdateSeries();
- break;
- }
- }
}
void CChart::DrawLine(const CShaderProgramPtr& shader, const CGUIColor& color, const std::vector& vertices) const
{
shader->Uniform(str_color, color);
shader->VertexPointer(3, GL_FLOAT, 0, &vertices[0]);
shader->AssertPointersBound();
#if !CONFIG2_GLES
glEnable(GL_LINE_SMOOTH);
#endif
glLineWidth(1.1f);
if (!g_Renderer.m_SkipSubmit)
glDrawArrays(GL_LINE_STRIP, 0, vertices.size() / 3);
glLineWidth(1.0f);
#if !CONFIG2_GLES
glDisable(GL_LINE_SMOOTH);
#endif
}
void CChart::DrawTriangleStrip(const CShaderProgramPtr& shader, const CGUIColor& color, const std::vector& vertices) const
{
shader->Uniform(str_color, color);
shader->VertexPointer(3, GL_FLOAT, 0, &vertices[0]);
shader->AssertPointersBound();
if (!g_Renderer.m_SkipSubmit)
glDrawArrays(GL_TRIANGLE_STRIP, 0, vertices.size() / 3);
}
void CChart::DrawAxes(const CShaderProgramPtr& shader) const
{
const float bz = GetBufferedZ();
CRect rect = GetChartRect();
std::vector vertices;
vertices.reserve(30);
#define ADD(x, y) vertices.push_back(x); vertices.push_back(y); vertices.push_back(bz + 0.5f);
ADD(m_CachedActualSize.right, m_CachedActualSize.bottom);
ADD(rect.right + m_AxisWidth, rect.bottom);
ADD(m_CachedActualSize.left, m_CachedActualSize.bottom);
ADD(rect.left, rect.bottom);
ADD(m_CachedActualSize.left, m_CachedActualSize.top);
ADD(rect.left, rect.top - m_AxisWidth);
#undef ADD
DrawTriangleStrip(shader, m_AxisColor, vertices);
}
void CChart::Draw()
{
PROFILE3("render chart");
if (m_Series.empty())
return;
const float bz = GetBufferedZ();
CRect rect = GetChartRect();
const float width = rect.GetWidth();
const float height = rect.GetHeight();
// Disable depth updates to prevent apparent z-fighting-related issues
// with some drivers causing units to get drawn behind the texture.
glDepthMask(0);
// Setup the render state
CMatrix3D transform = GetDefaultGuiMatrix();
CShaderDefines lineDefines;
CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid, g_Renderer.GetSystemShaderDefines(), lineDefines);
tech->BeginPass();
CShaderProgramPtr shader = tech->GetShader();
shader->Uniform(str_transform, transform);
CVector2D scale(width / (m_RightTop.X - m_LeftBottom.X), height / (m_RightTop.Y - m_LeftBottom.Y));
for (const CChartData& data : m_Series)
{
if (data.m_Points.empty())
continue;
std::vector vertices;
for (const CVector2D& point : data.m_Points)
{
if (fabs(point.X) != std::numeric_limits::infinity() && fabs(point.Y) != std::numeric_limits::infinity())
{
vertices.push_back(rect.left + (point.X - m_LeftBottom.X) * scale.X);
vertices.push_back(rect.bottom - (point.Y - m_LeftBottom.Y) * scale.Y);
vertices.push_back(bz + 0.5f);
}
else
{
DrawLine(shader, data.m_Color, vertices);
vertices.clear();
}
}
if (!vertices.empty())
DrawLine(shader, data.m_Color, vertices);
}
if (m_AxisWidth > 0)
DrawAxes(shader);
tech->EndPass();
// Reset depth mask
glDepthMask(1);
for (size_t i = 0; i < m_TextPositions.size(); ++i)
DrawText(i, CGUIColor(1.f, 1.f, 1.f, 1.f), m_TextPositions[i], bz + 0.5f);
}
CRect CChart::GetChartRect() const
{
return CRect(
m_CachedActualSize.TopLeft() + CPos(m_AxisWidth, m_AxisWidth),
m_CachedActualSize.BottomRight() - CPos(m_AxisWidth, m_AxisWidth)
);
}
void CChart::UpdateSeries()
{
m_Series.clear();
m_Series.resize(m_SeriesSetting.m_Series.size());
for (size_t i = 0; i < m_SeriesSetting.m_Series.size(); ++i)
{
CChartData& data = m_Series[i];
if (i < m_SeriesColor.m_Items.size() && !data.m_Color.ParseString(m_pGUI, m_SeriesColor.m_Items[i].GetOriginalString().ToUTF8(), 0))
LOGWARNING("GUI: Error parsing 'series_color' (\"%s\")", utf8_from_wstring(m_SeriesColor.m_Items[i].GetOriginalString()));
data.m_Points = m_SeriesSetting.m_Series[i];
}
UpdateBounds();
SetupText();
}
void CChart::SetupText()
{
m_GeneratedTexts.clear();
m_TextPositions.clear();
if (m_Series.empty())
return;
// Add Y-axis
const float height = GetChartRect().GetHeight();
// TODO: split values depend on the format;
if (m_EqualY)
{
// We don't need to generate many items for equal values
AddFormattedValue(m_FormatY, m_RightTop.Y, m_Font, m_BufferZone);
m_TextPositions.emplace_back(GetChartRect().TopLeft());
}
else
for (int i = 0; i < 3; ++i)
{
AddFormattedValue(m_FormatY, m_RightTop.Y - (m_RightTop.Y - m_LeftBottom.Y) / 3.f * i, m_Font, m_BufferZone);
m_TextPositions.emplace_back(GetChartRect().TopLeft() + CPos(0.f, height / 3.f * i));
}
// Add X-axis
const float width = GetChartRect().GetWidth();
if (m_EqualX)
{
CSize text_size = AddFormattedValue(m_FormatX, m_RightTop.X, m_Font, m_BufferZone);
m_TextPositions.emplace_back(GetChartRect().BottomRight() - text_size);
}
else
for (int i = 0; i < 3; ++i)
{
CSize text_size = AddFormattedValue(m_FormatX, m_RightTop.X - (m_RightTop.X - m_LeftBottom.X) / 3 * i, m_Font, m_BufferZone);
m_TextPositions.emplace_back(GetChartRect().BottomRight() - text_size - CPos(width / 3 * i, 0.f));
}
}
CSize CChart::AddFormattedValue(const CStrW& format, const float value, const CStrW& font, const float buffer_zone)
{
// TODO: we need to catch cases with equal formatted values.
CGUIString gui_str;
if (format == L"DECIMAL2")
{
wchar_t buffer[64];
swprintf(buffer, 64, L"%.2f", value);
gui_str.SetValue(buffer);
}
else if (format == L"INTEGER")
{
wchar_t buffer[64];
swprintf(buffer, 64, L"%d", std::lround(value));
gui_str.SetValue(buffer);
}
else if (format == L"DURATION_SHORT")
{
const int seconds = value;
wchar_t buffer[64];
swprintf(buffer, 64, L"%d:%02d", seconds / 60, seconds % 60);
gui_str.SetValue(buffer);
}
else if (format == L"PERCENTAGE")
{
wchar_t buffer[64];
swprintf(buffer, 64, L"%d%%", std::lround(value));
gui_str.SetValue(buffer);
}
else
{
LOGERROR("Unsupported chart format: " + format.EscapeToPrintableASCII());
return CSize();
}
return AddText(gui_str, font, 0, buffer_zone).GetSize();
}
void CChart::UpdateBounds()
{
if (m_Series.empty() || m_Series[0].m_Points.empty())
{
m_LeftBottom = m_RightTop = CVector2D(0.f, 0.f);
return;
}
m_LeftBottom = m_RightTop = m_Series[0].m_Points[0];
for (const CChartData& data : m_Series)
for (const CVector2D& point : data.m_Points)
{
if (fabs(point.X) != std::numeric_limits::infinity() && point.X < m_LeftBottom.X)
m_LeftBottom.X = point.X;
if (fabs(point.Y) != std::numeric_limits::infinity() && point.Y < m_LeftBottom.Y)
m_LeftBottom.Y = point.Y;
if (fabs(point.X) != std::numeric_limits::infinity() && point.X > m_RightTop.X)
m_RightTop.X = point.X;
if (fabs(point.Y) != std::numeric_limits::infinity() && point.Y > m_RightTop.Y)
m_RightTop.Y = point.Y;
}
m_EqualY = m_RightTop.Y == m_LeftBottom.Y;
if (m_EqualY)
m_RightTop.Y += 1;
m_EqualX = m_RightTop.X == m_LeftBottom.X;
if (m_EqualX)
m_RightTop.X += 1;
}
Index: ps/trunk/source/gui/ObjectTypes/CInput.cpp
===================================================================
--- ps/trunk/source/gui/ObjectTypes/CInput.cpp (revision 24267)
+++ ps/trunk/source/gui/ObjectTypes/CInput.cpp (revision 24268)
@@ -1,2070 +1,2070 @@
/* Copyright (C) 2020 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 "CInput.h"
#include "graphics/FontMetrics.h"
#include "graphics/ShaderManager.h"
#include "graphics/TextRenderer.h"
#include "gui/CGUI.h"
#include "gui/CGUIScrollBarVertical.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "ps/ConfigDB.h"
#include "ps/GameSetup/Config.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "renderer/Renderer.h"
#include
extern int g_yres;
const CStr CInput::EventNameTextEdit = "TextEdit";
const CStr CInput::EventNamePress = "Press";
const CStr CInput::EventNameTab = "Tab";
CInput::CInput(CGUI& pGUI)
:
IGUIObject(pGUI),
IGUIScrollBarOwner(*static_cast(this)),
m_iBufferPos(-1),
m_iBufferPos_Tail(-1),
m_SelectingText(),
m_HorizontalScroll(),
m_PrevTime(),
m_CursorVisState(true),
m_CursorBlinkRate(0.5),
m_ComposingText(),
m_iComposedLength(),
m_iComposedPos(),
m_iInsertPos(),
m_BufferPosition(),
m_BufferZone(),
m_Caption(),
m_CellID(),
m_Font(),
m_MaskChar(),
m_Mask(),
m_MaxLength(),
m_MultiLine(),
m_Readonly(),
m_ScrollBar(),
m_ScrollBarStyle(),
m_Sprite(),
m_SpriteSelectArea(),
m_TextColor(),
m_TextColorSelected()
{
RegisterSetting("buffer_position", m_BufferPosition);
RegisterSetting("buffer_zone", m_BufferZone);
RegisterSetting("caption", m_Caption);
RegisterSetting("cell_id", m_CellID);
RegisterSetting("font", m_Font);
RegisterSetting("mask_char", m_MaskChar);
RegisterSetting("mask", m_Mask);
RegisterSetting("max_length", m_MaxLength);
RegisterSetting("multiline", m_MultiLine);
RegisterSetting("readonly", m_Readonly);
RegisterSetting("scrollbar", m_ScrollBar);
RegisterSetting("scrollbar_style", m_ScrollBarStyle);
RegisterSetting("sprite", m_Sprite);
RegisterSetting("sprite_selectarea", m_SpriteSelectArea);
RegisterSetting("textcolor", m_TextColor);
RegisterSetting("textcolor_selected", m_TextColorSelected);
CFG_GET_VAL("gui.cursorblinkrate", m_CursorBlinkRate);
CGUIScrollBarVertical* bar = new CGUIScrollBarVertical(pGUI);
bar->SetRightAligned(true);
AddScrollBar(bar);
}
CInput::~CInput()
{
}
void CInput::UpdateBufferPositionSetting()
{
SetSetting("buffer_position", m_iBufferPos, false);
}
void CInput::ClearComposedText()
{
m_Caption.erase(m_iInsertPos, m_iComposedLength);
m_iBufferPos = m_iInsertPos;
UpdateBufferPositionSetting();
m_iComposedLength = 0;
m_iComposedPos = 0;
}
InReaction CInput::ManuallyHandleKeys(const SDL_Event_* ev)
{
ENSURE(m_iBufferPos != -1);
switch (ev->ev.type)
{
case SDL_HOTKEYDOWN:
{
if (m_ComposingText)
return IN_HANDLED;
return ManuallyHandleHotkeyEvent(ev);
}
// SDL2 has a new method of text input that better supports Unicode and CJK
// see https://wiki.libsdl.org/Tutorials/TextInput
case SDL_TEXTINPUT:
{
if (m_Readonly)
return IN_PASS;
// Text has been committed, either single key presses or through an IME
std::wstring text = wstring_from_utf8(ev->ev.text.text);
// Check max length
if (m_MaxLength != 0 && m_Caption.length() + text.length() > static_cast(m_MaxLength))
return IN_HANDLED;
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
if (m_ComposingText)
{
ClearComposedText();
m_ComposingText = false;
}
if (m_iBufferPos == static_cast(m_Caption.length()))
m_Caption.append(text);
else
m_Caption.insert(m_iBufferPos, text);
UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1);
m_iBufferPos += text.length();
UpdateBufferPositionSetting();
m_iBufferPos_Tail = -1;
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
return IN_HANDLED;
}
case SDL_TEXTEDITING:
{
if (m_Readonly)
return IN_PASS;
// Text is being composed with an IME
// TODO: indicate this by e.g. underlining the uncommitted text
const char* rawText = ev->ev.edit.text;
int rawLength = strlen(rawText);
std::wstring wtext = wstring_from_utf8(rawText);
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
// Remember cursor position when text composition begins
if (!m_ComposingText)
m_iInsertPos = m_iBufferPos;
else
{
// Composed text is replaced each time
ClearComposedText();
}
m_ComposingText = ev->ev.edit.start != 0 || rawLength != 0;
if (m_ComposingText)
{
m_Caption.insert(m_iInsertPos, wtext);
// The text buffer is limited to SDL_TEXTEDITINGEVENT_TEXT_SIZE bytes, yet start
// increases without limit, so don't let it advance beyond the composed text length
m_iComposedLength = wtext.length();
m_iComposedPos = ev->ev.edit.start < m_iComposedLength ? ev->ev.edit.start : m_iComposedLength;
m_iBufferPos = m_iInsertPos + m_iComposedPos;
// TODO: composed text selection - what does ev.edit.length do?
m_iBufferPos_Tail = -1;
}
UpdateBufferPositionSetting();
UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1);
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
return IN_HANDLED;
}
case SDL_KEYDOWN:
{
if (m_ComposingText)
return IN_HANDLED;
// Since the GUI framework doesn't handle to set settings
// in Unicode (CStrW), we'll simply retrieve the actual
// pointer and edit that.
SDL_Keycode keyCode = ev->ev.key.keysym.sym;
// Escape is treated specially to let players close out windows even if an input is in focus.
// TODO: there is maybe a better way to only handle text-like keys here?
if (keyCode == SDLK_ESCAPE)
return IN_PASS;
ManuallyImmutableHandleKeyDownEvent(keyCode);
ManuallyMutableHandleKeyDownEvent(keyCode);
UpdateBufferPositionSetting();
return IN_HANDLED;
}
default:
{
return IN_PASS;
}
}
}
void CInput::ManuallyMutableHandleKeyDownEvent(const SDL_Keycode keyCode)
{
if (m_Readonly)
return;
wchar_t cooked = 0;
switch (keyCode)
{
case SDLK_TAB:
{
SendEvent(GUIM_TAB, EventNameTab);
// Don't send a textedit event, because it should only
// be sent if the GUI control changes the text
break;
}
case SDLK_BACKSPACE:
{
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
else
{
m_iBufferPos_Tail = -1;
if (m_Caption.empty() || m_iBufferPos == 0)
break;
if (m_iBufferPos == static_cast(m_Caption.length()))
m_Caption = m_Caption.Left(static_cast(m_Caption.length()) - 1);
else
m_Caption =
m_Caption.Left(m_iBufferPos - 1) +
m_Caption.Right(static_cast(m_Caption.length()) - m_iBufferPos);
--m_iBufferPos;
UpdateText(m_iBufferPos, m_iBufferPos + 1, m_iBufferPos);
}
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
break;
}
case SDLK_DELETE:
{
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
else
{
if (m_Caption.empty() || m_iBufferPos == static_cast(m_Caption.length()))
break;
m_Caption =
m_Caption.Left(m_iBufferPos) +
m_Caption.Right(static_cast(m_Caption.length()) - (m_iBufferPos + 1));
UpdateText(m_iBufferPos, m_iBufferPos + 1, m_iBufferPos);
}
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
break;
}
case SDLK_KP_ENTER:
case SDLK_RETURN:
{
// 'Return' should do a Press event for single liners (e.g. submitting forms)
// otherwise a '\n' character will be added.
if (!m_MultiLine)
{
SendEvent(GUIM_PRESSED, EventNamePress);
break;
}
cooked = '\n'; // Change to '\n' and do default:
FALLTHROUGH;
}
default: // Insert a character
{
// In SDL2, we no longer get Unicode wchars via SDL_Keysym
// we use text input events instead and they provide UTF-8 chars
if (cooked == 0)
return;
// Check max length
if (m_MaxLength != 0 && m_Caption.length() >= static_cast(m_MaxLength))
break;
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
m_iBufferPos_Tail = -1;
if (m_iBufferPos == static_cast(m_Caption.length()))
m_Caption += cooked;
else
m_Caption =
m_Caption.Left(m_iBufferPos) + cooked +
m_Caption.Right(static_cast(m_Caption.length()) - m_iBufferPos);
UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos + 1);
++m_iBufferPos;
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
break;
}
}
}
void CInput::ManuallyImmutableHandleKeyDownEvent(const SDL_Keycode keyCode)
{
bool shiftKeyPressed = g_keys[SDLK_RSHIFT] || g_keys[SDLK_LSHIFT];
switch (keyCode)
{
case SDLK_HOME:
{
// If there's not a selection, we should create one now
if (!shiftKeyPressed)
{
// Make sure a selection isn't created.
m_iBufferPos_Tail = -1;
}
else if (!SelectingText())
{
// Place tail at the current point:
m_iBufferPos_Tail = m_iBufferPos;
}
m_iBufferPos = 0;
m_WantedX = 0.0f;
UpdateAutoScroll();
break;
}
case SDLK_END:
{
// If there's not a selection, we should create one now
if (!shiftKeyPressed)
{
// Make sure a selection isn't created.
m_iBufferPos_Tail = -1;
}
else if (!SelectingText())
{
// Place tail at the current point:
m_iBufferPos_Tail = m_iBufferPos;
}
m_iBufferPos = static_cast(m_Caption.length());
m_WantedX = 0.0f;
UpdateAutoScroll();
break;
}
/**
* Conventions for Left/Right when text is selected:
*
* References:
*
* Visual Studio
* Visual Studio has the 'newer' approach, used by newer versions of
* things, and in newer applications. A left press will always place
* the pointer on the left edge of the selection, and then of course
* remove the selection. Right will do the exact same thing.
* If you have the pointer on the right edge and press right, it will
* in other words just remove the selection.
*
* Windows (eg. Notepad)
* A left press always takes the pointer a step to the left and
* removes the selection as if it were never there in the first place.
* Right of course does the same thing but to the right.
*
* I chose the Visual Studio convention. Used also in Word, gtk 2.0, MSN
* Messenger.
*/
case SDLK_LEFT:
{
m_WantedX = 0.f;
if (shiftKeyPressed || !SelectingText())
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
if (m_iBufferPos > 0)
--m_iBufferPos;
}
else
{
if (m_iBufferPos_Tail < m_iBufferPos)
m_iBufferPos = m_iBufferPos_Tail;
m_iBufferPos_Tail = -1;
}
UpdateAutoScroll();
break;
}
case SDLK_RIGHT:
{
m_WantedX = 0.0f;
if (shiftKeyPressed || !SelectingText())
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
if (m_iBufferPos < static_cast(m_Caption.length()))
++m_iBufferPos;
}
else
{
if (m_iBufferPos_Tail > m_iBufferPos)
m_iBufferPos = m_iBufferPos_Tail;
m_iBufferPos_Tail = -1;
}
UpdateAutoScroll();
break;
}
/**
* Conventions for Up/Down when text is selected:
*
* References:
*
* Visual Studio
* Visual Studio has a very strange approach, down takes you below the
* selection to the next row, and up to the one prior to the whole
* selection. The weird part is that it is always aligned as the
* 'pointer'. I decided this is to much work for something that is
* a bit arbitrary
*
* Windows (eg. Notepad)
* Just like with left/right, the selection is destroyed and it moves
* just as if there never were a selection.
*
* I chose the Notepad convention even though I use the VS convention with
* left/right.
*/
case SDLK_UP:
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
std::list::iterator current = m_CharacterPositions.begin();
while (current != m_CharacterPositions.end())
{
if (m_iBufferPos >= current->m_ListStart &&
m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size())
break;
++current;
}
float pos_x;
if (m_iBufferPos - current->m_ListStart == 0)
pos_x = 0.f;
else
pos_x = current->m_ListOfX[m_iBufferPos - current->m_ListStart - 1];
if (m_WantedX > pos_x)
pos_x = m_WantedX;
// Now change row:
if (current != m_CharacterPositions.begin())
{
--current;
// Find X-position:
m_iBufferPos = current->m_ListStart + GetXTextPosition(current, pos_x, m_WantedX);
}
// else we can't move up
UpdateAutoScroll();
break;
}
case SDLK_DOWN:
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
std::list::iterator current = m_CharacterPositions.begin();
while (current != m_CharacterPositions.end())
{
if (m_iBufferPos >= current->m_ListStart &&
m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size())
break;
++current;
}
float pos_x;
if (m_iBufferPos - current->m_ListStart == 0)
pos_x = 0.f;
else
pos_x = current->m_ListOfX[m_iBufferPos - current->m_ListStart - 1];
if (m_WantedX > pos_x)
pos_x = m_WantedX;
// Now change row:
// Add first, so we can check if it's .end()
++current;
if (current != m_CharacterPositions.end())
{
// Find X-position:
m_iBufferPos = current->m_ListStart + GetXTextPosition(current, pos_x, m_WantedX);
}
// else we can't move up
UpdateAutoScroll();
break;
}
case SDLK_PAGEUP:
{
GetScrollBar(0).ScrollMinusPlenty();
UpdateAutoScroll();
break;
}
case SDLK_PAGEDOWN:
{
GetScrollBar(0).ScrollPlusPlenty();
UpdateAutoScroll();
break;
}
default:
{
break;
}
}
}
InReaction CInput::ManuallyHandleHotkeyEvent(const SDL_Event_* ev)
{
bool shiftKeyPressed = g_keys[SDLK_RSHIFT] || g_keys[SDLK_LSHIFT];
std::string hotkey = static_cast(ev->ev.user.data1);
if (hotkey == "paste")
{
if (m_Readonly)
return IN_PASS;
m_WantedX = 0.0f;
char* utf8_text = SDL_GetClipboardText();
if (!utf8_text)
return IN_HANDLED;
std::wstring text = wstring_from_utf8(utf8_text);
SDL_free(utf8_text);
// Check max length
if (m_MaxLength != 0 && m_Caption.length() + text.length() > static_cast(m_MaxLength))
text = text.substr(0, static_cast(m_MaxLength) - m_Caption.length());
if (SelectingText())
DeleteCurSelection();
if (m_iBufferPos == static_cast(m_Caption.length()))
m_Caption += text;
else
m_Caption =
m_Caption.Left(m_iBufferPos) + text +
m_Caption.Right(static_cast(m_Caption.length()) - m_iBufferPos);
UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1);
m_iBufferPos += static_cast(text.size());
UpdateAutoScroll();
UpdateBufferPositionSetting();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
return IN_HANDLED;
}
else if (hotkey == "copy" || hotkey == "cut")
{
if (m_Readonly && hotkey == "cut")
return IN_PASS;
m_WantedX = 0.0f;
if (SelectingText())
{
int virtualFrom;
int virtualTo;
if (m_iBufferPos_Tail >= m_iBufferPos)
{
virtualFrom = m_iBufferPos;
virtualTo = m_iBufferPos_Tail;
}
else
{
virtualFrom = m_iBufferPos_Tail;
virtualTo = m_iBufferPos;
}
CStrW text = m_Caption.Left(virtualTo).Right(virtualTo - virtualFrom);
SDL_SetClipboardText(text.ToUTF8().c_str());
if (hotkey == "cut")
{
DeleteCurSelection();
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
}
}
return IN_HANDLED;
}
else if (hotkey == "text.delete.left")
{
if (m_Readonly)
return IN_PASS;
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
if (!m_Caption.empty() && m_iBufferPos != 0)
{
m_iBufferPos_Tail = m_iBufferPos;
CStrW searchString = m_Caption.Left(m_iBufferPos);
// If we are starting in whitespace, adjust position until we get a non whitespace
while (m_iBufferPos > 0)
{
if (!iswspace(searchString[m_iBufferPos - 1]))
break;
m_iBufferPos--;
}
// If we end up on a punctuation char we just delete it (treat punct like a word)
if (iswpunct(searchString[m_iBufferPos - 1]))
m_iBufferPos--;
else
{
// Now we are on a non white space character, adjust position to char after next whitespace char is found
while (m_iBufferPos > 0)
{
if (iswspace(searchString[m_iBufferPos - 1]) || iswpunct(searchString[m_iBufferPos - 1]))
break;
m_iBufferPos--;
}
}
UpdateBufferPositionSetting();
DeleteCurSelection();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
}
UpdateAutoScroll();
return IN_HANDLED;
}
else if (hotkey == "text.delete.right")
{
if (m_Readonly)
return IN_PASS;
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
if (!m_Caption.empty() && m_iBufferPos < static_cast(m_Caption.length()))
{
// Delete the word to the right of the cursor
m_iBufferPos_Tail = m_iBufferPos;
// Delete chars to the right unit we hit whitespace
while (++m_iBufferPos < static_cast(m_Caption.length()))
{
if (iswspace(m_Caption[m_iBufferPos]) || iswpunct(m_Caption[m_iBufferPos]))
break;
}
// Eliminate any whitespace behind the word we just deleted
while (m_iBufferPos < static_cast(m_Caption.length()))
{
if (!iswspace(m_Caption[m_iBufferPos]))
break;
++m_iBufferPos;
}
UpdateBufferPositionSetting();
DeleteCurSelection();
}
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
return IN_HANDLED;
}
else if (hotkey == "text.move.left")
{
m_WantedX = 0.0f;
if (shiftKeyPressed || !SelectingText())
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
if (!m_Caption.empty() && m_iBufferPos != 0)
{
CStrW searchString = m_Caption.Left(m_iBufferPos);
// If we are starting in whitespace, adjust position until we get a non whitespace
while (m_iBufferPos > 0)
{
if (!iswspace(searchString[m_iBufferPos - 1]))
break;
m_iBufferPos--;
}
// If we end up on a puctuation char we just select it (treat punct like a word)
if (iswpunct(searchString[m_iBufferPos - 1]))
m_iBufferPos--;
else
{
// Now we are on a non white space character, adjust position to char after next whitespace char is found
while (m_iBufferPos > 0)
{
if (iswspace(searchString[m_iBufferPos - 1]) || iswpunct(searchString[m_iBufferPos - 1]))
break;
m_iBufferPos--;
}
}
}
}
else
{
if (m_iBufferPos_Tail < m_iBufferPos)
m_iBufferPos = m_iBufferPos_Tail;
m_iBufferPos_Tail = -1;
}
UpdateBufferPositionSetting();
UpdateAutoScroll();
return IN_HANDLED;
}
else if (hotkey == "text.move.right")
{
m_WantedX = 0.0f;
if (shiftKeyPressed || !SelectingText())
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
if (!m_Caption.empty() && m_iBufferPos < static_cast(m_Caption.length()))
{
// Select chars to the right until we hit whitespace
while (++m_iBufferPos < static_cast(m_Caption.length()))
{
if (iswspace(m_Caption[m_iBufferPos]) || iswpunct(m_Caption[m_iBufferPos]))
break;
}
// Also select any whitespace following the word we just selected
while (m_iBufferPos < static_cast(m_Caption.length()))
{
if (!iswspace(m_Caption[m_iBufferPos]))
break;
++m_iBufferPos;
}
}
}
else
{
if (m_iBufferPos_Tail > m_iBufferPos)
m_iBufferPos = m_iBufferPos_Tail;
m_iBufferPos_Tail = -1;
}
UpdateBufferPositionSetting();
UpdateAutoScroll();
return IN_HANDLED;
}
return IN_PASS;
}
void CInput::ResetStates()
{
IGUIObject::ResetStates();
IGUIScrollBarOwner::ResetStates();
}
void CInput::HandleMessage(SGUIMessage& Message)
{
IGUIObject::HandleMessage(Message);
IGUIScrollBarOwner::HandleMessage(Message);
switch (Message.type)
{
case GUIM_SETTINGS_UPDATED:
{
// Update scroll-bar
// TODO Gee: (2004-09-01) Is this really updated each time it should?
if (m_ScrollBar &&
(Message.value == "size" ||
Message.value == "z" ||
Message.value == "absolute"))
{
GetScrollBar(0).SetX(m_CachedActualSize.right);
GetScrollBar(0).SetY(m_CachedActualSize.top);
GetScrollBar(0).SetZ(GetBufferedZ());
GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top);
}
// Update scrollbar
if (Message.value == "scrollbar_style")
GetScrollBar(0).SetScrollBarStyle(m_ScrollBarStyle);
if (Message.value == "buffer_position")
{
m_iBufferPos = m_MaxLength != 0 ? std::min(m_MaxLength, m_BufferPosition) : m_BufferPosition;
m_iBufferPos_Tail = -1; // position change resets selection
}
if (Message.value == "size" ||
Message.value == "z" ||
Message.value == "font" ||
Message.value == "absolute" ||
Message.value == "caption" ||
Message.value == "scrollbar" ||
Message.value == "scrollbar_style")
{
UpdateText();
}
if (Message.value == "multiline")
{
if (!m_MultiLine)
GetScrollBar(0).SetLength(0.f);
else
GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top);
UpdateText();
}
UpdateAutoScroll();
break;
}
case GUIM_MOUSE_PRESS_LEFT:
{
// Check if we're selecting the scrollbar
if (m_ScrollBar &&
m_MultiLine &&
GetScrollBar(0).GetStyle())
{
if (m_pGUI.GetMousePos().x > m_CachedActualSize.right - GetScrollBar(0).GetStyle()->m_Width)
break;
}
if (m_ComposingText)
break;
// Okay, this section is about pressing the mouse and
// choosing where the point should be placed. For
// instance, if we press between a and b, the point
// should of course be placed accordingly. Other
// special cases are handled like the input box norms.
if (g_keys[SDLK_RSHIFT] || g_keys[SDLK_LSHIFT])
m_iBufferPos = GetMouseHoveringTextPosition();
else
m_iBufferPos = m_iBufferPos_Tail = GetMouseHoveringTextPosition();
m_SelectingText = true;
UpdateAutoScroll();
// If we immediately release the button it will just be seen as a click
// for the user though.
break;
}
case GUIM_MOUSE_DBLCLICK_LEFT:
{
if (m_ComposingText)
break;
if (m_Caption.empty())
break;
m_iBufferPos = m_iBufferPos_Tail = GetMouseHoveringTextPosition();
if (m_iBufferPos >= (int)m_Caption.length())
m_iBufferPos = m_iBufferPos_Tail = m_Caption.length() - 1;
// See if we are clicking over whitespace
if (iswspace(m_Caption[m_iBufferPos]))
{
// see if we are in a section of whitespace greater than one character
if ((m_iBufferPos + 1 < (int) m_Caption.length() && iswspace(m_Caption[m_iBufferPos + 1])) ||
(m_iBufferPos - 1 > 0 && iswspace(m_Caption[m_iBufferPos - 1])))
{
//
// We are clicking in an area with more than one whitespace character
// so we select both the word to the left and then the word to the right
//
// [1] First the left
// skip the whitespace
while (m_iBufferPos > 0)
{
if (!iswspace(m_Caption[m_iBufferPos - 1]))
break;
m_iBufferPos--;
}
// now go until we hit white space or punctuation
while (m_iBufferPos > 0)
{
if (iswspace(m_Caption[m_iBufferPos - 1]))
break;
m_iBufferPos--;
if (iswpunct(m_Caption[m_iBufferPos]))
break;
}
// [2] Then the right
// go right until we are not in whitespace
while (++m_iBufferPos_Tail < static_cast(m_Caption.length()))
{
if (!iswspace(m_Caption[m_iBufferPos_Tail]))
break;
}
if (m_iBufferPos_Tail == static_cast(m_Caption.length()))
break;
// now go to the right until we hit whitespace or punctuation
while (++m_iBufferPos_Tail < static_cast(m_Caption.length()))
{
if (iswspace(m_Caption[m_iBufferPos_Tail]) || iswpunct(m_Caption[m_iBufferPos_Tail]))
break;
}
}
else
{
// single whitespace so select word to the right
while (++m_iBufferPos_Tail < static_cast(m_Caption.length()))
{
if (!iswspace(m_Caption[m_iBufferPos_Tail]))
break;
}
if (m_iBufferPos_Tail == static_cast(m_Caption.length()))
break;
// Don't include the leading whitespace
m_iBufferPos = m_iBufferPos_Tail;
// now go to the right until we hit whitespace or punctuation
while (++m_iBufferPos_Tail < static_cast(m_Caption.length()))
{
if (iswspace(m_Caption[m_iBufferPos_Tail]) || iswpunct(m_Caption[m_iBufferPos_Tail]))
break;
}
}
}
else
{
// clicked on non-whitespace so select current word
// go until we hit white space or punctuation
while (m_iBufferPos > 0)
{
if (iswspace(m_Caption[m_iBufferPos - 1]))
break;
m_iBufferPos--;
if (iswpunct(m_Caption[m_iBufferPos]))
break;
}
// go to the right until we hit whitespace or punctuation
while (++m_iBufferPos_Tail < static_cast(m_Caption.length()))
if (iswspace(m_Caption[m_iBufferPos_Tail]) || iswpunct(m_Caption[m_iBufferPos_Tail]))
break;
}
UpdateAutoScroll();
break;
}
case GUIM_MOUSE_RELEASE_LEFT:
{
if (m_SelectingText)
m_SelectingText = false;
break;
}
case GUIM_MOUSE_MOTION:
{
// If we just pressed down and started to move before releasing
// this is one way of selecting larger portions of text.
if (m_SelectingText)
{
// Actually, first we need to re-check that the mouse button is
// really pressed (it can be released while outside the control.
if (!g_mouse_buttons[SDL_BUTTON_LEFT])
m_SelectingText = false;
else
m_iBufferPos = GetMouseHoveringTextPosition();
UpdateAutoScroll();
}
break;
}
case GUIM_LOAD:
{
GetScrollBar(0).SetX(m_CachedActualSize.right);
GetScrollBar(0).SetY(m_CachedActualSize.top);
GetScrollBar(0).SetZ(GetBufferedZ());
GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top);
GetScrollBar(0).SetScrollBarStyle(m_ScrollBarStyle);
UpdateText();
UpdateAutoScroll();
break;
}
case GUIM_GOT_FOCUS:
{
m_iBufferPos = 0;
m_PrevTime = 0.0;
m_CursorVisState = false;
// Tell the IME where to draw the candidate list
SDL_Rect rect;
rect.h = m_CachedActualSize.GetSize().cy;
rect.w = m_CachedActualSize.GetSize().cx;
rect.x = m_CachedActualSize.TopLeft().x;
rect.y = m_CachedActualSize.TopLeft().y;
SDL_SetTextInputRect(&rect);
SDL_StartTextInput();
break;
}
case GUIM_LOST_FOCUS:
{
if (m_ComposingText)
{
// Simulate a final text editing event to clear the composition
SDL_Event_ evt;
evt.ev.type = SDL_TEXTEDITING;
evt.ev.edit.length = 0;
evt.ev.edit.start = 0;
evt.ev.edit.text[0] = 0;
ManuallyHandleKeys(&evt);
}
SDL_StopTextInput();
m_iBufferPos = -1;
m_iBufferPos_Tail = -1;
break;
}
default:
{
break;
}
}
UpdateBufferPositionSetting();
}
void CInput::UpdateCachedSize()
{
// If an ancestor's size changed, this will let us intercept the change and
// update our scrollbar positions
IGUIObject::UpdateCachedSize();
if (m_ScrollBar)
{
GetScrollBar(0).SetX(m_CachedActualSize.right);
GetScrollBar(0).SetY(m_CachedActualSize.top);
GetScrollBar(0).SetZ(GetBufferedZ());
GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top);
}
}
void CInput::Draw()
{
float bz = GetBufferedZ();
if (m_CursorBlinkRate > 0.0)
{
// check if the cursor visibility state needs to be changed
double currTime = timer_Time();
if (currTime - m_PrevTime >= m_CursorBlinkRate)
{
m_CursorVisState = !m_CursorVisState;
m_PrevTime = currTime;
}
}
else
// should always be visible
m_CursorVisState = true;
// First call draw on ScrollBarOwner
if (m_ScrollBar && m_MultiLine)
IGUIScrollBarOwner::Draw();
CStrIntern font_name(m_Font.ToUTF8());
wchar_t mask_char = L'*';
if (m_Mask && m_MaskChar.length() > 0)
mask_char = m_MaskChar[0];
m_pGUI.DrawSprite(m_Sprite, m_CellID, bz, m_CachedActualSize);
float scroll = 0.f;
if (m_ScrollBar && m_MultiLine)
scroll = GetScrollBar(0).GetPos();
CFontMetrics font(font_name);
// We'll have to setup clipping manually, since we're doing the rendering manually.
CRect cliparea(m_CachedActualSize);
// First we'll figure out the clipping area, which is the cached actual size
// substracted by an optional scrollbar
if (m_ScrollBar)
{
scroll = GetScrollBar(0).GetPos();
// substract scrollbar from cliparea
if (cliparea.right > GetScrollBar(0).GetOuterRect().left &&
cliparea.right <= GetScrollBar(0).GetOuterRect().right)
cliparea.right = GetScrollBar(0).GetOuterRect().left;
if (cliparea.left >= GetScrollBar(0).GetOuterRect().left &&
cliparea.left < GetScrollBar(0).GetOuterRect().right)
cliparea.left = GetScrollBar(0).GetOuterRect().right;
}
if (cliparea != CRect())
{
glEnable(GL_SCISSOR_TEST);
glScissor(
cliparea.left * g_GuiScale,
g_yres - cliparea.bottom * g_GuiScale,
cliparea.GetWidth() * g_GuiScale,
cliparea.GetHeight() * g_GuiScale);
}
// These are useful later.
int VirtualFrom, VirtualTo;
if (m_iBufferPos_Tail >= m_iBufferPos)
{
VirtualFrom = m_iBufferPos;
VirtualTo = m_iBufferPos_Tail;
}
else
{
VirtualFrom = m_iBufferPos_Tail;
VirtualTo = m_iBufferPos;
}
// Get the height of this font.
float h = (float)font.GetHeight();
float ls = (float)font.GetLineSpacing();
CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text);
CTextRenderer textRenderer(tech->GetShader());
textRenderer.Font(font_name);
// Set the Z to somewhat more, so we can draw a selected area between the
// the control and the text.
textRenderer.Translate(
(float)(int)(m_CachedActualSize.left) + m_BufferZone,
(float)(int)(m_CachedActualSize.top+h) + m_BufferZone,
bz+0.1f);
// U+FE33: PRESENTATION FORM FOR VERTICAL LOW LINE
// (sort of like a | which is aligned to the left of most characters)
float buffered_y = -scroll + m_BufferZone;
// When selecting larger areas, we need to draw a rectangle box
// around it, and this is to keep track of where the box
// started, because we need to follow the iteration until we
// reach the end, before we can actually draw it.
bool drawing_box = false;
float box_x = 0.f;
float x_pointer = 0.f;
// If we have a selecting box (i.e. when you have selected letters, not just when
// the pointer is between two letters) we need to process all letters once
// before we do it the second time and render all the text. We can't do it
// in the same loop because text will have been drawn, so it will disappear when
// drawn behind the text that has already been drawn. Confusing, well it's necessary
// (I think).
if (SelectingText())
{
// Now m_iBufferPos_Tail can be of both sides of m_iBufferPos,
// just like you can select from right to left, as you can
// left to right. Is there a difference? Yes, the pointer
// be placed accordingly, so that if you select shift and
// expand this selection, it will expand on appropriate side.
// Anyway, since the drawing procedure needs "To" to be
// greater than from, we need virtual values that might switch
// place.
-
- int VirtualFrom, VirtualTo;
+ int virtualFrom = 0;
+ int virtualTo = 0;
if (m_iBufferPos_Tail >= m_iBufferPos)
{
- VirtualFrom = m_iBufferPos;
- VirtualTo = m_iBufferPos_Tail;
+ virtualFrom = m_iBufferPos;
+ virtualTo = m_iBufferPos_Tail;
}
else
{
- VirtualFrom = m_iBufferPos_Tail;
- VirtualTo = m_iBufferPos;
+ virtualFrom = m_iBufferPos_Tail;
+ virtualTo = m_iBufferPos;
}
bool done = false;
for (std::list::const_iterator it = m_CharacterPositions.begin();
it != m_CharacterPositions.end();
++it, buffered_y += ls, x_pointer = 0.f)
{
if (m_MultiLine && buffered_y > m_CachedActualSize.GetHeight())
break;
// We might as well use 'i' here to iterate, because we need it
// (often compared against ints, so don't make it size_t)
for (int i = 0; i < (int)it->m_ListOfX.size()+2; ++i)
{
- if (it->m_ListStart + i == VirtualFrom)
+ if (it->m_ListStart + i == virtualFrom)
{
// we won't actually draw it now, because we don't
// know the width of each glyph to that position.
// we need to go along with the iteration, and
// make a mark where the box started:
drawing_box = true; // will turn false when finally rendered.
// Get current x position
box_x = x_pointer;
}
const bool at_end = (i == (int)it->m_ListOfX.size()+1);
- if (drawing_box && (it->m_ListStart + i == VirtualTo || at_end))
+ if (drawing_box && (it->m_ListStart + i == virtualTo || at_end))
{
// Depending on if it's just a row change, or if it's
// the end of the select box, do slightly different things.
if (at_end)
{
- if (it->m_ListStart + i != VirtualFrom)
+ if (it->m_ListStart + i != virtualFrom)
// and actually add a white space! yes, this is done in any common input
x_pointer += font.GetCharacterWidth(L' ');
}
else
{
drawing_box = false;
done = true;
}
CRect rect;
// Set 'rect' depending on if it's a multiline control, or a one-line control
if (m_MultiLine)
{
rect = CRect(
m_CachedActualSize.left + box_x + m_BufferZone,
m_CachedActualSize.top + buffered_y + (h - ls) / 2,
m_CachedActualSize.left + x_pointer + m_BufferZone,
m_CachedActualSize.top + buffered_y + (h + ls) / 2);
if (rect.bottom < m_CachedActualSize.top)
continue;
if (rect.top < m_CachedActualSize.top)
rect.top = m_CachedActualSize.top;
if (rect.bottom > m_CachedActualSize.bottom)
rect.bottom = m_CachedActualSize.bottom;
}
else // if one-line
{
rect = CRect(
m_CachedActualSize.left + box_x + m_BufferZone - m_HorizontalScroll,
m_CachedActualSize.top + buffered_y + (h - ls) / 2,
m_CachedActualSize.left + x_pointer + m_BufferZone - m_HorizontalScroll,
m_CachedActualSize.top + buffered_y + (h + ls) / 2);
if (rect.left < m_CachedActualSize.left)
rect.left = m_CachedActualSize.left;
if (rect.right > m_CachedActualSize.right)
rect.right = m_CachedActualSize.right;
}
m_pGUI.DrawSprite(m_SpriteSelectArea, m_CellID, bz + 0.05f, rect);
}
if (i < (int)it->m_ListOfX.size())
{
if (!m_Mask)
x_pointer += font.GetCharacterWidth(m_Caption[it->m_ListStart + i]);
else
x_pointer += font.GetCharacterWidth(mask_char);
}
}
if (done)
break;
// If we're about to draw a box, and all of a sudden changes
// line, we need to draw that line's box, and then reset
// the box drawing to the beginning of the new line.
if (drawing_box)
box_x = 0.f;
}
}
// Reset some from previous run
buffered_y = -scroll;
// Setup initial color (then it might change and change back, when drawing selected area)
textRenderer.Color(m_TextColor);
tech->BeginPass();
bool using_selected_color = false;
for (std::list::const_iterator it = m_CharacterPositions.begin();
it != m_CharacterPositions.end();
++it, buffered_y += ls)
{
if (buffered_y + m_BufferZone >= -ls || !m_MultiLine)
{
if (m_MultiLine && buffered_y + m_BufferZone > m_CachedActualSize.GetHeight())
break;
CMatrix3D savedTransform = textRenderer.GetTransform();
// Text must always be drawn in integer values. So we have to convert scroll
if (m_MultiLine)
textRenderer.Translate(0.f, -(float)(int)scroll, 0.f);
else
textRenderer.Translate(-(float)(int)m_HorizontalScroll, 0.f, 0.f);
// We might as well use 'i' here, because we need it
// (often compared against ints, so don't make it size_t)
for (int i = 0; i < (int)it->m_ListOfX.size()+1; ++i)
{
if (!m_MultiLine && i < (int)it->m_ListOfX.size())
{
if (it->m_ListOfX[i] - m_HorizontalScroll < -m_BufferZone)
{
// We still need to translate the OpenGL matrix
if (i == 0)
textRenderer.Translate(it->m_ListOfX[i], 0.f, 0.f);
else
textRenderer.Translate(it->m_ListOfX[i] - it->m_ListOfX[i-1], 0.f, 0.f);
continue;
}
}
// End of selected area, change back color
if (SelectingText() && it->m_ListStart + i == VirtualTo)
{
using_selected_color = false;
textRenderer.Color(m_TextColor);
}
// selecting only one, then we need only to draw a cursor.
if (i != (int)it->m_ListOfX.size() && it->m_ListStart + i == m_iBufferPos && m_CursorVisState)
textRenderer.Put(0.0f, 0.0f, L"_");
// Drawing selected area
if (SelectingText() &&
it->m_ListStart + i >= VirtualFrom &&
it->m_ListStart + i < VirtualTo &&
!using_selected_color)
{
using_selected_color = true;
textRenderer.Color(m_TextColorSelected);
}
if (i != (int)it->m_ListOfX.size())
{
if (!m_Mask)
textRenderer.PrintfAdvance(L"%lc", m_Caption[it->m_ListStart + i]);
else
textRenderer.PrintfAdvance(L"%lc", mask_char);
}
// check it's now outside a one-liner, then we'll break
if (!m_MultiLine && i < (int)it->m_ListOfX.size() &&
it->m_ListOfX[i] - m_HorizontalScroll > m_CachedActualSize.GetWidth() - m_BufferZone)
break;
}
if (it->m_ListStart + (int)it->m_ListOfX.size() == m_iBufferPos)
{
textRenderer.Color(m_TextColor);
if (m_CursorVisState)
textRenderer.PutAdvance(L"_");
if (using_selected_color)
textRenderer.Color(m_TextColorSelected);
}
textRenderer.SetTransform(savedTransform);
}
textRenderer.Translate(0.f, ls, 0.f);
}
textRenderer.Render();
if (cliparea != CRect())
glDisable(GL_SCISSOR_TEST);
tech->EndPass();
}
void CInput::UpdateText(int from, int to_before, int to_after)
{
if (m_MaxLength != 0 && m_Caption.length() > static_cast(m_MaxLength))
m_Caption = m_Caption.substr(0, m_MaxLength);
CStrIntern font_name(m_Font.ToUTF8());
wchar_t mask_char = L'*';
if (m_Mask && m_MaskChar.length() > 0)
mask_char = m_MaskChar[0];
// Ensure positions are valid after caption changes
m_iBufferPos = std::min(m_iBufferPos, static_cast(m_Caption.size()));
m_iBufferPos_Tail = std::min(m_iBufferPos_Tail, static_cast(m_Caption.size()));
UpdateBufferPositionSetting();
if (font_name.empty())
{
// Destroy everything stored, there's no font, so there can be no data.
m_CharacterPositions.clear();
return;
}
SRow row;
row.m_ListStart = 0;
int to = 0; // make sure it's initialized
if (to_before == -1)
to = static_cast(m_Caption.length());
CFontMetrics font(font_name);
std::list::iterator current_line;
// Used to ... TODO
int check_point_row_start = -1;
int check_point_row_end = -1;
// Reset
if (from == 0 && to_before == -1)
{
m_CharacterPositions.clear();
current_line = m_CharacterPositions.begin();
}
else
{
ENSURE(to_before != -1);
std::list::iterator destroy_row_from;
std::list::iterator destroy_row_to;
// Used to check if the above has been set to anything,
// previously a comparison like:
// destroy_row_from == std::list::iterator()
// ... was used, but it didn't work with GCC.
bool destroy_row_from_used = false;
bool destroy_row_to_used = false;
// Iterate, and remove everything between 'from' and 'to_before'
// actually remove the entire lines they are on, it'll all have
// to be redone. And when going along, we'll delete a row at a time
// when continuing to see how much more after 'to' we need to remake.
int i = 0;
for (std::list::iterator it = m_CharacterPositions.begin();
it != m_CharacterPositions.end();
++it, ++i)
{
if (!destroy_row_from_used && it->m_ListStart > from)
{
// Destroy the previous line, and all to 'to_before'
destroy_row_from = it;
--destroy_row_from;
destroy_row_from_used = true;
// For the rare case that we might remove characters to a word
// so that it suddenly fits on the previous row,
// we need to by standards re-do the whole previous line too
// (if one exists)
if (destroy_row_from != m_CharacterPositions.begin())
--destroy_row_from;
}
if (!destroy_row_to_used && it->m_ListStart > to_before)
{
destroy_row_to = it;
destroy_row_to_used = true;
// If it isn't the last row, we'll add another row to delete,
// just so we can see if the last restorted line is
// identical to what it was before. If it isn't, then we'll
// have to continue.
// 'check_point_row_start' is where we store how the that
// line looked.
if (destroy_row_to != m_CharacterPositions.end())
{
check_point_row_start = destroy_row_to->m_ListStart;
check_point_row_end = check_point_row_start + (int)destroy_row_to->m_ListOfX.size();
if (destroy_row_to->m_ListOfX.empty())
++check_point_row_end;
}
++destroy_row_to;
break;
}
}
if (!destroy_row_from_used)
{
destroy_row_from = m_CharacterPositions.end();
--destroy_row_from;
// As usual, let's destroy another row back
if (destroy_row_from != m_CharacterPositions.begin())
--destroy_row_from;
current_line = destroy_row_from;
}
if (!destroy_row_to_used)
{
destroy_row_to = m_CharacterPositions.end();
check_point_row_start = -1;
}
// set 'from' to the row we'll destroy from
// and 'to' to the row we'll destroy to
from = destroy_row_from->m_ListStart;
if (destroy_row_to != m_CharacterPositions.end())
to = destroy_row_to->m_ListStart; // notice it will iterate [from, to), so it will never reach to.
else
to = static_cast(m_Caption.length());
// Setup the first row
row.m_ListStart = destroy_row_from->m_ListStart;
std::list::iterator temp_it = destroy_row_to;
--temp_it;
current_line = m_CharacterPositions.erase(destroy_row_from, destroy_row_to);
// If there has been a change in number of characters
// we need to change all m_ListStart that comes after
// the interval we just destroyed. We'll change all
// values with the delta change of the string length.
int delta = to_after - to_before;
if (delta != 0)
{
for (std::list::iterator it = current_line;
it != m_CharacterPositions.end();
++it)
it->m_ListStart += delta;
// Update our check point too!
check_point_row_start += delta;
check_point_row_end += delta;
if (to != static_cast(m_Caption.length()))
to += delta;
}
}
int last_word_started = from;
float x_pos = 0.f;
//if (to_before != -1)
// return;
for (int i = from; i < to; ++i)
{
if (m_Caption[i] == L'\n' && m_MultiLine)
{
if (i == to-1 && to != static_cast(m_Caption.length()))
break; // it will be added outside
current_line = m_CharacterPositions.insert(current_line, row);
++current_line;
// Setup the next row:
row.m_ListOfX.clear();
row.m_ListStart = i+1;
x_pos = 0.f;
}
else
{
if (m_Caption[i] == L' '/* || TODO Gee (2004-10-13): the '-' disappears, fix.
m_Caption[i] == L'-'*/)
last_word_started = i+1;
if (!m_Mask)
x_pos += font.GetCharacterWidth(m_Caption[i]);
else
x_pos += font.GetCharacterWidth(mask_char);
if (x_pos >= GetTextAreaWidth() && m_MultiLine)
{
// The following decides whether it will word-wrap a word,
// or if it's only one word on the line, where it has to
// break the word apart.
if (last_word_started == row.m_ListStart)
{
last_word_started = i;
row.m_ListOfX.resize(row.m_ListOfX.size() - (i-last_word_started));
//row.m_ListOfX.push_back(x_pos);
//continue;
}
else
{
// regular word-wrap
row.m_ListOfX.resize(row.m_ListOfX.size() - (i-last_word_started+1));
}
// Now, create a new line:
// notice: when we enter a newline, you can stand with the cursor
// both before and after that character, being on different
// rows. With automatic word-wrapping, that is not possible. Which
// is intuitively correct.
current_line = m_CharacterPositions.insert(current_line, row);
++current_line;
// Setup the next row:
row.m_ListOfX.clear();
row.m_ListStart = last_word_started;
i = last_word_started-1;
x_pos = 0.f;
}
else
// Get width of this character:
row.m_ListOfX.push_back(x_pos);
}
// Check if it's the last iteration, and we're not revising the whole string
// because in that case, more word-wrapping might be needed.
// also check if the current line isn't the end
if (to_before != -1 && i == to-1 && current_line != m_CharacterPositions.end())
{
// check all rows and see if any existing
if (row.m_ListStart != check_point_row_start)
{
std::list::iterator destroy_row_from;
std::list::iterator destroy_row_to;
// Are used to check if the above has been set to anything,
// previously a comparison like:
// destroy_row_from == std::list::iterator()
// was used, but it didn't work with GCC.
bool destroy_row_from_used = false;
bool destroy_row_to_used = false;
// Iterate, and remove everything between 'from' and 'to_before'
// actually remove the entire lines they are on, it'll all have
// to be redone. And when going along, we'll delete a row at a time
// when continuing to see how much more after 'to' we need to remake.
- int i = 0;
+
for (std::list::iterator it = m_CharacterPositions.begin();
it != m_CharacterPositions.end();
- ++it, ++i)
+ ++it)
{
if (!destroy_row_from_used && it->m_ListStart > check_point_row_start)
{
// Destroy the previous line, and all to 'to_before'
//if (i >= 2)
// destroy_row_from = it-2;
//else
// destroy_row_from = it-1;
destroy_row_from = it;
destroy_row_from_used = true;
//--destroy_row_from;
}
if (!destroy_row_to_used && it->m_ListStart > check_point_row_end)
{
destroy_row_to = it;
destroy_row_to_used = true;
// If it isn't the last row, we'll add another row to delete,
// just so we can see if the last restorted line is
// identical to what it was before. If it isn't, then we'll
// have to continue.
// 'check_point_row_start' is where we store how the that
// line looked.
if (destroy_row_to != m_CharacterPositions.end())
{
check_point_row_start = destroy_row_to->m_ListStart;
check_point_row_end = check_point_row_start + (int)destroy_row_to->m_ListOfX.size();
if (destroy_row_to->m_ListOfX.empty())
++check_point_row_end;
}
else
check_point_row_start = check_point_row_end = -1;
++destroy_row_to;
break;
}
}
if (!destroy_row_from_used)
{
destroy_row_from = m_CharacterPositions.end();
--destroy_row_from;
current_line = destroy_row_from;
}
if (!destroy_row_to_used)
{
destroy_row_to = m_CharacterPositions.end();
check_point_row_start = check_point_row_end = -1;
}
if (destroy_row_to != m_CharacterPositions.end())
to = destroy_row_to->m_ListStart; // notice it will iterate [from, to[, so it will never reach to.
else
to = static_cast(m_Caption.length());
// Set current line, new rows will be added before current_line, so
// we'll choose the destroy_row_to, because it won't be deleted
// in the coming erase.
current_line = destroy_row_to;
m_CharacterPositions.erase(destroy_row_from, destroy_row_to);
}
// else, the for loop will end naturally.
}
}
// This is kind of special, when we renew a some lines, then the last
// one will sometimes end with a space (' '), that really should
// be omitted when word-wrapping. So we'll check if the last row
// we'll add has got the same value as the next row.
if (current_line != m_CharacterPositions.end())
{
if (row.m_ListStart + (int)row.m_ListOfX.size() == current_line->m_ListStart)
row.m_ListOfX.resize(row.m_ListOfX.size()-1);
}
// add the final row (even if empty)
m_CharacterPositions.insert(current_line, row);
if (m_ScrollBar)
{
GetScrollBar(0).SetScrollRange(m_CharacterPositions.size() * font.GetLineSpacing() + m_BufferZone * 2.f);
GetScrollBar(0).SetScrollSpace(m_CachedActualSize.GetHeight());
}
}
int CInput::GetMouseHoveringTextPosition() const
{
if (m_CharacterPositions.empty())
return 0;
// Return position
int retPosition;
std::list::const_iterator current = m_CharacterPositions.begin();
CPos mouse = m_pGUI.GetMousePos();
if (m_MultiLine)
{
float scroll = 0.f;
if (m_ScrollBar)
scroll = GetScrollBarPos(0);
// Now get the height of the font.
// TODO: Get the real font
CFontMetrics font(CStrIntern(m_Font.ToUTF8()));
float spacing = (float)font.GetLineSpacing();
// Change mouse position relative to text.
mouse -= m_CachedActualSize.TopLeft();
mouse.x -= m_BufferZone;
mouse.y += scroll - m_BufferZone;
int row = (int)((mouse.y) / spacing);
if (row < 0)
row = 0;
if (row > (int)m_CharacterPositions.size()-1)
row = (int)m_CharacterPositions.size()-1;
// TODO Gee (2004-11-21): Okay, I need a 'std::list' for some reasons, but I would really like to
// be able to get the specific element here. This is hopefully a temporary hack.
for (int i = 0; i < row; ++i)
++current;
}
else
{
// current is already set to begin,
// but we'll change the mouse.x to fit our horizontal scrolling
mouse -= m_CachedActualSize.TopLeft();
mouse.x -= m_BufferZone - m_HorizontalScroll;
// mouse.y is moot
}
retPosition = current->m_ListStart;
// Okay, now loop through the glyphs to find the appropriate X position
float dummy;
retPosition += GetXTextPosition(current, mouse.x, dummy);
return retPosition;
}
// Does not process horizontal scrolling, 'x' must be modified before inputted.
int CInput::GetXTextPosition(const std::list::const_iterator& current, const float& x, float& wanted) const
{
int ret = 0;
float previous = 0.f;
int i = 0;
for (std::vector::const_iterator it = current->m_ListOfX.begin();
it != current->m_ListOfX.end();
++it, ++i)
{
if (*it >= x)
{
if (x - previous >= *it - x)
ret += i+1;
else
ret += i;
break;
}
previous = *it;
}
// If a position wasn't found, we will assume the last
// character of that line.
if (i == (int)current->m_ListOfX.size())
{
ret += i;
wanted = x;
}
else
wanted = 0.f;
return ret;
}
void CInput::DeleteCurSelection()
{
int virtualFrom;
int virtualTo;
if (m_iBufferPos_Tail >= m_iBufferPos)
{
virtualFrom = m_iBufferPos;
virtualTo = m_iBufferPos_Tail;
}
else
{
virtualFrom = m_iBufferPos_Tail;
virtualTo = m_iBufferPos;
}
m_Caption =
m_Caption.Left(virtualFrom) +
m_Caption.Right(static_cast(m_Caption.length()) - virtualTo);
UpdateText(virtualFrom, virtualTo, virtualFrom);
// Remove selection
m_iBufferPos_Tail = -1;
m_iBufferPos = virtualFrom;
UpdateBufferPositionSetting();
}
bool CInput::SelectingText() const
{
return m_iBufferPos_Tail != -1 &&
m_iBufferPos_Tail != m_iBufferPos;
}
float CInput::GetTextAreaWidth()
{
if (m_ScrollBar && GetScrollBar(0).GetStyle())
return m_CachedActualSize.GetWidth() - m_BufferZone * 2.f - GetScrollBar(0).GetStyle()->m_Width;
return m_CachedActualSize.GetWidth() - m_BufferZone * 2.f;
}
void CInput::UpdateAutoScroll()
{
// Autoscrolling up and down
if (m_MultiLine)
{
if (!m_ScrollBar)
return;
const float scroll = GetScrollBar(0).GetPos();
// Now get the height of the font.
// TODO: Get the real font
CFontMetrics font(CStrIntern(m_Font.ToUTF8()));
float spacing = (float)font.GetLineSpacing();
//float height = font.GetHeight();
// TODO Gee (2004-11-21): Okay, I need a 'std::list' for some reasons, but I would really like to
// be able to get the specific element here. This is hopefully a temporary hack.
std::list::iterator current = m_CharacterPositions.begin();
int row = 0;
while (current != m_CharacterPositions.end())
{
if (m_iBufferPos >= current->m_ListStart &&
m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size())
break;
++current;
++row;
}
// If scrolling down
if (-scroll + static_cast(row + 1) * spacing + m_BufferZone * 2.f > m_CachedActualSize.GetHeight())
{
// Scroll so the selected row is shown completely, also with m_BufferZone length to the edge.
GetScrollBar(0).SetPos(static_cast(row + 1) * spacing - m_CachedActualSize.GetHeight() + m_BufferZone * 2.f);
}
// If scrolling up
else if (-scroll + (float)row * spacing < 0.f)
{
// Scroll so the selected row is shown completely, also with m_BufferZone length to the edge.
GetScrollBar(0).SetPos((float)row * spacing);
}
}
else // autoscrolling left and right
{
// Get X position of position:
if (m_CharacterPositions.empty())
return;
float x_position = 0.f;
float x_total = 0.f;
if (!m_CharacterPositions.begin()->m_ListOfX.empty())
{
// Get position of m_iBufferPos
if ((int)m_CharacterPositions.begin()->m_ListOfX.size() >= m_iBufferPos &&
m_iBufferPos > 0)
x_position = m_CharacterPositions.begin()->m_ListOfX[m_iBufferPos-1];
// Get complete length:
x_total = m_CharacterPositions.begin()->m_ListOfX[m_CharacterPositions.begin()->m_ListOfX.size()-1];
}
// Check if outside to the right
if (x_position - m_HorizontalScroll + m_BufferZone * 2.f > m_CachedActualSize.GetWidth())
m_HorizontalScroll = x_position - m_CachedActualSize.GetWidth() + m_BufferZone * 2.f;
// Check if outside to the left
if (x_position - m_HorizontalScroll < 0.f)
m_HorizontalScroll = x_position;
// Check if the text doesn't even fill up to the right edge even though scrolling is done.
if (m_HorizontalScroll != 0.f &&
x_total - m_HorizontalScroll + m_BufferZone * 2.f < m_CachedActualSize.GetWidth())
m_HorizontalScroll = x_total - m_CachedActualSize.GetWidth() + m_BufferZone * 2.f;
// Now this is the fail-safe, if x_total isn't even the length of the control,
// remove all scrolling
if (x_total + m_BufferZone * 2.f < m_CachedActualSize.GetWidth())
m_HorizontalScroll = 0.f;
}
}
Index: ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp
===================================================================
--- ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 24267)
+++ ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 24268)
@@ -1,755 +1,755 @@
/* Copyright (C) 2020 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 "CMiniMap.h"
#include "graphics/GameView.h"
#include "graphics/LOSTexture.h"
#include "graphics/MiniPatch.h"
#include "graphics/Terrain.h"
#include "graphics/TerrainTextureEntry.h"
#include "graphics/TerrainTextureManager.h"
#include "graphics/TerritoryTexture.h"
#include "gui/CGUI.h"
#include "gui/GUIManager.h"
#include "gui/GUIMatrix.h"
#include "lib/bits.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/ogl.h"
#include "lib/timer.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/GameSetup/Config.h"
#include "ps/Profile.h"
#include "ps/World.h"
#include "ps/XML/Xeromyces.h"
#include "renderer/Renderer.h"
#include "renderer/RenderingOptions.h"
#include "renderer/WaterManager.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpMinimap.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/helpers/Los.h"
#include "simulation2/system/ParamNode.h"
#include
extern bool g_GameRestarted;
// Set max drawn entities to UINT16_MAX for now, which is more than enough
// TODO: we should be cleverer about drawing them to reduce clutter
const u16 MAX_ENTITIES_DRAWN = 65535;
static unsigned int ScaleColor(unsigned int color, float x)
{
unsigned int r = unsigned(float(color & 0xff) * x);
unsigned int g = unsigned(float((color>>8) & 0xff) * x);
unsigned int b = unsigned(float((color>>16) & 0xff) * x);
return (0xff000000 | b | g<<8 | r<<16);
}
const CStr CMiniMap::EventNameWorldClick = "WorldClick";
CMiniMap::CMiniMap(CGUI& pGUI) :
IGUIObject(pGUI),
m_TerrainTexture(0), m_TerrainData(0), m_MapSize(0), m_Terrain(0), m_TerrainDirty(true), m_MapScale(1.f),
m_EntitiesDrawn(0), m_IndexArray(GL_STATIC_DRAW), m_VertexArray(GL_DYNAMIC_DRAW), m_Mask(false),
m_NextBlinkTime(0.0), m_PingDuration(25.0), m_BlinkState(false), m_WaterHeight(0.0)
{
RegisterSetting("mask", m_Mask);
m_Clicking = false;
m_MouseHovering = false;
// Register Relax NG validator
CXeromyces::AddValidator(g_VFS, "pathfinder", "simulation/data/pathfinder.rng");
m_ShallowPassageHeight = GetShallowPassageHeight();
m_AttributePos.type = GL_FLOAT;
m_AttributePos.elems = 2;
m_VertexArray.AddAttribute(&m_AttributePos);
m_AttributeColor.type = GL_UNSIGNED_BYTE;
m_AttributeColor.elems = 4;
m_VertexArray.AddAttribute(&m_AttributeColor);
m_VertexArray.SetNumVertices(MAX_ENTITIES_DRAWN);
m_VertexArray.Layout();
m_IndexArray.SetNumVertices(MAX_ENTITIES_DRAWN);
m_IndexArray.Layout();
VertexArrayIterator index = m_IndexArray.GetIterator();
for (u16 i = 0; i < MAX_ENTITIES_DRAWN; ++i)
*index++ = i;
m_IndexArray.Upload();
m_IndexArray.FreeBackingStore();
VertexArrayIterator attrPos = m_AttributePos.GetIterator();
VertexArrayIterator attrColor = m_AttributeColor.GetIterator();
for (u16 i = 0; i < MAX_ENTITIES_DRAWN; ++i)
{
(*attrColor)[0] = 0;
(*attrColor)[1] = 0;
(*attrColor)[2] = 0;
(*attrColor)[3] = 0;
++attrColor;
(*attrPos)[0] = -10000.0f;
(*attrPos)[1] = -10000.0f;
++attrPos;
}
m_VertexArray.Upload();
double blinkDuration = 1.0;
// Tests won't have config initialised
if (CConfigDB::IsInitialised())
{
CFG_GET_VAL("gui.session.minimap.pingduration", m_PingDuration);
CFG_GET_VAL("gui.session.minimap.blinkduration", blinkDuration);
}
m_HalfBlinkDuration = blinkDuration/2;
}
CMiniMap::~CMiniMap()
{
Destroy();
}
void CMiniMap::HandleMessage(SGUIMessage& Message)
{
IGUIObject::HandleMessage(Message);
switch (Message.type)
{
case GUIM_MOUSE_PRESS_LEFT:
if (m_MouseHovering)
{
if (!CMiniMap::FireWorldClickEvent(SDL_BUTTON_LEFT, 1))
{
SetCameraPos();
m_Clicking = true;
}
}
break;
case GUIM_MOUSE_RELEASE_LEFT:
if (m_MouseHovering && m_Clicking)
SetCameraPos();
m_Clicking = false;
break;
case GUIM_MOUSE_DBLCLICK_LEFT:
if (m_MouseHovering && m_Clicking)
SetCameraPos();
m_Clicking = false;
break;
case GUIM_MOUSE_ENTER:
m_MouseHovering = true;
break;
case GUIM_MOUSE_LEAVE:
m_Clicking = false;
m_MouseHovering = false;
break;
case GUIM_MOUSE_RELEASE_RIGHT:
CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 1);
break;
case GUIM_MOUSE_DBLCLICK_RIGHT:
CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 2);
break;
case GUIM_MOUSE_MOTION:
if (m_MouseHovering && m_Clicking)
SetCameraPos();
break;
case GUIM_MOUSE_WHEEL_DOWN:
case GUIM_MOUSE_WHEEL_UP:
Message.Skip();
break;
default:
break;
}
}
bool CMiniMap::IsMouseOver() const
{
// Get the mouse position.
const CPos& mousePos = m_pGUI.GetMousePos();
// Get the position of the center of the minimap.
CPos minimapCenter = CPos(m_CachedActualSize.left + m_CachedActualSize.GetWidth() / 2.0, m_CachedActualSize.bottom - m_CachedActualSize.GetHeight() / 2.0);
// Take the magnitude of the difference of the mouse position and minimap center.
double distFromCenter = sqrt(pow((mousePos.x - minimapCenter.x), 2) + pow((mousePos.y - minimapCenter.y), 2));
// If the distance is less then the radius of the minimap (half the width) the mouse is over the minimap.
if (distFromCenter < m_CachedActualSize.GetWidth() / 2.0)
return true;
else
return false;
}
void CMiniMap::GetMouseWorldCoordinates(float& x, float& z) const
{
// Determine X and Z according to proportion of mouse position and minimap
const CPos& mousePos = m_pGUI.GetMousePos();
float px = (mousePos.x - m_CachedActualSize.left) / m_CachedActualSize.GetWidth();
float py = (m_CachedActualSize.bottom - mousePos.y) / m_CachedActualSize.GetHeight();
float angle = GetAngle();
// Scale world coordinates for shrunken square map
x = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(px-0.5) - sin(angle)*(py-0.5)) + 0.5);
z = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(py-0.5) + sin(angle)*(px-0.5)) + 0.5);
}
void CMiniMap::SetCameraPos()
{
CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
CVector3D target;
GetMouseWorldCoordinates(target.X, target.Z);
target.Y = terrain->GetExactGroundLevel(target.X, target.Z);
g_Game->GetView()->MoveCameraTarget(target);
}
float CMiniMap::GetAngle() const
{
CVector3D cameraIn = m_Camera->GetOrientation().GetIn();
return -atan2(cameraIn.X, cameraIn.Z);
}
bool CMiniMap::FireWorldClickEvent(int button, int UNUSED(clicks))
{
ScriptRequest rq(g_GUI->GetActiveGUI()->GetScriptInterface());
float x, z;
GetMouseWorldCoordinates(x, z);
JS::RootedValue coords(rq.cx);
ScriptInterface::CreateObject(rq, &coords, "x", x, "z", z);
JS::RootedValue buttonJs(rq.cx);
ScriptInterface::ToJSVal(rq, &buttonJs, button);
JS::AutoValueVector paramData(rq.cx);
DISCARD paramData.append(coords);
DISCARD paramData.append(buttonJs);
return ScriptEventWithReturn(EventNameWorldClick, paramData);
}
// This sets up and draws the rectangle on the minimap
// which represents the view of the camera in the world.
void CMiniMap::DrawViewRect(CMatrix3D transform) const
{
// Compute the camera frustum intersected with a fixed-height plane.
// Use the water height as a fixed base height, which should be the lowest we can go
float h = g_Renderer.GetWaterManager()->m_WaterHeight;
const float width = m_CachedActualSize.GetWidth();
const float height = m_CachedActualSize.GetHeight();
const float invTileMapSize = 1.0f / float(TERRAIN_TILE_SIZE * m_MapSize);
CVector3D hitPt[4];
hitPt[0] = m_Camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), h);
hitPt[1] = m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), g_Renderer.GetHeight(), h);
hitPt[2] = m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, h);
hitPt[3] = m_Camera->GetWorldCoordinates(0, 0, h);
float ViewRect[4][2];
for (int i = 0; i < 4; ++i)
{
// convert to minimap space
ViewRect[i][0] = (width * hitPt[i].X * invTileMapSize);
ViewRect[i][1] = (height * hitPt[i].Z * invTileMapSize);
}
float viewVerts[] = {
ViewRect[0][0], -ViewRect[0][1],
ViewRect[1][0], -ViewRect[1][1],
ViewRect[2][0], -ViewRect[2][1],
ViewRect[3][0], -ViewRect[3][1]
};
// Enable Scissoring to restrict the rectangle to only the minimap.
glScissor(
m_CachedActualSize.left * g_GuiScale,
g_Renderer.GetHeight() - m_CachedActualSize.bottom * g_GuiScale,
width * g_GuiScale,
height * g_GuiScale);
glEnable(GL_SCISSOR_TEST);
glLineWidth(2.0f);
CShaderDefines lineDefines;
lineDefines.Add(str_MINIMAP_LINE, str_1);
CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), lineDefines);
tech->BeginPass();
CShaderProgramPtr shader = tech->GetShader();
shader->Uniform(str_transform, transform);
shader->Uniform(str_color, 1.0f, 0.3f, 0.3f, 1.0f);
shader->VertexPointer(2, GL_FLOAT, 0, viewVerts);
shader->AssertPointersBound();
if (!g_Renderer.m_SkipSubmit)
glDrawArrays(GL_LINE_LOOP, 0, 4);
tech->EndPass();
glLineWidth(1.0f);
glDisable(GL_SCISSOR_TEST);
}
struct MinimapUnitVertex
{
// This struct is copyable for convenience and because to move is to copy for primitives.
u8 r, g, b, a;
float x, y;
};
// Adds a vertex to the passed VertexArray
static void inline addVertex(const MinimapUnitVertex& v,
VertexArrayIterator& attrColor,
VertexArrayIterator& attrPos)
{
(*attrColor)[0] = v.r;
(*attrColor)[1] = v.g;
(*attrColor)[2] = v.b;
(*attrColor)[3] = v.a;
++attrColor;
(*attrPos)[0] = v.x;
(*attrPos)[1] = v.y;
++attrPos;
}
void CMiniMap::DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float z) const
{
// Rotate the texture coordinates (0,0)-(coordMax,coordMax) around their center point (m,m)
// Scale square maps to fit in circular minimap area
const float s = sin(angle) * m_MapScale;
const float c = cos(angle) * m_MapScale;
const float m = coordMax / 2.f;
float quadTex[] = {
m*(-c + s + 1.f), m*(-c + -s + 1.f),
m*(c + s + 1.f), m*(-c + s + 1.f),
m*(c + -s + 1.f), m*(c + s + 1.f),
m*(c + -s + 1.f), m*(c + s + 1.f),
m*(-c + -s + 1.f), m*(c + -s + 1.f),
m*(-c + s + 1.f), m*(-c + -s + 1.f)
};
float quadVerts[] = {
x, y, z,
x2, y, z,
x2, y2, z,
x2, y2, z,
x, y2, z,
x, y, z
};
shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, quadTex);
shader->VertexPointer(3, GL_FLOAT, 0, quadVerts);
shader->AssertPointersBound();
if (!g_Renderer.m_SkipSubmit)
glDrawArrays(GL_TRIANGLES, 0, 6);
}
// TODO: render the minimap in a framebuffer and just draw the frambuffer texture
// most of the time, updating the framebuffer twice a frame.
// Here it updates as ping-pong either texture or vertex array each sec to lower gpu stalling
// (those operations cause a gpu sync, which slows down the way gpu works)
void CMiniMap::Draw()
{
PROFILE3("render minimap");
// The terrain isn't actually initialized until the map is loaded, which
// happens when the game is started, so abort until then.
if (!g_Game || !g_Game->IsGameStarted())
return;
CSimulation2* sim = g_Game->GetSimulation2();
CmpPtr cmpRangeManager(*sim, SYSTEM_ENTITY);
ENSURE(cmpRangeManager);
// Set our globals in case they hadn't been set before
m_Camera = g_Game->GetView()->GetCamera();
m_Terrain = g_Game->GetWorld()->GetTerrain();
m_Width = (u32)(m_CachedActualSize.right - m_CachedActualSize.left);
m_Height = (u32)(m_CachedActualSize.bottom - m_CachedActualSize.top);
m_MapSize = m_Terrain->GetVerticesPerSide();
m_TextureSize = (GLsizei)round_up_to_pow2((size_t)m_MapSize);
m_MapScale = (cmpRangeManager->GetLosCircular() ? 1.f : 1.414f);
if (!m_TerrainTexture || g_GameRestarted)
CreateTextures();
// only update 2x / second
// (note: since units only move a few pixels per second on the minimap,
// we can get away with infrequent updates; this is slow)
// TODO: Update all but camera at same speed as simulation
static double last_time;
const double cur_time = timer_Time();
const bool doUpdate = cur_time - last_time > 0.5;
if (doUpdate)
{
last_time = cur_time;
if (m_TerrainDirty || m_WaterHeight != g_Renderer.GetWaterManager()->m_WaterHeight)
RebuildTerrainTexture();
}
const float x = m_CachedActualSize.left, y = m_CachedActualSize.bottom;
const float x2 = m_CachedActualSize.right, y2 = m_CachedActualSize.top;
const float z = GetBufferedZ();
const float texCoordMax = (float)(m_MapSize - 1) / (float)m_TextureSize;
const float angle = GetAngle();
const float unitScale = (cmpRangeManager->GetLosCircular() ? 1.f : m_MapScale/2.f);
CLOSTexture& losTexture = g_Game->GetView()->GetLOSTexture();
// Disable depth updates to prevent apparent z-fighting-related issues
// with some drivers causing units to get drawn behind the texture.
glDepthMask(0);
CShaderProgramPtr shader;
CShaderTechniquePtr tech;
CShaderDefines baseDefines;
baseDefines.Add(str_MINIMAP_BASE, str_1);
if (m_Mask)
baseDefines.Add(str_MINIMAP_MASK, str_1);
tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), baseDefines);
tech->BeginPass();
shader = tech->GetShader();
// Draw the main textured quad
shader->BindTexture(str_baseTex, m_TerrainTexture);
if (m_Mask)
{
shader->BindTexture(str_maskTex, losTexture.GetTexture());
CMatrix3D maskTextureTransform = *losTexture.GetMinimapTextureMatrix();
// We need to have texture coordinates in the same coordinate space.
const float scale = 1.0f / texCoordMax;
maskTextureTransform.Scale(scale, scale, 1.0f);
shader->Uniform(str_maskTextureTransform, maskTextureTransform);
}
const CMatrix3D baseTransform = GetDefaultGuiMatrix();
CMatrix3D baseTextureTransform;
baseTextureTransform.SetIdentity();
shader->Uniform(str_transform, baseTransform);
shader->Uniform(str_textureTransform, baseTextureTransform);
if (m_Mask)
{
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
DrawTexture(shader, texCoordMax, angle, x, y, x2, y2, z);
if (!m_Mask)
{
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
// Draw territory boundaries
CTerritoryTexture& territoryTexture = g_Game->GetView()->GetTerritoryTexture();
shader->BindTexture(str_baseTex, territoryTexture.GetTexture());
if (m_Mask)
{
shader->BindTexture(str_maskTex, losTexture.GetTexture());
shader->Uniform(str_maskTextureTransform, *losTexture.GetMinimapTextureMatrix());
}
const CMatrix3D* territoryTransform = territoryTexture.GetMinimapTextureMatrix();
shader->Uniform(str_transform, baseTransform);
shader->Uniform(str_textureTransform, *territoryTransform);
DrawTexture(shader, 1.0f, angle, x, y, x2, y2, z);
tech->EndPass();
// Draw the LOS quad in black, using alpha values from the LOS texture
if (!m_Mask)
{
CShaderDefines losDefines;
losDefines.Add(str_MINIMAP_LOS, str_1);
tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), losDefines);
tech->BeginPass();
shader = tech->GetShader();
shader->BindTexture(str_baseTex, losTexture.GetTexture());
const CMatrix3D* losTransform = losTexture.GetMinimapTextureMatrix();
shader->Uniform(str_transform, baseTransform);
shader->Uniform(str_textureTransform, *losTransform);
DrawTexture(shader, 1.0f, angle, x, y, x2, y2, z);
tech->EndPass();
}
glDisable(GL_BLEND);
PROFILE_START("minimap units");
CShaderDefines pointDefines;
pointDefines.Add(str_MINIMAP_POINT, str_1);
tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), pointDefines);
tech->BeginPass();
shader = tech->GetShader();
shader->Uniform(str_transform, baseTransform);
shader->Uniform(str_pointSize, 3.f);
CMatrix3D unitMatrix;
unitMatrix.SetIdentity();
// Center the minimap on the origin of the axis of rotation.
unitMatrix.Translate(-(x2 - x) / 2.f, -(y2 - y) / 2.f, 0.f);
// Rotate the map.
unitMatrix.RotateZ(angle);
// Scale square maps to fit.
unitMatrix.Scale(unitScale, unitScale, 1.f);
// Move the minimap back to it's starting position.
unitMatrix.Translate((x2 - x) / 2.f, (y2 - y) / 2.f, 0.f);
// Move the minimap to it's final location.
unitMatrix.Translate(x, y, z);
// Apply the gui matrix.
unitMatrix *= GetDefaultGuiMatrix();
// Load the transform into the shader.
shader->Uniform(str_transform, unitMatrix);
const float sx = (float)m_Width / ((m_MapSize - 1) * TERRAIN_TILE_SIZE);
const float sy = (float)m_Height / ((m_MapSize - 1) * TERRAIN_TILE_SIZE);
CSimulation2::InterfaceList ents = sim->GetEntitiesWithInterface(IID_Minimap);
if (doUpdate)
{
VertexArrayIterator attrPos = m_AttributePos.GetIterator();
VertexArrayIterator attrColor = m_AttributeColor.GetIterator();
m_EntitiesDrawn = 0;
MinimapUnitVertex v;
std::vector pingingVertices;
pingingVertices.reserve(MAX_ENTITIES_DRAWN / 2);
if (cur_time > m_NextBlinkTime)
{
m_BlinkState = !m_BlinkState;
m_NextBlinkTime = cur_time + m_HalfBlinkDuration;
}
entity_pos_t posX, posZ;
for (CSimulation2::InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it)
{
ICmpMinimap* cmpMinimap = static_cast(it->second);
if (cmpMinimap->GetRenderData(v.r, v.g, v.b, posX, posZ))
{
LosVisibility vis = cmpRangeManager->GetLosVisibility(it->first, g_Game->GetSimulation2()->GetSimContext().GetCurrentDisplayedPlayer());
if (vis != LosVisibility::HIDDEN)
{
v.a = 255;
v.x = posX.ToFloat() * sx;
v.y = -posZ.ToFloat() * sy;
// Check minimap pinging to indicate something
if (m_BlinkState && cmpMinimap->CheckPing(cur_time, m_PingDuration))
{
v.r = 255; // ping color is white
v.g = 255;
v.b = 255;
pingingVertices.push_back(v);
}
else
{
addVertex(v, attrColor, attrPos);
++m_EntitiesDrawn;
}
}
}
}
// Add the pinged vertices at the end, so they are drawn on top
- for (size_t v = 0; v < pingingVertices.size(); ++v)
+ for (const MinimapUnitVertex& vertex : pingingVertices)
{
- addVertex(pingingVertices[v], attrColor, attrPos);
+ addVertex(vertex, attrColor, attrPos);
++m_EntitiesDrawn;
}
ENSURE(m_EntitiesDrawn < MAX_ENTITIES_DRAWN);
m_VertexArray.Upload();
}
m_VertexArray.PrepareForRendering();
if (m_EntitiesDrawn > 0)
{
#if !CONFIG2_GLES
if (g_RenderingOptions.GetRenderPath() == RenderPath::SHADER)
glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);
#endif
u8* indexBase = m_IndexArray.Bind();
u8* base = m_VertexArray.Bind();
const GLsizei stride = (GLsizei)m_VertexArray.GetStride();
shader->VertexPointer(2, GL_FLOAT, stride, base + m_AttributePos.offset);
shader->ColorPointer(4, GL_UNSIGNED_BYTE, stride, base + m_AttributeColor.offset);
shader->AssertPointersBound();
if (!g_Renderer.m_SkipSubmit)
glDrawElements(GL_POINTS, (GLsizei)(m_EntitiesDrawn), GL_UNSIGNED_SHORT, indexBase);
g_Renderer.GetStats().m_DrawCalls++;
CVertexBuffer::Unbind();
#if !CONFIG2_GLES
if (g_RenderingOptions.GetRenderPath() == RenderPath::SHADER)
glDisable(GL_VERTEX_PROGRAM_POINT_SIZE);
#endif
}
tech->EndPass();
DrawViewRect(unitMatrix);
PROFILE_END("minimap units");
// Reset depth mask
glDepthMask(1);
}
void CMiniMap::CreateTextures()
{
Destroy();
// Create terrain texture
glGenTextures(1, &m_TerrainTexture);
g_Renderer.BindTexture(0, m_TerrainTexture);
// Initialise texture with solid black, for the areas we don't
// overwrite with glTexSubImage2D later
u32* texData = new u32[m_TextureSize * m_TextureSize];
for (ssize_t i = 0; i < m_TextureSize * m_TextureSize; ++i)
texData[i] = 0xFF000000;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_TextureSize, m_TextureSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, texData);
delete[] texData;
m_TerrainData = new u32[(m_MapSize - 1) * (m_MapSize - 1)];
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// Rebuild and upload both of them
RebuildTerrainTexture();
}
void CMiniMap::RebuildTerrainTexture()
{
u32 x = 0;
u32 y = 0;
u32 w = m_MapSize - 1;
u32 h = m_MapSize - 1;
m_WaterHeight = g_Renderer.GetWaterManager()->m_WaterHeight;
m_TerrainDirty = false;
for (u32 j = 0; j < h; ++j)
{
u32* dataPtr = m_TerrainData + ((y + j) * (m_MapSize - 1)) + x;
for (u32 i = 0; i < w; ++i)
{
float avgHeight = ( m_Terrain->GetVertexGroundLevel((int)i, (int)j)
+ m_Terrain->GetVertexGroundLevel((int)i+1, (int)j)
+ m_Terrain->GetVertexGroundLevel((int)i, (int)j+1)
+ m_Terrain->GetVertexGroundLevel((int)i+1, (int)j+1)
) / 4.0f;
if (avgHeight < m_WaterHeight && avgHeight > m_WaterHeight - m_ShallowPassageHeight)
{
// shallow water
*dataPtr++ = 0xffc09870;
}
else if (avgHeight < m_WaterHeight)
{
// Set water as constant color for consistency on different maps
*dataPtr++ = 0xffa07850;
}
else
{
int hmap = ((int)m_Terrain->GetHeightMap()[(y + j) * m_MapSize + x + i]) >> 8;
int val = (hmap / 3) + 170;
u32 color = 0xFFFFFFFF;
CMiniPatch* mp = m_Terrain->GetTile(x + i, y + j);
if (mp)
{
CTerrainTextureEntry* tex = mp->GetTextureEntry();
if (tex)
{
// If the texture can't be loaded yet, set the dirty flags
// so we'll try regenerating the terrain texture again soon
if(!tex->GetTexture()->TryLoad())
m_TerrainDirty = true;
color = tex->GetBaseColor();
}
}
*dataPtr++ = ScaleColor(color, float(val) / 255.0f);
}
}
}
// Upload the texture
g_Renderer.BindTexture(0, m_TerrainTexture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_MapSize - 1, m_MapSize - 1, GL_RGBA, GL_UNSIGNED_BYTE, m_TerrainData);
}
void CMiniMap::Destroy()
{
if (m_TerrainTexture)
{
glDeleteTextures(1, &m_TerrainTexture);
m_TerrainTexture = 0;
}
SAFE_ARRAY_DELETE(m_TerrainData);
}
// static
float CMiniMap::GetShallowPassageHeight()
{
float shallowPassageHeight = 0.0f;
CParamNode externalParamNode;
CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder");
const CParamNode pathingSettings = externalParamNode.GetChild("Pathfinder").GetChild("PassabilityClasses");
if (pathingSettings.GetChild("default").IsOk() && pathingSettings.GetChild("default").GetChild("MaxWaterDepth").IsOk())
shallowPassageHeight = pathingSettings.GetChild("default").GetChild("MaxWaterDepth").ToFloat();
return shallowPassageHeight;
}
Index: ps/trunk/source/gui/ObjectTypes/COList.cpp
===================================================================
--- ps/trunk/source/gui/ObjectTypes/COList.cpp (revision 24267)
+++ ps/trunk/source/gui/ObjectTypes/COList.cpp (revision 24268)
@@ -1,444 +1,440 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* Copyright (C) 2020 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 "COList.h"
#include "gui/CGUI.h"
#include "gui/IGUIScrollBar.h"
#include "gui/SettingTypes/CGUIColor.h"
#include "gui/SettingTypes/CGUIList.h"
#include "i18n/L10n.h"
#include "ps/CLogger.h"
const float SORT_SPRITE_DIM = 16.0f;
const CPos COLUMN_SHIFT = CPos(0, 4);
const CStr COList::EventNameSelectionColumnChange = "SelectionColumnChange";
COList::COList(CGUI& pGUI)
: CList(pGUI),
m_SpriteHeading(),
m_Sortable(),
m_SelectedColumn(),
m_SelectedColumnOrder(),
m_SpriteAsc(),
m_SpriteDesc(),
m_SpriteNotSorted()
{
RegisterSetting("sprite_heading", m_SpriteHeading);
RegisterSetting("sortable", m_Sortable); // The actual sorting is done in JS for more versatility
RegisterSetting("selected_column", m_SelectedColumn);
RegisterSetting("selected_column_order", m_SelectedColumnOrder);
RegisterSetting("sprite_asc", m_SpriteAsc); // Show the order of sorting
RegisterSetting("sprite_desc", m_SpriteDesc);
RegisterSetting("sprite_not_sorted", m_SpriteNotSorted);
}
void COList::SetupText()
{
m_ItemsYPositions.resize(m_List.m_Items.size() + 1);
// Delete all generated texts. Some could probably be saved,
// but this is easier, and this function will never be called
// continuously, or even often, so it'll probably be okay.
m_GeneratedTexts.clear();
m_TotalAvailableColumnWidth = GetListRect().GetWidth();
// remove scrollbar if applicable
if (m_ScrollBar && GetScrollBar(0).GetStyle())
m_TotalAvailableColumnWidth -= GetScrollBar(0).GetStyle()->m_Width;
m_HeadingHeight = SORT_SPRITE_DIM; // At least the size of the sorting sprite
for (const COListColumn& column : m_Columns)
{
float width = column.m_Width;
if (column.m_Width > 0 && column.m_Width < 1)
width *= m_TotalAvailableColumnWidth;
CGUIString gui_string;
gui_string.SetValue(column.m_Heading);
const CGUIText& text = AddText(gui_string, m_Font, width, m_BufferZone);
m_HeadingHeight = std::max(m_HeadingHeight, text.GetSize().cy + COLUMN_SHIFT.y);
}
// Generate texts
float buffered_y = 0.f;
for (size_t i = 0; i < m_List.m_Items.size(); ++i)
{
m_ItemsYPositions[i] = buffered_y;
float shift = 0.0f;
for (const COListColumn& column : m_Columns)
{
float width = column.m_Width;
if (column.m_Width > 0 && column.m_Width < 1)
width *= m_TotalAvailableColumnWidth;
CGUIText* text;
if (!column.m_List.m_Items[i].GetOriginalString().empty())
text = &AddText(column.m_List.m_Items[i], m_Font, width, m_BufferZone);
else
{
// Minimum height of a space character of the current font size
CGUIString align_string;
align_string.SetValue(L" ");
text = &AddText(align_string, m_Font, width, m_BufferZone);
}
shift = std::max(shift, text->GetSize().cy);
}
buffered_y += shift;
}
m_ItemsYPositions[m_List.m_Items.size()] = buffered_y;
if (m_ScrollBar)
{
CRect rect = GetListRect();
GetScrollBar(0).SetScrollRange(m_ItemsYPositions.back());
GetScrollBar(0).SetScrollSpace(rect.GetHeight());
GetScrollBar(0).SetX(rect.right);
GetScrollBar(0).SetY(rect.top);
GetScrollBar(0).SetZ(GetBufferedZ());
GetScrollBar(0).SetLength(rect.bottom - rect.top);
}
}
CRect COList::GetListRect() const
{
return m_CachedActualSize + CRect(0, m_HeadingHeight, 0, 0);
}
void COList::HandleMessage(SGUIMessage& Message)
{
CList::HandleMessage(Message);
switch (Message.type)
{
// If somebody clicks on the column heading
case GUIM_MOUSE_PRESS_LEFT:
{
if (!m_Sortable)
return;
const CPos& mouse = m_pGUI.GetMousePos();
if (!m_CachedActualSize.PointInside(mouse))
return;
float xpos = 0;
for (const COListColumn& column : m_Columns)
{
if (column.m_Hidden)
continue;
float width = column.m_Width;
// Check if it's a decimal value, and if so, assume relative positioning.
if (column.m_Width < 1 && column.m_Width > 0)
width *= m_TotalAvailableColumnWidth;
CPos leftTopCorner = m_CachedActualSize.TopLeft() + CPos(xpos, 0);
if (mouse.x >= leftTopCorner.x &&
mouse.x < leftTopCorner.x + width &&
mouse.y < leftTopCorner.y + m_HeadingHeight)
{
if (column.m_Id != m_SelectedColumn)
{
SetSetting("selected_column_order", -1, true);
CStr selected_column = column.m_Id;
SetSetting("selected_column", selected_column, true);
}
else
SetSetting("selected_column_order", -m_SelectedColumnOrder, true);
ScriptEvent(EventNameSelectionColumnChange);
PlaySound(m_SoundSelected);
return;
}
xpos += width;
}
return;
}
default:
return;
}
}
bool COList::HandleAdditionalChildren(const XMBElement& child, CXeromyces* pFile)
{
#define ELMT(x) int elmt_##x = pFile->GetElementID(#x)
#define ATTR(x) int attr_##x = pFile->GetAttributeID(#x)
ELMT(item);
ELMT(column);
ELMT(translatableAttribute);
ATTR(id);
ATTR(context);
if (child.GetNodeName() == elmt_item)
{
AddItem(child.GetText().FromUTF8(), child.GetText().FromUTF8());
return true;
}
else if (child.GetNodeName() == elmt_column)
{
COListColumn column;
for (XMBAttribute attr : child.GetAttributes())
{
CStr attr_name(pFile->GetAttributeString(attr.Name));
CStr attr_value(attr.Value);
if (attr_name == "color")
{
if (!CGUI::ParseString(&m_pGUI, attr_value.FromUTF8(), column.m_TextColor))
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str());
}
else if (attr_name == "id")
{
column.m_Id = attr_value;
}
else if (attr_name == "hidden")
{
bool hidden = false;
if (!CGUI::ParseString(&m_pGUI, attr_value.FromUTF8(), hidden))
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str());
else
column.m_Hidden = hidden;
}
else if (attr_name == "width")
{
float width;
if (!CGUI::ParseString(&m_pGUI, attr_value.FromUTF8(), width))
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str());
else
{
// Check if it's a relative value, and save as decimal if so.
if (attr_value.find("%") != std::string::npos)
width = width / 100.f;
column.m_Width = width;
}
}
else if (attr_name == "heading")
{
column.m_Heading = attr_value.FromUTF8();
}
}
for (XMBElement grandchild : child.GetChildNodes())
{
if (grandchild.GetNodeName() != elmt_translatableAttribute)
continue;
CStr attributeName(grandchild.GetAttributes().GetNamedItem(attr_id));
// only the heading is translatable for list column
if (attributeName.empty() || attributeName != "heading")
{
LOGERROR("GUI: translatable attribute in olist column that isn't a heading. (object: %s)", this->GetPresentableName().c_str());
continue;
}
CStr value(grandchild.GetText());
if (value.empty())
continue;
CStr context(grandchild.GetAttributes().GetNamedItem(attr_context)); // Read the context if any.
if (!context.empty())
{
CStr translatedValue(g_L10n.TranslateWithContext(context, value));
column.m_Heading = translatedValue.FromUTF8();
}
else
{
CStr translatedValue(g_L10n.Translate(value));
column.m_Heading = translatedValue.FromUTF8();
}
}
m_Columns.emplace_back(std::move(column));
return true;
}
return false;
}
void COList::AdditionalChildrenHandled()
{
SetupText();
// Do this after the last push_back call to avoid iterator invalidation
for (COListColumn& column : m_Columns)
{
RegisterSetting("list_" + column.m_Id, column.m_List);
RegisterSetting("hidden_" + column.m_Id, column.m_Hidden);
}
}
void COList::DrawList(const int& selected, const CGUISpriteInstance& sprite, const CGUISpriteInstance& sprite_selected, const CGUIColor& textcolor)
{
const float bz = GetBufferedZ();
if (m_ScrollBar)
IGUIScrollBarOwner::Draw();
CRect rect = GetListRect();
m_pGUI.DrawSprite(sprite, m_CellID, bz, rect);
float scroll = 0.f;
if (m_ScrollBar)
scroll = GetScrollBar(0).GetPos();
// Draw item selection
if (selected != -1)
{
ENSURE(selected >= 0 && selected+1 < (int)m_ItemsYPositions.size());
// Get rectangle of selection:
CRect rect_sel(rect.left, rect.top + m_ItemsYPositions[selected] - scroll,
rect.right, rect.top + m_ItemsYPositions[selected+1] - scroll);
if (rect_sel.top <= rect.bottom &&
rect_sel.bottom >= rect.top)
{
if (rect_sel.bottom > rect.bottom)
rect_sel.bottom = rect.bottom;
if (rect_sel.top < rect.top)
rect_sel.top = rect.top;
if (m_ScrollBar)
{
// Remove any overlapping area of the scrollbar.
if (rect_sel.right > GetScrollBar(0).GetOuterRect().left &&
rect_sel.right <= GetScrollBar(0).GetOuterRect().right)
rect_sel.right = GetScrollBar(0).GetOuterRect().left;
if (rect_sel.left >= GetScrollBar(0).GetOuterRect().left &&
rect_sel.left < GetScrollBar(0).GetOuterRect().right)
rect_sel.left = GetScrollBar(0).GetOuterRect().right;
}
// Draw item selection
m_pGUI.DrawSprite(sprite_selected, m_CellID, bz + 0.05f, rect_sel);
}
}
// Draw line above column header
CRect rect_head(m_CachedActualSize.left, m_CachedActualSize.top, m_CachedActualSize.right,
m_CachedActualSize.top + m_HeadingHeight);
m_pGUI.DrawSprite(m_SpriteHeading, m_CellID, bz, rect_head);
// Draw column headers
float xpos = 0;
size_t col = 0;
for (const COListColumn& column : m_Columns)
{
if (column.m_Hidden)
{
++col;
continue;
}
// Check if it's a decimal value, and if so, assume relative positioning.
float width = column.m_Width;
if (column.m_Width < 1 && column.m_Width > 0)
width *= m_TotalAvailableColumnWidth;
CPos leftTopCorner = m_CachedActualSize.TopLeft() + CPos(xpos, 0);
// Draw sort arrows in colum header
if (m_Sortable)
{
- const CGUISpriteInstance* sprite;
+ const CGUISpriteInstance* pSprite;
if (m_SelectedColumn == column.m_Id)
{
if (m_SelectedColumnOrder == 0)
LOGERROR("selected_column_order must not be 0");
if (m_SelectedColumnOrder != -1)
- sprite = &m_SpriteAsc;
+ pSprite = &m_SpriteAsc;
else
- sprite = &m_SpriteDesc;
+ pSprite = &m_SpriteDesc;
}
else
- sprite = &m_SpriteNotSorted;
+ pSprite = &m_SpriteNotSorted;
- m_pGUI.DrawSprite(*sprite, m_CellID, bz + 0.1f, CRect(leftTopCorner + CPos(width - SORT_SPRITE_DIM, 0), leftTopCorner + CPos(width, SORT_SPRITE_DIM)));
+ m_pGUI.DrawSprite(*pSprite, m_CellID, bz + 0.1f, CRect(leftTopCorner + CPos(width - SORT_SPRITE_DIM, 0), leftTopCorner + CPos(width, SORT_SPRITE_DIM)));
}
// Draw column header text
DrawText(col, textcolor, leftTopCorner + COLUMN_SHIFT, bz + 0.1f, rect_head);
xpos += width;
++col;
}
// Draw list items for each column
const size_t objectsCount = m_Columns.size();
for (size_t i = 0; i < m_List.m_Items.size(); ++i)
{
if (m_ItemsYPositions[i+1] - scroll < 0 ||
m_ItemsYPositions[i] - scroll > rect.GetHeight())
continue;
const float rowHeight = m_ItemsYPositions[i+1] - m_ItemsYPositions[i];
// Clipping area (we'll have to substract the scrollbar)
CRect cliparea = GetListRect();
if (m_ScrollBar)
{
if (cliparea.right > GetScrollBar(0).GetOuterRect().left &&
cliparea.right <= GetScrollBar(0).GetOuterRect().right)
cliparea.right = GetScrollBar(0).GetOuterRect().left;
if (cliparea.left >= GetScrollBar(0).GetOuterRect().left &&
cliparea.left < GetScrollBar(0).GetOuterRect().right)
cliparea.left = GetScrollBar(0).GetOuterRect().right;
}
// Draw all items for that column
xpos = 0;
- size_t col = 0;
- for (const COListColumn& column : m_Columns)
+ for (size_t colIdx = 0; colIdx < m_Columns.size(); ++colIdx)
{
+ const COListColumn& column = m_Columns[i];
if (column.m_Hidden)
- {
- ++col;
continue;
- }
// Determine text position and width
const CPos textPos = rect.TopLeft() + CPos(xpos, -scroll + m_ItemsYPositions[i]);
float width = column.m_Width;
// Check if it's a decimal value, and if so, assume relative positioning.
if (column.m_Width < 1 && column.m_Width > 0)
width *= m_TotalAvailableColumnWidth;
// Clip text to the column (to prevent drawing text into the neighboring column)
CRect cliparea2 = cliparea;
cliparea2.right = std::min(cliparea2.right, textPos.x + width);
cliparea2.bottom = std::min(cliparea2.bottom, textPos.y + rowHeight);
// Draw list item
- DrawText(objectsCount * (i +/*Heading*/1) + col, column.m_TextColor, textPos, bz + 0.1f, cliparea2);
+ DrawText(objectsCount * (i +/*Heading*/1) + colIdx, column.m_TextColor, textPos, bz + 0.1f, cliparea2);
xpos += width;
- ++col;
}
}
}
Index: ps/trunk/source/network/NetServer.cpp
===================================================================
--- ps/trunk/source/network/NetServer.cpp (revision 24267)
+++ ps/trunk/source/network/NetServer.cpp (revision 24268)
@@ -1,1636 +1,1634 @@
/* Copyright (C) 2020 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 "NetServer.h"
#include "NetClient.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "NetServerTurnManager.h"
#include "NetStats.h"
#include "lib/external_libraries/enet.h"
#include "lib/types.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/GUID.h"
#include "ps/Profile.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
#if CONFIG2_MINIUPNPC
#include
#include
#include
#include
#endif
#include
/**
* Number of peers to allocate for the enet host.
* Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
*
* At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
*/
#define MAX_CLIENTS 41
#define DEFAULT_SERVER_NAME L"Unnamed Server"
static const int CHANNEL_COUNT = 1;
/**
* enet_host_service timeout (msecs).
* Smaller numbers may hurt performance; larger numbers will
* hurt latency responding to messages from game thread.
*/
static const int HOST_SERVICE_TIMEOUT = 50;
CNetServer* g_NetServer = NULL;
static CStr DebugName(CNetServerSession* session)
{
if (session == NULL)
return "[unknown host]";
if (session->GetGUID().empty())
return "[unauthed host]";
return "[" + session->GetGUID().substr(0, 8) + "...]";
}
/**
* Async task for receiving the initial game state to be forwarded to another
* client that is rejoining an in-progress network game.
*/
class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
public:
CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
: m_Server(server), m_RejoinerHostID(hostID)
{
}
virtual void OnComplete()
{
// We've received the game state from an existing player - now
// we need to send it onwards to the newly rejoining player
// Find the session corresponding to the rejoining host (if any)
CNetServerSession* session = NULL;
for (CNetServerSession* serverSession : m_Server.m_Sessions)
{
if (serverSession->GetHostID() == m_RejoinerHostID)
{
session = serverSession;
break;
}
}
if (!session)
{
LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
return;
}
// Store the received state file, and tell the client to start downloading it from us
// TODO: this will get kind of confused if there's multiple clients downloading in parallel;
// they'll race and get whichever happens to be the latest received by the server,
// which should still work but isn't great
m_Server.m_JoinSyncFile = m_Buffer;
CJoinSyncStartMessage message;
session->SendMessage(&message);
}
private:
CNetServerWorker& m_Server;
u32 m_RejoinerHostID;
};
/*
* XXX: We use some non-threadsafe functions from the worker thread.
* See http://trac.wildfiregames.com/ticket/654
*/
CNetServerWorker::CNetServerWorker(bool useLobbyAuth, int autostartPlayers) :
m_AutostartPlayers(autostartPlayers),
m_LobbyAuth(useLobbyAuth),
m_Shutdown(false),
m_ScriptInterface(NULL),
m_NextHostID(1), m_Host(NULL), m_HostGUID(), m_Stats(NULL),
m_LastConnectionCheck(0)
{
m_State = SERVER_STATE_UNCONNECTED;
m_ServerTurnManager = NULL;
m_ServerName = DEFAULT_SERVER_NAME;
}
CNetServerWorker::~CNetServerWorker()
{
if (m_State != SERVER_STATE_UNCONNECTED)
{
// Tell the thread to shut down
{
std::lock_guard lock(m_WorkerMutex);
m_Shutdown = true;
}
// Wait for it to shut down cleanly
m_WorkerThread.join();
}
#if CONFIG2_MINIUPNPC
if (m_UPnPThread.joinable())
m_UPnPThread.detach();
#endif
// Clean up resources
delete m_Stats;
for (CNetServerSession* session : m_Sessions)
{
session->DisconnectNow(NDR_SERVER_SHUTDOWN);
delete session;
}
if (m_Host)
enet_host_destroy(m_Host);
delete m_ServerTurnManager;
}
bool CNetServerWorker::SetupConnection(const u16 port)
{
ENSURE(m_State == SERVER_STATE_UNCONNECTED);
ENSURE(!m_Host);
// Bind to default host
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = port;
// Create ENet server
m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0);
if (!m_Host)
{
LOGERROR("Net server: enet_host_create failed");
return false;
}
m_Stats = new CNetStatsTable();
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats);
m_State = SERVER_STATE_PREGAME;
// Launch the worker thread
m_WorkerThread = std::thread(RunThread, this);
#if CONFIG2_MINIUPNPC
// Launch the UPnP thread
m_UPnPThread = std::thread(SetupUPnP);
#endif
return true;
}
#if CONFIG2_MINIUPNPC
void CNetServerWorker::SetupUPnP()
{
debug_SetThreadName("UPnP");
// Values we want to set.
char psPort[6];
sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT);
const char* leaseDuration = "0"; // Indefinite/permanent lease duration.
const char* description = "0AD Multiplayer";
const char* protocall = "UDP";
char internalIPAddress[64];
char externalIPAddress[40];
// Variables to hold the values that actually get set.
char intClient[40];
char intPort[6];
char duration[16];
// Intermediate variables.
bool allocatedUrls = false;
struct UPNPUrls urls;
struct IGDdatas data;
struct UPNPDev* devlist = NULL;
// Make sure everything is properly freed.
std::function freeUPnP = [&allocatedUrls, &urls, &devlist]()
{
if (allocatedUrls)
FreeUPNPUrls(&urls);
freeUPNPDevlist(devlist);
// IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl
};
// Cached root descriptor URL.
std::string rootDescURL;
CFG_GET_VAL("network.upnprootdescurl", rootDescURL);
if (!rootDescURL.empty())
LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str());
int ret = 0;
// Try a cached URL first
if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
{
LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL);
ret = 1;
}
// No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL)
#else
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL)
#endif
{
ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress));
allocatedUrls = ret != 0; // urls is allocated on non-zero return values
}
else
{
LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
freeUPnP();
return;
}
switch (ret)
{
case 0:
LOGMESSAGE("Net server: No IGD found");
break;
case 1:
LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL);
break;
case 2:
LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL);
break;
case 3:
LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL);
break;
default:
debug_warn(L"Unrecognized return value from UPNP_GetValidIGD");
}
// Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret));
freeUPnP();
return;
}
LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress);
// Try to setup port forwarding.
ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort,
internalIPAddress, description, protocall, 0, leaseDuration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
psPort, psPort, internalIPAddress, ret, strupnperror(ret));
freeUPnP();
return;
}
// Check that the port was actually forwarded.
ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
data.first.servicetype,
psPort, protocall,
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
NULL/*remoteHost*/,
#endif
intClient, intPort, NULL/*desc*/,
NULL/*enabled*/, duration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret));
freeUPnP();
return;
}
LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
externalIPAddress, psPort, protocall, intClient, intPort, duration);
// Cache root descriptor URL to try to avoid discovery next time.
g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL);
g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL);
LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL);
freeUPnP();
}
#endif // CONFIG2_MINIUPNPC
bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
{
ENSURE(m_Host);
CNetServerSession* session = static_cast(peer->data);
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
}
bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector& targetStates)
{
ENSURE(m_Host);
bool ok = true;
// TODO: this does lots of repeated message serialisation if we have lots
// of remote peers; could do it more efficiently if that's a real problem
for (CNetServerSession* session : m_Sessions)
- if (std::find(targetStates.begin(), targetStates.end(), session->GetCurrState()) != targetStates.end() &&
+ if (std::find(targetStates.begin(), targetStates.end(), static_cast(session->GetCurrState())) != targetStates.end() &&
!session->SendMessage(message))
ok = false;
return ok;
}
void CNetServerWorker::RunThread(CNetServerWorker* data)
{
debug_SetThreadName("NetServer");
data->Run();
}
void CNetServerWorker::Run()
{
// The script context uses the profiler and therefore the thread must be registered before the context is created
g_Profiler2.RegisterCurrentThread("Net server");
// We create a new ScriptContext for this network thread, with a single ScriptInterface.
shared_ptr netServerContext = ScriptContext::CreateContext();
m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext);
m_GameAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue());
while (true)
{
if (!RunStep())
break;
// Implement autostart mode
if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
StartGame();
// Update profiler stats
m_Stats->LatchHostState(m_Host);
}
// Clear roots before deleting their context
m_SavedCommands.clear();
SAFE_DELETE(m_ScriptInterface);
}
bool CNetServerWorker::RunStep()
{
// Check for messages from the game thread.
// (Do as little work as possible while the mutex is held open,
// to avoid performance problems and deadlocks.)
m_ScriptInterface->GetContext()->MaybeIncrementalGC(0.5f);
ScriptRequest rq(m_ScriptInterface);
std::vector newStartGame;
std::vector newGameAttributes;
std::vector> newLobbyAuths;
std::vector newTurnLength;
{
std::lock_guard lock(m_WorkerMutex);
if (m_Shutdown)
return false;
newStartGame.swap(m_StartGameQueue);
newGameAttributes.swap(m_GameAttributesQueue);
newLobbyAuths.swap(m_LobbyAuthQueue);
newTurnLength.swap(m_TurnLengthQueue);
}
if (!newGameAttributes.empty())
{
JS::RootedValue gameAttributesVal(rq.cx);
GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal);
UpdateGameAttributes(&gameAttributesVal);
}
if (!newTurnLength.empty())
SetTurnLength(newTurnLength.back());
// Do StartGame last, so we have the most up-to-date game attributes when we start
if (!newStartGame.empty())
StartGame();
while (!newLobbyAuths.empty())
{
const std::pair& auth = newLobbyAuths.back();
ProcessLobbyAuth(auth.first, auth.second);
newLobbyAuths.pop_back();
}
// Perform file transfers
for (CNetServerSession* session : m_Sessions)
session->GetFileTransferer().Poll();
CheckClientConnections();
// Process network events:
ENetEvent event;
int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
if (status < 0)
{
LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
// TODO: notify game that the server has shut down
return false;
}
if (status == 0)
{
// Reached timeout with no events - try again
return true;
}
// Process the event:
switch (event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
// Report the client address
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
// Set up a session object for this peer
CNetServerSession* session = new CNetServerSession(*this, event.peer);
m_Sessions.push_back(session);
SetupSession(session);
ENSURE(event.peer->data == NULL);
event.peer->data = session;
HandleConnect(session);
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
// If there is an active session with this peer, then reset and delete it
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
// Remove the session first, so we won't send player-update messages to it
// when updating the FSM
m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
session->Update((uint)NMT_CONNECTION_LOST, NULL);
delete session;
event.peer->data = NULL;
}
if (m_State == SERVER_STATE_LOADING)
CheckGameLoadStatus(NULL);
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
// If there is an active session with this peer, then process the message
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
// Create message from raw data
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
if (msg)
{
LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
HandleMessageReceive(msg, session);
delete msg;
}
}
// Done using the packet
enet_packet_destroy(event.packet);
break;
}
case ENET_EVENT_TYPE_NONE:
break;
}
return true;
}
void CNetServerWorker::CheckClientConnections()
{
// Send messages at most once per second
std::time_t now = std::time(nullptr);
if (now <= m_LastConnectionCheck)
return;
m_LastConnectionCheck = now;
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
u32 meanRTT = m_Sessions[i]->GetMeanRTT();
CNetMessage* message = nullptr;
// Report if we didn't hear from the client since few seconds
if (lastReceived > NETWORK_WARNING_TIMEOUT)
{
CClientTimeoutMessage* msg = new CClientTimeoutMessage();
msg->m_GUID = m_Sessions[i]->GetGUID();
msg->m_LastReceivedTime = lastReceived;
message = msg;
}
// Report if the client has bad ping
else if (meanRTT > DEFAULT_TURN_LENGTH_MP)
{
CClientPerformanceMessage* msg = new CClientPerformanceMessage();
CClientPerformanceMessage::S_m_Clients client;
client.m_GUID = m_Sessions[i]->GetGUID();
client.m_MeanRTT = meanRTT;
msg->m_Clients.push_back(client);
message = msg;
}
// Send to all clients except the affected one
// (since that will show the locally triggered warning instead).
// Also send it to clients that finished the loading screen while
// the game is still waiting for other clients to finish the loading screen.
if (message)
for (size_t j = 0; j < m_Sessions.size(); ++j)
{
if (i != j && (
(m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) ||
m_Sessions[j]->GetCurrState() == NSS_INGAME))
{
m_Sessions[j]->SendMessage(message);
}
}
SAFE_DELETE(message);
}
}
void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
{
// Handle non-FSM messages first
Status status = session->GetFileTransferer().HandleMessageReceive(*message);
if (status != INFO::SKIPPED)
return;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
// Rejoining client got our JoinSyncStart after we received the state from
// another client, and has now requested that we forward it to them
ENSURE(!m_JoinSyncFile.empty());
session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
return;
}
// Update FSM
if (!session->Update(message->GetType(), (void*)message))
LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
}
void CNetServerWorker::SetupSession(CNetServerSession* session)
{
void* context = session;
// Set up transitions for session
session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context);
session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, (void*)&OnReady, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, (void*)&OnClearAllReady, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnStartGame, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context);
session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnSimulationCommand, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnSyncCheck, context);
session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnEndCommandBatch, context);
// Set first state
session->SetFirstState(NSS_HANDSHAKE);
}
bool CNetServerWorker::HandleConnect(CNetServerSession* session)
{
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
{
session->Disconnect(NDR_BANNED);
return false;
}
CSrvHandshakeMessage handshake;
handshake.m_Magic = PS_PROTOCOL_MAGIC;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
return session->SendMessage(&handshake);
}
void CNetServerWorker::OnUserJoin(CNetServerSession* session)
{
AddPlayer(session->GetGUID(), session->GetUserName());
if (m_HostGUID.empty() && session->IsLocalClient())
m_HostGUID = session->GetGUID();
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes;
session->SendMessage(&gameSetupMessage);
CPlayerAssignmentMessage assignMessage;
ConstructPlayerAssignmentMessage(assignMessage);
session->SendMessage(&assignMessage);
}
void CNetServerWorker::OnUserLeave(CNetServerSession* session)
{
std::vector::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
if (pausing != m_PausingPlayers.end())
m_PausingPlayers.erase(pausing);
RemovePlayer(session->GetGUID());
if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
// TODO: ought to switch the player controlled by that client
// back to AI control, or something?
}
void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
{
// Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
std::set usedIDs;
for (const std::pair& p : m_PlayerAssignments)
if (p.second.m_Enabled && p.second.m_PlayerID != -1)
usedIDs.insert(p.second.m_PlayerID);
// If the player is rejoining after disconnecting, try to give them
// back their old player ID
i32 playerID = -1;
// Try to match GUID first
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
goto found;
}
}
// Try to match username next
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
goto found;
}
}
// Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
found:
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = name;
assignment.m_PlayerID = playerID;
assignment.m_Status = 0;
m_PlayerAssignments[guid] = assignment;
// Send the new assignments to all currently active players
// (which does not include the one that's just joining)
SendPlayerAssignments();
}
void CNetServerWorker::RemovePlayer(const CStr& guid)
{
m_PlayerAssignments[guid].m_Enabled = false;
SendPlayerAssignments();
}
void CNetServerWorker::ClearAllPlayerReady()
{
for (std::pair& p : m_PlayerAssignments)
if (p.second.m_Status != 2)
p.second.m_Status = 0;
SendPlayerAssignments();
}
void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
{
// Find the user with that name
std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
[&](CNetServerSession* session) { return session->GetUserName() == playerName; });
// and return if no one or the host has that name
if (it == m_Sessions.end() || (*it)->GetGUID() == m_HostGUID)
return;
if (ban)
{
// Remember name
if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName);
// Remember IP address
u32 ipAddress = (*it)->GetIPAddress();
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end())
m_BannedIPs.push_back(ipAddress);
}
// Disconnect that user
(*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
// Send message notifying other clients
CKickedMessage kickedMessage;
kickedMessage.m_Name = playerName;
kickedMessage.m_Ban = ban;
Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
}
void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
{
// Remove anyone who's already assigned to this player
for (std::pair& p : m_PlayerAssignments)
{
if (p.second.m_PlayerID == playerID)
p.second.m_PlayerID = -1;
}
// Update this host's assignment if it exists
if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
m_PlayerAssignments[guid].m_PlayerID = playerID;
SendPlayerAssignments();
}
void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
{
for (const std::pair& p : m_PlayerAssignments)
{
if (!p.second.m_Enabled)
continue;
CPlayerAssignmentMessage::S_m_Hosts h;
h.m_GUID = p.first;
h.m_Name = p.second.m_Name;
h.m_PlayerID = p.second.m_PlayerID;
h.m_Status = p.second.m_Status;
message.m_Hosts.push_back(h);
}
}
void CNetServerWorker::SendPlayerAssignments()
{
CPlayerAssignmentMessage message;
ConstructPlayerAssignmentMessage(message);
Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
}
const ScriptInterface& CNetServerWorker::GetScriptInterface()
{
return *m_ScriptInterface;
}
void CNetServerWorker::SetTurnLength(u32 msecs)
{
if (m_ServerTurnManager)
m_ServerTurnManager->SetTurnLength(msecs);
}
void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token)
{
LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token);
// Find the user with that guid
std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
[&](CNetServerSession* session)
{ return session->GetGUID() == token; });
if (it == m_Sessions.end())
return;
(*it)->SetUserName(name.FromUTF8());
// Send an empty message to request the authentication message from the client
// after its identity has been confirmed via the lobby
CAuthenticateMessage emptyMessage;
(*it)->SendMessage(&emptyMessage);
}
bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
{
session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
return false;
}
CStr guid = ps_generate_guid();
int count = 0;
// Ensure unique GUID
while(std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&guid] (const CNetServerSession* session)
{ return session->GetGUID() == guid; }) != server.m_Sessions.end())
{
if (++count > 100)
{
session->Disconnect(NDR_GUID_FAILED);
return true;
}
guid = ps_generate_guid();
}
session->SetGUID(guid);
CSrvHandshakeResponseMessage handshakeResponse;
handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
handshakeResponse.m_GUID = guid;
handshakeResponse.m_Flags = 0;
if (server.m_LobbyAuth)
{
handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH;
session->SetNextState(NSS_LOBBY_AUTHENTICATE);
}
session->SendMessage(&handshakeResponse);
return true;
}
bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Prohibit joins while the game is loading
if (server.m_State == SERVER_STATE_LOADING)
{
LOGMESSAGE("Refused connection while the game is loading");
session->Disconnect(NDR_SERVER_LOADING);
return true;
}
CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
CStrW username = SanitisePlayerName(message->m_Name);
CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
// Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
// "[...] comparisons will be made in case-normalized canonical form."
if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase())
{
LOGERROR("Net server: lobby auth: %s tried joining as %s",
session->GetUserName().ToUTF8(),
usernameWithoutRating.ToUTF8());
session->Disconnect(NDR_LOBBY_AUTH_FAILED);
return true;
}
// Either deduplicate or prohibit join if name is in use
bool duplicatePlayernames = false;
CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
// If lobby authentication is enabled, the clients playername has already been registered.
// There also can't be any duplicated names.
if (!server.m_LobbyAuth && duplicatePlayernames)
username = server.DeduplicatePlayerName(username);
else
{
std::vector::iterator it = std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&username] (const CNetServerSession* session)
{ return session->GetUserName() == username; });
if (it != server.m_Sessions.end() && (*it) != session)
{
session->Disconnect(NDR_PLAYERNAME_IN_USE);
return true;
}
}
// Disconnect banned usernames
if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end())
{
session->Disconnect(NDR_BANNED);
return true;
}
int maxObservers = 0;
CFG_GET_VAL("network.observerlimit", maxObservers);
bool isRejoining = false;
bool serverFull = false;
if (server.m_State == SERVER_STATE_PREGAME)
{
// Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
serverFull = server.m_Sessions.size() >= MAX_CLIENTS;
}
else
{
bool isObserver = true;
int disconnectedPlayers = 0;
int connectedPlayers = 0;
// (TODO: if GUIDs were stable, we should use them instead)
for (const std::pair& p : server.m_PlayerAssignments)
{
const PlayerAssignment& assignment = p.second;
if (!assignment.m_Enabled && assignment.m_Name == username)
{
isObserver = assignment.m_PlayerID == -1;
isRejoining = true;
}
if (assignment.m_PlayerID == -1)
continue;
if (assignment.m_Enabled)
++connectedPlayers;
else
++disconnectedPlayers;
}
// Optionally allow everyone or only buddies to join after the game has started
if (!isRejoining)
{
CStr observerLateJoin;
CFG_GET_VAL("network.lateobservers", observerLateJoin);
if (observerLateJoin == "everyone")
{
isRejoining = true;
}
else if (observerLateJoin == "buddies")
{
CStr buddies;
CFG_GET_VAL("lobby.buddies", buddies);
std::wstringstream buddiesStream(wstring_from_utf8(buddies));
CStrW buddy;
while (std::getline(buddiesStream, buddy, L','))
{
if (buddy == usernameWithoutRating)
{
isRejoining = true;
break;
}
}
}
}
if (!isRejoining)
{
LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username));
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
return true;
}
// Ensure all players will be able to rejoin
serverFull = isObserver && (
(int) server.m_Sessions.size() - connectedPlayers > maxObservers ||
(int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS);
}
if (serverFull)
{
session->Disconnect(NDR_SERVER_FULL);
return true;
}
// TODO: check server password etc?
u32 newHostID = server.m_NextHostID++;
session->SetUserName(username);
session->SetHostID(newHostID);
session->SetLocalClient(message->m_IsLocalClient);
CAuthenticateResultMessage authenticateResult;
authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
authenticateResult.m_HostID = newHostID;
authenticateResult.m_Message = L"Logged in";
session->SendMessage(&authenticateResult);
server.OnUserJoin(session);
if (isRejoining)
{
// Request a copy of the current game state from an existing player,
// so we can send it on to the new player
// Assume session 0 is most likely the local player, so they're
// the most efficient client to request a copy from
CNetServerSession* sourceSession = server.m_Sessions.at(0);
session->SetLongTimeout(true);
sourceSession->GetFileTransferer().StartTask(
shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
);
session->SetNextState(NSS_JOIN_SYNCING);
}
return true;
}
bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef();
// Ignore messages sent by one player on behalf of another player
// unless cheating is enabled
bool cheatsEnabled = false;
const ScriptInterface& scriptInterface = server.GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue settings(rq.cx);
scriptInterface.GetProperty(server.m_GameAttributes, "settings", &settings);
if (scriptInterface.HasProperty(settings, "CheatsEnabled"))
scriptInterface.GetProperty(settings, "CheatsEnabled", cheatsEnabled);
PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID());
// When cheating is disabled, fail if the player the message claims to
// represent does not exist or does not match the sender's player name
if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player))
return true;
// Send it back to all clients that have finished
// the loading screen (and the synchronization when rejoining)
server.Broadcast(message, { NSS_INGAME });
// Save all the received commands
if (server.m_SavedCommands.size() < message->m_Turn + 1)
server.m_SavedCommands.resize(message->m_Turn + 1);
server.m_SavedCommands[message->m_Turn].push_back(*message);
// TODO: we shouldn't send the message back to the client that first sent it
return true;
}
bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef();
server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash);
return true;
}
bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef();
// The turn-length field is ignored
server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn);
return true;
}
bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CChatMessage* message = (CChatMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
return true;
}
bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Occurs if a client presses not-ready
// in the very last moment before the hosts starts the game
if (server.m_State == SERVER_STATE_LOADING)
return true;
CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_PREGAME });
server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status;
return true;
}
bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_HostGUID)
server.ClearAllPlayerReady();
return true;
}
bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
// This happened when doubleclicking on the startgame button.
if (server.m_State != SERVER_STATE_PREGAME)
return true;
if (session->GetGUID() == server.m_HostGUID)
{
CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
server.UpdateGameAttributes(&(message->m_Data));
}
return true;
}
bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_HostGUID)
{
CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
server.AssignPlayer(message->m_PlayerID, message->m_GUID);
}
return true;
}
bool CNetServerWorker::OnStartGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_START);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_HostGUID)
server.StartGame();
return true;
}
bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* loadedSession = (CNetServerSession*)context;
CNetServerWorker& server = loadedSession->GetServer();
loadedSession->SetLongTimeout(false);
// We're in the loading state, so wait until every client has loaded
// before starting the game
ENSURE(server.m_State == SERVER_STATE_LOADING);
if (server.CheckGameLoadStatus(loadedSession))
return true;
CClientsLoadingMessage message;
// We always send all GUIDs of clients in the loading state
// so that we don't have to bother about switching GUI pages
for (CNetServerSession* session : server.m_Sessions)
if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID())
{
CClientsLoadingMessage::S_m_Clients client;
client.m_GUID = session->GetGUID();
message.m_Clients.push_back(client);
}
// Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
loadedSession->SendMessage(&message);
server.Broadcast(&message, { NSS_INGAME });
return true;
}
bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
{
// A client rejoining an in-progress game has now finished loading the
// map and deserialized the initial state.
// The simulation may have progressed since then, so send any subsequent
// commands to them and set them as an active player so they can participate
// in all future turns.
//
// (TODO: if it takes a long time for them to receive and execute all these
// commands, the other players will get frozen for that time and may be unhappy;
// we could try repeating this process a few times until the client converges
// on the up-to-date state, before setting them as active.)
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
u32 turn = message->m_CurrentTurn;
u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
// Send them all commands received since their saved state,
// and turn-ended messages for any turns that have already been processed
for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
{
if (i < server.m_SavedCommands.size())
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
session->SendMessage(&server.m_SavedCommands[i][j]);
if (i <= readyTurn)
{
CEndCommandBatchMessage endMessage;
endMessage.m_Turn = i;
endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
session->SendMessage(&endMessage);
}
}
// Tell the turn manager to expect commands from this new client
server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn);
// Tell the client that everything has finished loading and it should start now
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = readyTurn;
session->SendMessage(&loaded);
return true;
}
bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
{
// A client has finished rejoining and the loading screen disappeared.
ENSURE(event->GetType() == (uint)NMT_REJOINED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Inform everyone of the client having rejoined
CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_INGAME });
// Send all pausing players to the rejoined client.
for (const CStr& guid : server.m_PausingPlayers)
{
CClientPausedMessage pausedMessage;
pausedMessage.m_GUID = guid;
pausedMessage.m_Pause = true;
session->SendMessage(&pausedMessage);
}
session->SetLongTimeout(false);
return true;
}
bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_KICKED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_HostGUID)
{
CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
server.KickPlayer(message->m_Name, message->m_Ban);
}
return true;
}
bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
server.OnUserLeave(session);
return true;
}
bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
// Update the list of pausing players.
std::vector::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
if (message->m_Pause)
{
if (player != server.m_PausingPlayers.end())
return true;
server.m_PausingPlayers.push_back(session->GetGUID());
}
else
{
if (player == server.m_PausingPlayers.end())
return true;
server.m_PausingPlayers.erase(player);
}
// Send messages to clients that are in game, and are not the client who paused.
- for (CNetServerSession* session : server.m_Sessions)
- {
- if (session->GetCurrState() == NSS_INGAME && message->m_GUID != session->GetGUID())
- session->SendMessage(message);
- }
+ for (CNetServerSession* netSession : server.m_Sessions)
+ if (netSession->GetCurrState() == NSS_INGAME && message->m_GUID != netSession->GetGUID())
+ netSession->SendMessage(message);
return true;
}
bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
{
for (const CNetServerSession* session : m_Sessions)
if (session != changedSession && session->GetCurrState() != NSS_INGAME)
return false;
// Inform clients that everyone has loaded the map and that the game can start
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = 0;
// Notice the changedSession is still in the NSS_PREGAME state
Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME });
m_State = SERVER_STATE_INGAME;
return true;
}
void CNetServerWorker::StartGame()
{
for (std::pair& player : m_PlayerAssignments)
if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0)
{
LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str());
return;
}
m_ServerTurnManager = new CNetServerTurnManager(*this);
for (CNetServerSession* session : m_Sessions)
{
m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers
session->SetLongTimeout(true);
}
m_State = SERVER_STATE_LOADING;
// Send the final setup state to all clients
UpdateGameAttributes(&m_GameAttributes);
// Remove players and observers that are not present when the game starts
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();)
if (it->second.m_Enabled)
++it;
else
it = m_PlayerAssignments.erase(it);
SendPlayerAssignments();
CGameStartMessage gameStart;
Broadcast(&gameStart, { NSS_PREGAME });
}
void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs)
{
m_GameAttributes = attrs;
if (!m_Host)
return;
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes;
Broadcast(&gameSetupMessage, { NSS_PREGAME });
}
CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
{
const size_t MAX_LENGTH = 32;
CStrW name = original;
name.Replace(L"[", L"{"); // remove GUI tags
name.Replace(L"]", L"}"); // remove for symmetry
// Restrict the length
if (name.length() > MAX_LENGTH)
name = name.Left(MAX_LENGTH);
// Don't allow surrounding whitespace
name.Trim(PS_TRIM_BOTH);
// Don't allow empty name
if (name.empty())
name = L"Anonymous";
return name;
}
CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
{
CStrW name = original;
// Try names "Foo", "Foo (2)", "Foo (3)", etc
size_t id = 2;
while (true)
{
bool unique = true;
for (const CNetServerSession* session : m_Sessions)
{
if (session->GetUserName() == name)
{
unique = false;
break;
}
}
if (unique)
return name;
name = original + L" (" + CStrW::FromUInt(id++) + L")";
}
}
void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
{
if (m_Host)
StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
}
CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) :
m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)),
m_LobbyAuth(useLobbyAuth)
{
}
CNetServer::~CNetServer()
{
delete m_Worker;
}
bool CNetServer::UseLobbyAuth() const
{
return m_LobbyAuth;
}
bool CNetServer::SetupConnection(const u16 port)
{
return m_Worker->SetupConnection(port);
}
void CNetServer::StartGame()
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_StartGameQueue.push_back(true);
}
void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface)
{
// Pass the attributes as JSON, since that's the easiest safe
// cross-thread way of passing script data
std::string attrsJSON = scriptInterface.StringifyJSON(attrs, false);
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_GameAttributesQueue.push_back(attrsJSON);
}
void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
}
void CNetServer::SetTurnLength(u32 msecs)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_TurnLengthQueue.push_back(msecs);
}
void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
{
m_Worker->SendHolePunchingMessage(ip, port);
}
Index: ps/trunk/source/network/NetServerTurnManager.cpp
===================================================================
--- ps/trunk/source/network/NetServerTurnManager.cpp (revision 24267)
+++ ps/trunk/source/network/NetServerTurnManager.cpp (revision 24268)
@@ -1,202 +1,202 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* Copyright (C) 2020 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 "NetMessage.h"
#include "NetServerTurnManager.h"
#include "NetServer.h"
#include "NetSession.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include "simulation2/system/TurnManager.h"
#if 0
#define NETSERVERTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETSERVERTURN_LOG(...)
#endif
CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server)
: m_NetServer(server), m_ReadyTurn(1), m_TurnLength(DEFAULT_TURN_LENGTH_MP), m_HasSyncError(false)
{
// The first turn we will actually execute is number 2,
// so store dummy values into the saved lengths list
m_SavedTurnLengths.push_back(0);
m_SavedTurnLengths.push_back(0);
}
void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, u32 turn)
{
int client = session.GetHostID();
NETSERVERTURN_LOG("NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn);
// Must be a client we've already heard of
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
// Clients must advance one turn at a time
if (turn != m_ClientsReady[client] + 1)
{
LOGERROR("NotifyFinishedClientCommands: Client %d (%s) is ready for turn %d, but expected %d",
client,
utf8_from_wstring(session.GetUserName()).c_str(),
turn,
m_ClientsReady[client] + 1);
session.Disconnect(NDR_INCORRECT_READY_TURN_COMMANDS);
}
m_ClientsReady[client] = turn;
// Check whether this was the final client to become ready
CheckClientsReady();
}
void CNetServerTurnManager::CheckClientsReady()
{
// See if all clients (including self) are ready for a new turn
for (const std::pair& clientReady : m_ClientsReady)
{
NETSERVERTURN_LOG(" %d: %d <=? %d\n", clientReady.first, clientReady.second, m_ReadyTurn);
if (clientReady.second <= m_ReadyTurn)
return; // wasn't ready for m_ReadyTurn+1
}
++m_ReadyTurn;
NETSERVERTURN_LOG("CheckClientsReady: ready for turn %d\n", m_ReadyTurn);
// Tell all clients that the next turn is ready
CEndCommandBatchMessage msg;
msg.m_TurnLength = m_TurnLength;
msg.m_Turn = m_ReadyTurn;
m_NetServer.Broadcast(&msg, { NSS_INGAME });
ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash)
{
int client = session.GetHostID();
const CStrW& playername = session.GetUserName();
// Clients must advance one turn at a time
if (turn != m_ClientsSimulated[client] + 1)
{
LOGERROR("NotifyFinishedClientUpdate: Client %d (%s) is ready for turn %d, but expected %d",
client,
utf8_from_wstring(playername).c_str(),
turn,
m_ClientsReady[client] + 1);
session.Disconnect(NDR_INCORRECT_READY_TURN_SIMULATED);
}
m_ClientsSimulated[client] = turn;
// Check for OOS only if in sync
if (m_HasSyncError)
return;
m_ClientPlayernames[client] = playername;
m_ClientStateHashes[turn][client] = hash;
// Find the newest turn which we know all clients have simulated
u32 newest = std::numeric_limits::max();
for (const std::pair& clientSimulated : m_ClientsSimulated)
if (clientSimulated.second < newest)
newest = clientSimulated.second;
// For every set of state hashes that all clients have simulated, check for OOS
for (const std::pair>& clientStateHash : m_ClientStateHashes)
{
if (clientStateHash.first > newest)
break;
// Assume the host is correct (maybe we should choose the most common instead to help debugging)
std::string expected = clientStateHash.second.begin()->second;
// Find all players that are OOS on that turn
std::vector OOSPlayerNames;
for (const std::pair& hashPair : clientStateHash.second)
{
NETSERVERTURN_LOG("sync check %d: %d = %hs\n", it->first, cit->first, Hexify(cit->second).c_str());
if (hashPair.second != expected)
{
// Oh no, out of sync
m_HasSyncError = true;
OOSPlayerNames.push_back(m_ClientPlayernames[hashPair.first]);
}
}
// Tell everyone about it
if (m_HasSyncError)
{
CSyncErrorMessage msg;
msg.m_Turn = clientStateHash.first;
msg.m_HashExpected = expected;
- for (const CStrW& playername : OOSPlayerNames)
+ for (const CStrW& oosPlayername : OOSPlayerNames)
{
CSyncErrorMessage::S_m_PlayerNames h;
- h.m_Name = playername;
+ h.m_Name = oosPlayername;
msg.m_PlayerNames.push_back(h);
}
m_NetServer.Broadcast(&msg, { NSS_INGAME });
break;
}
}
// Delete the saved hashes for all turns that we've already verified
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
}
void CNetServerTurnManager::InitialiseClient(int client, u32 turn)
{
NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);
ENSURE(m_ClientsReady.find(client) == m_ClientsReady.end());
m_ClientsReady[client] = turn + 1;
m_ClientsSimulated[client] = turn;
}
void CNetServerTurnManager::UninitialiseClient(int client)
{
NETSERVERTURN_LOG("UninitialiseClient(client=%d)\n", client);
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
m_ClientsReady.erase(client);
m_ClientsSimulated.erase(client);
// Check whether we're ready for the next turn now that we're not
// waiting for this client any more
CheckClientsReady();
}
void CNetServerTurnManager::SetTurnLength(u32 msecs)
{
m_TurnLength = msecs;
}
u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn)
{
ENSURE(turn <= m_ReadyTurn);
return m_SavedTurnLengths.at(turn);
}
Index: ps/trunk/source/network/scripting/JSInterface_Network.cpp
===================================================================
--- ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 24267)
+++ ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 24268)
@@ -1,248 +1,248 @@
/* Copyright (C) 2020 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 "JSInterface_Network.h"
#include "lib/external_libraries/enet.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/types.h"
#include "lobby/IXmppClient.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetServer.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "scriptinterface/ScriptInterface.h"
u16 JSI_Network::GetDefaultPort(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
return PS_DEFAULT_PORT;
}
bool JSI_Network::HasNetServer(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
- return g_NetServer;
+ return !!g_NetServer;
}
bool JSI_Network::HasNetClient(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
- return g_NetClient;
+ return !!g_NetClient;
}
JS::Value JSI_Network::FindStunEndpoint(ScriptInterface::CmptPrivate* pCmptPrivate, int port)
{
return StunClient::FindStunEndpointHost(*(pCmptPrivate->pScriptInterface), port);
}
void JSI_Network::StartNetworkHost(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
// Always use lobby authentication for lobby matches to prevent impersonation and smurfing, in particular through mods that implemented an UI for arbitrary or other players nicknames.
- g_NetServer = new CNetServer(static_cast(g_XmppClient));
+ g_NetServer = new CNetServer(!!g_XmppClient);
if (!g_NetServer->SetupConnection(serverPort))
{
ScriptRequest rq(pCmptPrivate->pScriptInterface);
ScriptException::Raise(rq, "Failed to start server");
SAFE_DELETE(g_NetServer);
return;
}
g_Game = new CGame(true);
g_NetClient = new CNetClient(g_Game, true);
g_NetClient->SetUserName(playerName);
g_NetClient->SetHostingPlayerName(hostLobbyName);
if (!g_NetClient->SetupConnection("127.0.0.1", serverPort, nullptr))
{
ScriptRequest rq(pCmptPrivate->pScriptInterface);
ScriptException::Raise(rq, "Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void JSI_Network::StartNetworkJoin(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const CStr& serverAddress, u16 serverPort, bool useSTUN, const CStr& hostJID)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
ENetHost* enetClient = nullptr;
if (g_XmppClient && useSTUN)
{
// Find an unused port
for (int i = 0; i < 5 && !enetClient; ++i)
{
// Ports below 1024 are privileged on unix
u16 port = 1024 + rand() % (UINT16_MAX - 1024);
ENetAddress hostAddr{ENET_HOST_ANY, port};
enetClient = enet_host_create(&hostAddr, 1, 1, 0, 0);
++hostAddr.port;
}
if (!enetClient)
{
ScriptRequest rq(pCmptPrivate->pScriptInterface);
ScriptException::Raise(rq, "Could not find an unused port for the enet STUN client");
return;
}
StunClient::StunEndpoint stunEndpoint;
if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint))
{
ScriptRequest rq(pCmptPrivate->pScriptInterface);
ScriptException::Raise(rq, "Could not find the STUN endpoint");
return;
}
g_XmppClient->SendStunEndpointToHost(stunEndpoint, hostJID);
SDL_Delay(1000);
}
g_Game = new CGame(true);
g_NetClient = new CNetClient(g_Game, false);
g_NetClient->SetUserName(playerName);
g_NetClient->SetHostingPlayerName(hostJID.substr(0, hostJID.find("@")));
if (g_XmppClient && useSTUN)
StunClient::SendHolePunchingMessages(*enetClient, serverAddress, serverPort);
if (!g_NetClient->SetupConnection(serverAddress, serverPort, enetClient))
{
ScriptRequest rq(pCmptPrivate->pScriptInterface);
ScriptException::Raise(rq, "Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void JSI_Network::DisconnectNetworkGame(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
// TODO: we ought to do async reliable disconnections
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
CStr JSI_Network::GetPlayerGUID(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
if (!g_NetClient)
return "local";
return g_NetClient->GetGUID();
}
JS::Value JSI_Network::PollNetworkClient(ScriptInterface::CmptPrivate* pCmptPrivate)
{
if (!g_NetClient)
return JS::UndefinedValue();
// Convert from net client context to GUI script context
ScriptRequest rqNet(g_NetClient->GetScriptInterface());
JS::RootedValue pollNet(rqNet.cx);
g_NetClient->GuiPoll(&pollNet);
return pCmptPrivate->pScriptInterface->CloneValueFromOtherCompartment(g_NetClient->GetScriptInterface(), pollNet, false);
}
void JSI_Network::SetNetworkGameAttributes(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue attribs1)
{
ENSURE(g_NetClient);
// TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere (with no obvious reason).
ScriptRequest rq(pCmptPrivate->pScriptInterface);
JS::RootedValue attribs(rq.cx, attribs1);
g_NetClient->SendGameSetupMessage(&attribs, *(pCmptPrivate->pScriptInterface));
}
void JSI_Network::AssignNetworkPlayer(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int playerID, const CStr& guid)
{
ENSURE(g_NetClient);
g_NetClient->SendAssignPlayerMessage(playerID, guid);
}
void JSI_Network::KickPlayer(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& playerName, bool ban)
{
ENSURE(g_NetClient);
g_NetClient->SendKickPlayerMessage(playerName, ban);
}
void JSI_Network::SendNetworkChat(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& message)
{
ENSURE(g_NetClient);
g_NetClient->SendChatMessage(message);
}
void JSI_Network::SendNetworkReady(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int message)
{
ENSURE(g_NetClient);
g_NetClient->SendReadyMessage(message);
}
void JSI_Network::ClearAllPlayerReady (ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
ENSURE(g_NetClient);
g_NetClient->SendClearAllReadyMessage();
}
void JSI_Network::StartNetworkGame(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
ENSURE(g_NetClient);
g_NetClient->SendStartGameMessage();
}
void JSI_Network::SetTurnLength(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int length)
{
if (g_NetServer)
g_NetServer->SetTurnLength(length);
else
LOGERROR("Only network host can change turn length");
}
void JSI_Network::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("GetDefaultPort");
scriptInterface.RegisterFunction("HasNetServer");
scriptInterface.RegisterFunction("HasNetClient");
scriptInterface.RegisterFunction("FindStunEndpoint");
scriptInterface.RegisterFunction("StartNetworkHost");
scriptInterface.RegisterFunction("StartNetworkJoin");
scriptInterface.RegisterFunction("DisconnectNetworkGame");
scriptInterface.RegisterFunction("GetPlayerGUID");
scriptInterface.RegisterFunction("PollNetworkClient");
scriptInterface.RegisterFunction("SetNetworkGameAttributes");
scriptInterface.RegisterFunction("AssignNetworkPlayer");
scriptInterface.RegisterFunction("KickPlayer");
scriptInterface.RegisterFunction("SendNetworkChat");
scriptInterface.RegisterFunction("SendNetworkReady");
scriptInterface.RegisterFunction("ClearAllPlayerReady");
scriptInterface.RegisterFunction("StartNetworkGame");
scriptInterface.RegisterFunction("SetTurnLength");
}
Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp
===================================================================
--- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 24267)
+++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 24268)
@@ -1,1638 +1,1638 @@
/* Copyright (C) 2020 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 "lib/app_hooks.h"
#include "lib/config2.h"
#include "lib/input.h"
#include "lib/ogl.h"
#include "lib/timer.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/file/common/file_stats.h"
#include "lib/res/h_mgr.h"
#include "lib/res/graphics/cursor.h"
#include "graphics/CinemaManager.h"
#include "graphics/FontMetrics.h"
#include "graphics/GameView.h"
#include "graphics/LightEnv.h"
#include "graphics/MapReader.h"
#include "graphics/MaterialManager.h"
#include "graphics/TerrainTextureManager.h"
#include "gui/CGUI.h"
#include "gui/GUIManager.h"
#include "i18n/L10n.h"
#include "maths/MathUtil.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/GameSetup.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/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/VertexBufferManager.h"
#include "renderer/ModelRenderer.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptStats.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptConversions.h"
#include "simulation2/Simulation2.h"
#include "lobby/IXmppClient.h"
#include "soundmanager/scripting/JSInterface_Sound.h"
#include "soundmanager/ISoundManager.h"
#include "tools/atlas/GameInterface/GameLoop.h"
#include "tools/atlas/GameInterface/View.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
ERROR_GROUP(System);
ERROR_TYPE(System, SDLInitFailed);
ERROR_TYPE(System, VmodeFailed);
ERROR_TYPE(System, RequiredExtensionsMissing);
bool g_DoRenderGui = true;
bool g_DoRenderLogger = true;
bool g_DoRenderCursor = true;
thread_local shared_ptr g_ScriptContext;
static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code
static const CStr g_EventNameGameLoadProgress = "GameLoadProgress";
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);
}
//----------------------------------------------------------------------------
// GUI integration
//----------------------------------------------------------------------------
// display progress / description in loading screen
void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task)
{
const ScriptInterface& scriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface());
ScriptRequest rq(scriptInterface);
JS::AutoValueVector paramData(rq.cx);
DISCARD paramData.append(JS::NumberValue(percent));
JS::RootedValue valPendingTask(rq.cx);
scriptInterface.ToJSVal(rq, &valPendingTask, pending_task);
DISCARD paramData.append(valPendingTask);
g_GUI->SendEventToAll(g_EventNameGameLoadProgress, paramData);
}
bool ShouldRender()
{
return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen());
}
void Render()
{
// 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;
PROFILE3("render");
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameStart();
ogl_WarnIfError();
// prepare before starting the renderer frame
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->BeginFrame();
if (g_Game)
g_Renderer.SetSimulation(g_Game->GetSimulation2());
// start new frame
g_Renderer.BeginFrame();
ogl_WarnIfError();
if (g_Game && g_Game->IsGameStarted())
g_Game->GetView()->Render();
ogl_WarnIfError();
g_Renderer.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();
if (g_DoRenderGui)
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)
{
g_AtlasGameLoop->view->DrawOverlays();
ogl_WarnIfError();
}
// Text:
glDisable(GL_DEPTH_TEST);
g_Console->Render();
ogl_WarnIfError();
if (g_DoRenderLogger)
g_Logger->Render();
ogl_WarnIfError();
// Profile information
g_ProfileViewer.RenderProfile();
ogl_WarnIfError();
// Draw the cursor (or set the Windows cursor, on Windows)
if (g_DoRenderCursor)
{
PROFILE3_GPU("cursor");
CStrW cursorName = g_CursorName;
if (cursorName.empty())
{
cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false);
}
else
{
bool forceGL = false;
CFG_GET_VAL("nohwcursor", forceGL);
#if CONFIG2_GLES
#warning TODO: implement cursors for GLES
#else
// set up transform for GL cursor
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
CMatrix3D transform;
transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f);
glLoadMatrixf(&transform._11);
#endif
#if OS_ANDROID
#warning TODO: cursors for Android
#else
if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0)
LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName));
#endif
#if CONFIG2_GLES
#warning TODO: implement cursors for GLES
#else
// restore transform
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
#endif
}
}
glEnable(GL_DEPTH_TEST);
g_Renderer.EndFrame();
PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls);
PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris);
PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris);
PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris);
PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris);
PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats);
PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles);
ogl_WarnIfError();
g_Profiler2.RecordGPUFrameEnd();
ogl_WarnIfError();
}
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 (ThreadUtil::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;
}
const std::vector& GetMods(const CmdLineArgs& args, int flags)
{
const bool init_mods = (flags & INIT_MODS) == INIT_MODS;
const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod");
const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC;
if (!init_mods)
{
// Add the user mod if it should be present
if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user"))
g_modsLoaded.push_back("user");
return g_modsLoaded;
}
g_modsLoaded = args.GetMultiple("mod");
if (add_public)
g_modsLoaded.insert(g_modsLoaded.begin(), "public");
g_modsLoaded.insert(g_modsLoaded.begin(), "mod");
// Add the user mod if not explicitly disabled or we have a dev copy so
// that saved files end up in version control and not in the user mod.
if (add_user)
g_modsLoaded.push_back("user");
return g_modsLoaded;
}
void MountMods(const Paths& paths, const std::vector& mods)
{
OsPath modPath = paths.RData()/"mods";
OsPath modUserPath = paths.UserData()/"mods";
for (size_t i = 0; i < mods.size(); ++i)
{
size_t priority = (i+1)*2; // mods are higher priority than regular mountings, which default to priority 0
size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_REPLACEABLE;
size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST;
OsPath modName(mods[i]);
if (InDevelopmentCopy())
{
// We are running a dev copy, so only mount mods in the user mod path
// if the mod does not exist in the data path.
if (DirectoryExists(modPath / modName/""))
g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority);
else
g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority);
}
else
{
g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority);
// Ensure that user modified files are loaded, if they are present
g_VFS->Mount(L"", modUserPath / modName/"", userFlags, 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"/"";
g_VFS->Mount(L"config/", readonlyConfig);
// Engine localization files.
g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/"");
MountMods(paths, GetMods(args, flags));
// We mount these dirs last as otherwise writing could result in files being placed in a mod's dir.
g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"");
g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH);
// Mounting with highest priority, so that a mod supplied user.cfg is harmless
g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1);
if(readonlyConfig != paths.Config())
g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1);
g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads)
// 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->UpdateScreenSize(g_xres, g_yres);
// Calculate and store the line spacing
CFontMetrics font(CStrIntern(CONSOLE_FONT));
g_Console->m_iFontHeight = font.GetLineSpacing();
g_Console->m_iFontWidth = font.GetCharacterWidth(L'C');
g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth);
// Offset by an arbitrary amount, to make it fit more nicely
g_Console->m_iFontOffset = 7;
double blinkRate = 0.5;
CFG_GET_VAL("gui.cursorblinkrate", blinkRate);
g_Console->SetCursorBlinkRate(blinkRate);
}
// hotkeys
{
TIMER(L"ps_lang_hotkeys");
LoadHotkeys();
}
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);
ScriptInterface::CreateObject(rq, &playerAssignments);
if (!networked)
{
JS::RootedValue localPlayer(rq.cx);
ScriptInterface::CreateObject(rq, &localPlayer, "player", g_Game->GetPlayerID());
scriptInterface.SetProperty(playerAssignments, "local", localPlayer);
}
JS::RootedValue sessionInitData(rq.cx);
ScriptInterface::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(conInputHandler);
in_add_handler(HotkeyInputHandler);
// 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);
in_add_handler(touch_input_handler);
// must be registered after (called before) the GUI which relies on these globals
in_add_handler(GlobalsInputHandler);
// Should be called first, this updates our hotkey press state
// so that js calls to HotkeyIsPressed are synched with events.
in_add_handler(HotkeyStateChange);
}
static void ShutdownPs()
{
SAFE_DELETE(g_GUI);
UnloadHotkeys();
// disable the special Windows cursor, or free textures for OGL cursors
cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false);
}
static void InitRenderer()
{
TIMER(L"InitRenderer");
// create renderer
new CRenderer;
// create terrain related stuff
new CTerrainTextureManager;
g_RenderingOptions.ReadConfigAndSetupHooks();
g_Renderer.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.
g_Renderer.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;
g_Renderer.SetViewport(vp);
ColorActivateFastImpl();
ModelRenderer::Init();
}
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 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.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();
// Free cursors before shutting down SDL, as they may depend on SDL.
cursor_shutdown();
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");
delete &g_ConfigDB;
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);
ThreadUtil::SetMainThread();
debug_SetThreadName("main");
// add all debug_printf "tags" that we are interested in:
debug_filter_add("TIMER");
timer_LatchStartTime();
// 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);
Mod::CacheEnabledModVersions(g_ScriptContext);
// 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"))
ISoundManager::CreateSoundManager();
#endif
// Check if there are mods specified on the command line,
// or if we already set the mods (~INIT_MODS),
// else check if there are mods that should be loaded specified
// in the config and load those (by aborting init and restarting
// the engine).
if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS)
{
CStr modstring;
CFG_GET_VAL("mod.enabledmods", modstring);
if (!modstring.empty())
{
std::vector mods;
boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on);
std::swap(g_modsLoaded, mods);
// Abort init and restart
RestartEngine();
return false;
}
}
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();
const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file
SetTextureQuality(quality);
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();
// (must come after SetVideoMode, since it calls ogl_Init)
CStr8 renderPath = "default";
CFG_GET_VAL("renderpath", renderPath);
if ((ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB
&& ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL
|| 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"
);
}
const char* missing = ogl_HaveExtensions(0,
"GL_ARB_multitexture",
"GL_EXT_draw_range_elements",
"GL_ARB_texture_env_combine",
"GL_ARB_texture_env_dot3",
NULL);
if(missing)
{
wchar_t buf[500];
swprintf_s(buf, ARRAY_SIZE(buf),
L"The %hs extension doesn't appear to be available on your computer."
L" The game may still work, though - you are welcome to try at your own risk."
L" If not or it doesn't look right, upgrade your graphics card.",
missing
);
DEBUG_DISPLAY_ERROR(buf);
// TODO: i18n
}
if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar"))
{
DEBUG_DISPLAY_ERROR(
L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer."
L" Shadows are not available and overall graphics quality might suffer."
L" You are advised to try installing newer drivers and/or upgrade your graphics card.");
g_ConfigDB.SetValueBool(CFG_HWDETECT, "shadows", false);
}
ogl_WarnIfError();
InitRenderer();
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
shared_ptr scriptInterface = g_GUI->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue data(rq.cx);
if (g_GUI)
{
ScriptInterface::CreateObject(rq, &data, "isStartup", true);
if (!installedMods.empty())
scriptInterface->SetProperty(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);
}
void RenderGui(bool RenderingState)
{
g_DoRenderGui = RenderingState;
}
void RenderLogger(bool RenderingState)
{
g_DoRenderLogger = RenderingState;
}
void RenderCursor(bool RenderingState)
{
g_DoRenderCursor = RenderingState;
}
/**
* 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);
ScriptInterface::CreateObject(rq, &attrs);
ScriptInterface::CreateObject(rq, &settings);
ScriptInterface::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);
scriptInterface.ReadJSONFile(scriptPath, &scriptData);
if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings))
{
// JSON loaded ok - copy script name over to game attributes
std::wstring scriptFile;
scriptInterface.GetProperty(settings, "Script", scriptFile);
scriptInterface.SetProperty(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();
}
scriptInterface.SetProperty(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
ScriptInterface::CreateObject(rq, &player, "Civ", "athen");
scriptInterface.SetPropertyInt(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");
scriptInterface.ParseJSON(mapSettingsJSON, &settings);
// Initialize the playerData array being modified by autostart
// with the real map data, so sensible values are present:
scriptInterface.GetProperty(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.");
}
scriptInterface.SetProperty(attrs, "mapType", mapType);
scriptInterface.SetProperty(attrs, "map", "maps/" + autoStartName);
scriptInterface.SetProperty(settings, "mapType", mapType);
scriptInterface.SetProperty(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();
}
scriptInterface.SetProperty(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();
}
scriptInterface.SetProperty(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 (scriptInterface.GetPropertyInt(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 player(rq.cx);
- if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined())
+ JS::RootedValue currentPlayer(rq.cx);
+ if (!scriptInterface.GetPropertyInt(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;
}
- ScriptInterface::CreateObject(rq, &player);
+ ScriptInterface::CreateObject(rq, ¤tPlayer);
}
int teamID = civArgs[i].AfterFirst(":").ToInt() - 1;
- scriptInterface.SetProperty(player, "Team", teamID);
- scriptInterface.SetPropertyInt(playerData, playerID-offset, player);
+ scriptInterface.SetProperty(currentPlayer, "Team", teamID);
+ scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer);
}
}
int ceasefire = 0;
if (args.Has("autostart-ceasefire"))
ceasefire = args.Get("autostart-ceasefire").ToInt();
scriptInterface.SetProperty(settings, "Ceasefire", ceasefire);
if (args.Has("autostart-ai"))
{
std::vector