0ad/source/graphics/MapReader.cpp
phosit 13453a3c7b
Remove ScriptContext::CreateContext
This function nudges one into using it instead of the constructor. Even
though a `std::shared_ptr` isn't required.
2026-01-24 12:03:05 +01:00

1643 lines
47 KiB
C++

/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "MapReader.h"
#include "graphics/Camera.h"
#include "graphics/Color.h"
#include "graphics/Entity.h"
#include "graphics/GameView.h"
#include "graphics/MapGenerator.h"
#include "graphics/MapIO.h"
#include "graphics/MiniPatch.h"
#include "graphics/Patch.h"
#include "graphics/Terrain.h"
#include "graphics/TerrainTextureEntry.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/alignment.h"
#include "lib/code_annotation.h"
#include "lib/debug.h"
#include "lib/path.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "maths/Fixed.h"
#include "maths/FixedVector3D.h"
#include "maths/MathUtil.h"
#include "maths/Matrix3D.h"
#include "maths/NUSpline.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Future.h"
#include "ps/Loader.h"
#include "ps/Profiler2.h"
#include "ps/TaskManager.h"
#include "ps/World.h"
#include "ps/XMB/XMBData.h"
#include "ps/XML/Xeromyces.h"
#include "renderer/PostprocManager.h"
#include "renderer/SkyManager.h"
#include "renderer/WaterManager.h"
#include "scriptinterface/JSON.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRequest.h"
#include "scriptinterface/StructuredClone.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 "simulation2/helpers/CinemaPath.h"
#include "simulation2/helpers/Position.h"
#include "simulation2/system/CmpPtr.h"
#include <algorithm>
#include <atomic>
#include <functional>
#include <js/PropertyAndElement.h>
#include <numeric>
#include <ranges>
#include <string>
#include <string_view>
#include <utility>
extern bool IsQuitRequested();
#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
// TODO: Maybe this should be optimized depending on the map size.
constexpr int MAP_GENERATION_CONTEXT_SIZE{96 * MiB};
CMapReader::CMapReader() = default;
// 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);
m_FilenameXml = 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(m_FilenameXml))
{
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())
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->LoadScriptSettings();
}, this), L"CMapReader::LoadScriptSettings", 50);
else
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->LoadRMSettings();
}, this), L"CMapReader::LoadRMSettings", 50);
// load player settings script (must be done before reading map)
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->LoadPlayerSettings();
}, this), L"CMapReader::LoadPlayerSettings", 50);
// unpack the data
if (!only_xml)
PS::Loader::Register([this]
{
return UnpackTerrain();
}, L"CMapReader::UnpackMap", 1200);
// read the corresponding XML file
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->ReadXML();
}, this), L"CMapReader::ReadXML", 50);
// apply terrain data to the world
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->ApplyTerrainData();
}, this), L"CMapReader::ApplyTerrainData", 5);
// read entities
PS::Loader::Register([this]
{
return ReadXMLEntities();
}, L"CMapReader::ReadXMLEntities", 5800);
// apply misc data to the world
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->ApplyData();
}, this), L"CMapReader::ApplyData", 5);
// load map settings script (must be done after reading map)
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->LoadMapSettings();
}, this), 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_)
{
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)
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->LoadRMSettings();
}, this), L"CMapReader::LoadRMSettings", 50);
// load player settings script (must be done before reading map)
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->LoadPlayerSettings();
}, this), L"CMapReader::LoadPlayerSettings", 50);
// load map generator with random map script
PS::Loader::Register([this, scriptFile]
{
return RunMapGeneration(scriptFile);
}, L"CMapReader::RunMapGeneration", 20000);
// parse RMS results into terrain structure
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->ParseTerrain();
}, this), L"CMapReader::ParseTerrain", 500);
// parse RMS results into environment settings
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->ParseEnvironment();
}, this), L"CMapReader::ParseEnvironment", 5);
// parse RMS results into camera settings
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->ParseCamera();
}, this), L"CMapReader::ParseCamera", 5);
// apply terrain data to the world
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->ApplyTerrainData();
}, this), L"CMapReader::ApplyTerrainData", 5);
// parse RMS results into entities
PS::Loader::Register([this]
{
return ParseEntities();
}, L"CMapReader::ParseEntities", 1010);
// apply misc data to the world
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->ApplyData();
}, this), L"CMapReader::ApplyData", 5);
// load map settings script (must be done after reading map)
PS::Loader::Register(std::bind_front([](CMapReader* mapReader) -> PS::Loader::Task
{
co_return mapReader->LoadMapSettings();
}, this), L"CMapReader::LoadMapSettings", 5);
}
// 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
PS::Loader::Task CMapReader::UnpackTerrain()
{
{
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
const std::size_t numTerrainTex{unpacker.UnpackSize()};
m_TerrainTextures.reserve(numTerrainTex);
// unpack texture names; find handle for each texture.
// interruptible.
for (std::size_t curTerrainTex{0}; curTerrainTex != numTerrainTex; ++curTerrainTex)
{
CStr texturename;
unpacker.UnpackString(texturename);
if (CTerrainTextureManager::IsInitialised())
{
CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texturename);
m_TerrainTextures.push_back(texentry);
}
co_yield 100 * (curTerrainTex + 1) / numTerrainTex;
}
// 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());
co_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]);
if (CTerrainTextureManager::IsInitialised())
{
// setup the textures on the minipatches
STileDesc* tileptr = &m_Tiles[0];
for (ssize_t j=0; j<m_PatchesPerSide; j++) {
for (ssize_t i=0; i<m_PatchesPerSide; i++) {
for (ssize_t m=0; m<PATCH_SIZE; m++) {
for (ssize_t k=0; k<PATCH_SIZE; k++) {
CMiniPatch& mp = pTerrain->GetPatch(i,j)->m_MiniPatches[m][k]; // can't fail
mp.Tex = m_TerrainTextures[tileptr->m_Tex1Index];
mp.Priority = tileptr->m_Priority;
tileptr++;
}
}
}
}
}
}
CmpPtr<ICmpTerrain> 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<ICmpPlayerManager> cmpPlayerManager(*pSimContext, SYSTEM_ENTITY);
if (pGameView && cmpPlayerManager)
{
// Default to global camera (with constraints)
pGameView->ResetCameraTarget(pGameView->GetCamera()->GetFocus());
// TODO: Starting rotation?
CmpPtr<ICmpPlayer> 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<ICmpPosition> cmpPosition(*pSimContext, m_StartingCameraTarget);
if (cmpPosition)
{
CFixedVector3D pos = cmpPosition->GetPosition();
pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat()));
}
}
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
PSRETURN CMapSummaryReader::LoadMap(const VfsPath& pathname)
{
VfsPath filename_xml = pathname.ChangeExtension(L".xml");
CXeromyces xmb_file;
if (xmb_file.Load(g_VFS, filename_xml, "scenario") != PSRETURN_OK)
return PSRETURN_File_ReadFailed;
// Define all the relevant elements used in the XML file
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(scenario);
EL(scriptsettings);
#undef AT
#undef EL
XMBElement root = xmb_file.GetRoot();
ENSURE(root.GetNodeName() == el_scenario);
XERO_ITER_EL(root, child)
{
int child_name = child.GetNodeName();
if (child_name == el_scriptsettings)
{
m_ScriptSettings = child.GetText();
}
}
return PSRETURN_OK;
}
void CMapSummaryReader::GetMapSettings(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
ScriptRequest rq(scriptInterface);
Script::CreateObject(rq, ret);
if (m_ScriptSettings.empty())
return;
JS::RootedValue scriptSettingsVal(rq.cx);
Script::ParseJSON(rq, m_ScriptSettings, &scriptSettingsVal);
Script::SetProperty(rq, ret, "settings", scriptSettingsVal, false);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Holds various state data while reading maps, so that loading can be
// interrupted (e.g. to update the progress display) then later resumed.
class CXMLReader
{
NONCOPYABLE(CXMLReader);
public:
CXMLReader(const VfsPath& xml_filename, CMapReader& mapReader)
: m_MapReader(mapReader), nodes(NULL, 0, NULL)
{
Init(xml_filename);
}
CStr ReadScriptSettings();
// read everything except for entities
void ReadXML();
CXeromyces xmb_file;
XMBElementList nodes; // children of root
void ReadEntities(XMBElement parent, CSimulation2& sim);
private:
CMapReader& m_MapReader;
int el_entity;
int el_tracks;
int el_template, el_player;
int el_position, el_orientation, el_obstruction;
int el_garrison;
int el_turrets;
int el_actor;
int at_x;
int at_y;
int at_z;
int at_group, at_group2;
int at_angle;
int at_uid;
int at_seed;
int at_turret;
// 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);
};
void CXMLReader::Init(const VfsPath& xml_filename)
{
if (xmb_file.Load(g_VFS, xml_filename, "scenario") != PSRETURN_OK)
throw PSERROR_Game_World_MapLoadFailed("Could not read map XML file!");
// define the elements and attributes that are frequently used in the XML file,
// so we don't need to do lots of string construction and comparison when
// reading the data.
// (Needs to be synchronised with the list in CXMLReader - ugh)
#define EL(x) el_##x = xmb_file.GetElementID(#x)
#define AT(x) at_##x = xmb_file.GetAttributeID(#x)
EL(entity);
EL(tracks);
EL(template);
EL(player);
EL(position);
EL(garrison);
EL(turrets);
EL(orientation);
EL(obstruction);
EL(actor);
AT(x); AT(y); AT(z);
AT(group); AT(group2);
AT(angle);
AT(uid);
AT(seed);
AT(turret);
#undef AT
#undef EL
XMBElement root = xmb_file.GetRoot();
ENSURE(xmb_file.GetElementStringView(root.GetNodeName()) == "Scenario");
nodes = root.GetChildNodes();
// Find the maximum entity ID, so we can safely allocate new IDs without conflicts
max_uid = SYSTEM_ENTITY;
XMBElement ents = nodes.GetFirstNamedItem(xmb_file.GetElementID("Entities"));
XERO_ITER_EL(ents, ent)
{
CStr uid = ent.GetAttributes().GetNamedItem(at_uid);
max_uid = std::max(max_uid, (entity_id_t)uid.ToUInt());
}
}
CStr CXMLReader::ReadScriptSettings()
{
XMBElement root = xmb_file.GetRoot();
ENSURE(xmb_file.GetElementStringView(root.GetNodeName()) == "Scenario");
nodes = root.GetChildNodes();
XMBElement settings = nodes.GetFirstNamedItem(xmb_file.GetElementID("ScriptSettings"));
return settings.GetText();
}
void CXMLReader::ReadTerrain(XMBElement parent)
{
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
AT(patches);
AT(texture);
AT(priority);
AT(height);
#undef AT
ssize_t patches = 9;
CStr texture = "grass1_spring";
int priority = 0;
u16 height = 16384;
XERO_ITER_ATTR(parent, attr)
{
if (attr.Name == at_patches)
patches = attr.Value.ToInt();
else if (attr.Name == at_texture)
texture = attr.Value;
else if (attr.Name == at_priority)
priority = attr.Value.ToInt();
else if (attr.Name == at_height)
height = (u16)attr.Value.ToInt();
}
m_MapReader.m_PatchesPerSide = patches;
// Load the texture
CTerrainTextureEntry* texentry = nullptr;
if (CTerrainTextureManager::IsInitialised())
texentry = g_TexMan.FindTexture(texture);
m_MapReader.pTerrain->Initialize(patches, NULL);
// Fill the heightmap
u16* heightmap = m_MapReader.pTerrain->GetHeightMap();
ssize_t verticesPerSide = m_MapReader.pTerrain->GetVerticesPerSide();
for (ssize_t i = 0; i < SQR(verticesPerSide); ++i)
heightmap[i] = height;
// Fill the texture map
for (ssize_t pz = 0; pz < patches; ++pz)
{
for (ssize_t px = 0; px < patches; ++px)
{
CPatch* patch = m_MapReader.pTerrain->GetPatch(px, pz); // can't fail
for (ssize_t z = 0; z < PATCH_SIZE; ++z)
{
for (ssize_t x = 0; x < PATCH_SIZE; ++x)
{
patch->m_MiniPatches[z][x].Tex = texentry;
patch->m_MiniPatches[z][x].Priority = priority;
}
}
}
}
}
void CXMLReader::ReadEnvironment(XMBElement parent)
{
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(posteffect);
EL(skyset);
EL(suncolor);
EL(sunelevation);
EL(sunrotation);
EL(ambientcolor);
EL(water);
EL(waterbody);
EL(type);
EL(color);
EL(tint);
EL(height);
EL(waviness);
EL(murkiness);
EL(windangle);
EL(fog);
EL(fogcolor);
EL(fogfactor);
EL(fogthickness);
EL(postproc);
EL(brightness);
EL(contrast);
EL(saturation);
EL(bloom);
AT(r); AT(g); AT(b);
#undef AT
#undef EL
XERO_ITER_EL(parent, element)
{
int element_name = element.GetNodeName();
XMBAttributeList attrs = element.GetAttributes();
if (element_name == el_skyset)
{
if (m_MapReader.pSkyMan)
m_MapReader.pSkyMan->SetSkySet(element.GetText().FromUTF8());
}
else if (element_name == el_suncolor)
{
m_MapReader.m_LightEnv.m_SunColor = RGBColor(
attrs.GetNamedItem(at_r).ToFloat(),
attrs.GetNamedItem(at_g).ToFloat(),
attrs.GetNamedItem(at_b).ToFloat());
}
else if (element_name == el_sunelevation)
{
m_MapReader.m_LightEnv.m_Elevation = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_sunrotation)
{
m_MapReader.m_LightEnv.m_Rotation = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_ambientcolor)
{
m_MapReader.m_LightEnv.m_AmbientColor = RGBColor(
attrs.GetNamedItem(at_r).ToFloat(),
attrs.GetNamedItem(at_g).ToFloat(),
attrs.GetNamedItem(at_b).ToFloat());
}
else if (element_name == el_fog)
{
XERO_ITER_EL(element, fog)
{
int fog_element_name = fog.GetNodeName();
if (fog_element_name == el_fogcolor)
{
XMBAttributeList fogAttributes = fog.GetAttributes();
m_MapReader.m_LightEnv.m_FogColor = RGBColor(
fogAttributes.GetNamedItem(at_r).ToFloat(),
fogAttributes.GetNamedItem(at_g).ToFloat(),
fogAttributes.GetNamedItem(at_b).ToFloat());
}
else if (fog_element_name == el_fogfactor)
{
m_MapReader.m_LightEnv.m_FogFactor = fog.GetText().ToFloat();
}
else if (fog_element_name == el_fogthickness)
{
m_MapReader.m_LightEnv.m_FogMax = fog.GetText().ToFloat();
}
}
}
else if (element_name == el_postproc)
{
XERO_ITER_EL(element, postproc)
{
int post_element_name = postproc.GetNodeName();
if (post_element_name == el_brightness)
{
m_MapReader.m_LightEnv.m_Brightness = postproc.GetText().ToFloat();
}
else if (post_element_name == el_contrast)
{
m_MapReader.m_LightEnv.m_Contrast = postproc.GetText().ToFloat();
}
else if (post_element_name == el_saturation)
{
m_MapReader.m_LightEnv.m_Saturation = postproc.GetText().ToFloat();
}
else if (post_element_name == el_bloom)
{
m_MapReader.m_LightEnv.m_Bloom = postproc.GetText().ToFloat();
}
else if (post_element_name == el_posteffect)
{
if (m_MapReader.pPostproc)
m_MapReader.pPostproc->SetPostEffect(postproc.GetText().FromUTF8());
}
}
}
else if (element_name == el_water)
{
XERO_ITER_EL(element, waterbody)
{
ENSURE(waterbody.GetNodeName() == el_waterbody);
XERO_ITER_EL(waterbody, waterelement)
{
int water_element_name = waterelement.GetNodeName();
if (water_element_name == el_height)
{
CmpPtr<ICmpWaterManager> cmpWaterManager(*m_MapReader.pSimContext, SYSTEM_ENTITY);
ENSURE(cmpWaterManager);
cmpWaterManager->SetWaterLevel(entity_pos_t::FromString(waterelement.GetText()));
continue;
}
// The rest are purely graphical effects, and should be ignored if
// graphics are disabled
if (!m_MapReader.pWaterMan)
continue;
if (water_element_name == el_type)
{
if (waterelement.GetText() == "default")
m_MapReader.pWaterMan->m_WaterType = L"ocean";
else
m_MapReader.pWaterMan->m_WaterType = waterelement.GetText().FromUTF8();
}
#define READ_COLOR(el, out) \
else if (water_element_name == el) \
{ \
XMBAttributeList colorAttrs = waterelement.GetAttributes(); \
out = CColor( \
colorAttrs.GetNamedItem(at_r).ToFloat(), \
colorAttrs.GetNamedItem(at_g).ToFloat(), \
colorAttrs.GetNamedItem(at_b).ToFloat(), \
1.f); \
}
#define READ_FLOAT(el, out) \
else if (water_element_name == el) \
{ \
out = waterelement.GetText().ToFloat(); \
} \
READ_COLOR(el_color, m_MapReader.pWaterMan->m_WaterColor)
READ_COLOR(el_tint, m_MapReader.pWaterMan->m_WaterTint)
READ_FLOAT(el_waviness, m_MapReader.pWaterMan->m_Waviness)
READ_FLOAT(el_murkiness, m_MapReader.pWaterMan->m_Murkiness)
READ_FLOAT(el_windangle, m_MapReader.pWaterMan->m_WindAngle)
#undef READ_FLOAT
#undef READ_COLOR
else
debug_warn(L"Invalid map XML data");
}
}
}
else
debug_warn(L"Invalid map XML data");
}
m_MapReader.m_LightEnv.CalculateSunDirection();
}
void CXMLReader::ReadCamera(XMBElement parent)
{
// defaults if we don't find player starting camera
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(declination);
EL(rotation);
EL(position);
AT(angle);
AT(x); AT(y); AT(z);
#undef AT
#undef EL
float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f);
CVector3D translation = CVector3D(100, 150, -100);
XERO_ITER_EL(parent, element)
{
int element_name = element.GetNodeName();
XMBAttributeList attrs = element.GetAttributes();
if (element_name == el_declination)
{
declination = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_rotation)
{
rotation = attrs.GetNamedItem(at_angle).ToFloat();
}
else if (element_name == el_position)
{
translation = CVector3D(
attrs.GetNamedItem(at_x).ToFloat(),
attrs.GetNamedItem(at_y).ToFloat(),
attrs.GetNamedItem(at_z).ToFloat());
}
else
debug_warn(L"Invalid map XML data");
}
if (m_MapReader.pGameView)
{
m_MapReader.pGameView->GetCamera()->m_Orientation.SetXRotation(declination);
m_MapReader.pGameView->GetCamera()->m_Orientation.RotateY(rotation);
m_MapReader.pGameView->GetCamera()->m_Orientation.Translate(translation);
m_MapReader.pGameView->GetCamera()->UpdateFrustum();
}
}
void CXMLReader::ReadPaths(XMBElement parent)
{
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
EL(path);
EL(rotation);
EL(node);
EL(position);
EL(target);
AT(name);
AT(timescale);
AT(orientation);
AT(mode);
AT(style);
AT(x);
AT(y);
AT(z);
AT(deltatime);
#undef EL
#undef AT
CmpPtr<ICmpCinemaManager> 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 /*parent*/)
{
}
void CXMLReader::ReadEntities(XMBElement entity, CSimulation2& sim)
{
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<entity_id_t> Garrison;
std::vector<std::pair<std::string, entity_id_t>> 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();
// <template>
if (element_name == el_template)
{
TemplateName = setting.GetText().FromUTF8();
}
// <player>
else if (element_name == el_player)
{
PlayerID = setting.GetText().ToInt();
}
// <position>
else if (element_name == el_position)
{
XMBAttributeList positionAttrs = setting.GetAttributes();
Position = CFixedVector3D(
fixed::FromString(positionAttrs.GetNamedItem(at_x)),
fixed::FromString(positionAttrs.GetNamedItem(at_y)),
fixed::FromString(positionAttrs.GetNamedItem(at_z)));
}
// <orientation>
else if (element_name == el_orientation)
{
XMBAttributeList orientationAttrs = setting.GetAttributes();
Orientation = CFixedVector3D(
fixed::FromString(orientationAttrs.GetNamedItem(at_x)),
fixed::FromString(orientationAttrs.GetNamedItem(at_y)),
fixed::FromString(orientationAttrs.GetNamedItem(at_z)));
// TODO: what happens if some attributes are missing?
}
// <obstruction>
else if (element_name == el_obstruction)
{
XMBAttributeList obstructionAttrs = setting.GetAttributes();
ControlGroup = obstructionAttrs.GetNamedItem(at_group).ToInt();
ControlGroup2 = obstructionAttrs.GetNamedItem(at_group2).ToInt();
}
// <garrison>
else if (element_name == el_garrison)
{
XMBElementList garrison = setting.GetChildNodes();
Garrison.reserve(garrison.size());
for (const XMBElement& garr_ent : garrison)
{
XMBAttributeList garrisonAttrs = garr_ent.GetAttributes();
Garrison.push_back(garrisonAttrs.GetNamedItem(at_uid).ToInt());
}
}
// <turrets>
else if (element_name == el_turrets)
{
XMBElementList turrets = setting.GetChildNodes();
Turrets.reserve(turrets.size());
for (const XMBElement& turretPoint : turrets)
{
XMBAttributeList turretAttrs = turretPoint.GetAttributes();
Turrets.emplace_back(
turretAttrs.GetNamedItem(at_turret),
turretAttrs.GetNamedItem(at_uid).ToInt()
);
}
}
// <actor>
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");
}
CmpPtr<ICmpPlayerManager> cmpPlayerManager(sim, SYSTEM_ENTITY);
entity_id_t player = cmpPlayerManager->GetPlayerByID(PlayerID);
CmpPtr<ICmpPlayer> cmpPlayer(sim, player);
// Don't add entities for removed players.
if (cmpPlayer && cmpPlayer->IsRemoved())
return;
entity_id_t ent = sim.AddEntity(TemplateName, EntityUid);
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));
return;
}
CmpPtr<ICmpPosition> cmpPosition(sim, ent);
if (cmpPosition)
{
cmpPosition->JumpTo(Position.X, Position.Z);
cmpPosition->SetYRotation(Orientation.Y);
// TODO: other parts of the position
}
if (!Garrison.empty())
{
CmpPtr<ICmpGarrisonHolder> cmpGarrisonHolder(sim, ent);
if (cmpGarrisonHolder)
cmpGarrisonHolder->SetInitEntities(std::move(Garrison));
else
LOGERROR("CXMLMapReader::ReadEntities() entity '%d' of player '%d' has no GarrisonHolder component and thus cannot garrison units.", ent, PlayerID);
}
// Needs to be before ownership changes to prevent initialising
// subunits too soon.
if (!Turrets.empty())
{
CmpPtr<ICmpTurretHolder> cmpTurretHolder(sim, ent);
if (cmpTurretHolder)
cmpTurretHolder->SetInitEntities(std::move(Turrets));
else
LOGERROR("CXMLMapReader::ReadEntities() entity '%d' of player '%d' has no TurretHolder component and thus cannot use turrets.", ent, PlayerID);
}
CmpPtr<ICmpOwnership> cmpOwnership(sim, ent);
if (cmpOwnership)
cmpOwnership->SetOwner(PlayerID);
CmpPtr<ICmpObstruction> cmpObstruction(sim, ent);
if (cmpObstruction)
{
if (ControlGroup != INVALID_ENTITY)
cmpObstruction->SetControlGroup(ControlGroup);
if (ControlGroup2 != INVALID_ENTITY)
cmpObstruction->SetControlGroup2(ControlGroup2);
cmpObstruction->ResolveFoundationCollisions();
}
CmpPtr<ICmpVisual> cmpVisual(sim, ent);
if (cmpVisual)
{
if (Seed != -1)
cmpVisual->SetActorSeed((u32)Seed);
// TODO: variation/selection strings
}
if (PlayerID == m_MapReader.m_PlayerID && (TemplateName.ends_with(L"civil_centre") || m_MapReader.m_StartingCameraTarget == INVALID_ENTITY))
{
// Focus on civil centre or first entity owned by player
m_MapReader.m_StartingCameraTarget = ent;
}
}
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 ReadXMLEntities 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");
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// load script settings from map
int CMapReader::LoadScriptSettings()
{
if (!m_XmlReader)
m_XmlReader = std::make_unique<CXMLReader>(m_FilenameXml, *this);
// parse the script settings
if (pSimulation2)
pSimulation2->SetMapSettings(m_XmlReader->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 (!m_XmlReader)
m_XmlReader = std::make_unique<CXMLReader>(m_FilenameXml, *this);
m_XmlReader->ReadXML();
return 0;
}
// progressive
PS::Loader::Task CMapReader::ReadXMLEntities()
{
ENSURE(pSimulation2);
if (m_SkipEntities)
co_return 0;
if (!m_XmlReader)
m_XmlReader = std::make_unique<CXMLReader>(m_FilenameXml, *this);
const std::size_t totalJobs{std::transform_reduce(m_XmlReader->nodes.begin(),
m_XmlReader->nodes.end(), static_cast<std::size_t>(0), std::plus<>{}, [](XMBElement node)
{
return node.GetChildNodes().size();
})};
std::size_t completedJobs{0};
for (XMBElement node : m_XmlReader->nodes)
{
CStr name = m_XmlReader->xmb_file.GetElementString(node.GetNodeName());
if (name != "Entities")
continue;
for (XMBElement child : node.GetChildNodes())
{
m_XmlReader->ReadEntities(child, *pSimulation2);
completedJobs++;
co_yield 100 * completedJobs / totalJobs;
}
}
m_XmlReader.reset();
co_return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int CMapReader::LoadRMSettings()
{
// copy random map settings over to sim
ENSURE(pSimulation2);
pSimulation2->SetMapSettings(m_ScriptSettings);
return 0;
}
[[noreturn]] void ThrowMapGenerationError()
{
throw PSERROR_Game_World_MapLoadFailed{
"Error generating random map.\nCheck application log for details."};
}
PS::Loader::Task CMapReader::RunMapGeneration(const CStrW& scriptFile)
{
std::atomic<int> progress{1};
// The settings are stringified to pass them to the task.
Future<Script::StructuredClone> task = {g_TaskManager,
[&progress, scriptFile, settings = Script::StringifyJSON(ScriptRequest{
pSimulation2->GetScriptInterface()}, &m_ScriptSettings)](const StopToken stopToken)
{
PROFILE2("Map Generation");
const VfsPath scriptPath{scriptFile.empty() ? L"" :
static_cast<std::wstring>(RANDOM_MAP_PREFIX) + scriptFile};
ScriptContext mapgenContext{MAP_GENERATION_CONTEXT_SIZE};
ScriptInterface mapgenInterface{"Engine", "MapGenerator", mapgenContext,
[](const VfsPath& path){
// Only allow to load modules inside the maps folder.
return path.string().find(RANDOM_MAP_PREFIX) == 0;
}};
return RunMapGenerationScript(stopToken, progress, mapgenInterface, scriptPath, settings);
}};
while (!task.IsDone())
{
co_yield progress.load();
co_await std::suspend_always{};
if (IsQuitRequested())
{
LOGWARNING("Quit requested!");
co_return -1;
}
}
const Script::StructuredClone results{task.Get()};
if (!results)
ThrowMapGenerationError();
// Parse data into simulation context
ScriptRequest rq(pSimulation2->GetScriptInterface());
JS::RootedValue data{rq.cx};
Script::ReadStructuredClone(rq, results, &data);
if (data.isUndefined())
ThrowMapGenerationError();
m_MapData.init(rq.cx, data);
co_return 0;
};
int CMapReader::ParseTerrain()
{
PROFILE2("ParseTerrain");
ScriptRequest rq(pSimulation2->GetScriptInterface());
// parse terrain from map data
// an error here should stop the loading process
const auto getTerrainProperty = [&](JS::HandleValue val, const char* prop, auto&& out)
{
if (Script::GetProperty(rq, val, prop, std::forward<decltype(out)>(out)))
return;
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;
getTerrainProperty(m_MapData, "size", size);
m_PatchesPerSide = size / PATCH_SIZE;
// flat heightmap of u16 data
getTerrainProperty(m_MapData, "height", m_Heightmap);
// load textures
std::vector<std::string> textureNames;
getTerrainProperty(m_MapData, "textureNames", textureNames);
for (const std::string& textureName : textureNames)
{
if (CTerrainTextureManager::IsInitialised())
{
CTerrainTextureEntry* texentry = g_TexMan.FindTexture(textureName);
m_TerrainTextures.push_back(texentry);
}
}
// build tile data
m_Tiles.resize(SQR(size));
JS::RootedValue tileData(rq.cx);
getTerrainProperty(m_MapData, "tileData", &tileData);
// parse tile data object into flat arrays
std::vector<u16> tileIndex;
std::vector<u16> tilePriority;
getTerrainProperty(tileData, "index", tileIndex);
getTerrainProperty(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;
}
}
return 0;
}
struct ParseEntitiesState
{
ScriptRequest rq;
CmpPtr<ICmpPlayerManager> cmpPlayerManager;
std::vector<Entity> entities;
size_t currentEntityIndex{0};
ParseEntitiesState(CSimulation2& sim)
: rq(sim.GetScriptInterface()), cmpPlayerManager(sim, SYSTEM_ENTITY) {}
};
PS::Loader::Task CMapReader::ParseEntities()
{
PROFILE2("ParseEntities");
CSimulation2& sim{*pSimulation2};
ScriptRequest rq{sim.GetScriptInterface()};
CmpPtr<ICmpPlayerManager> cmpPlayerManager{sim, SYSTEM_ENTITY};
std::vector<Entity> entities;
if (!Script::GetProperty(rq, m_MapData, "entities", entities))
LOGWARNING("CMapReader::ParseEntities() failed to get 'entities' property");
for (std::size_t index{0}; index != entities.size(); ++index)
{
co_yield Clamp<int>(index * 80 / entities.size(), 20, 100);
const Entity& currEnt{entities[index]};
// Get current entity struct
entity_id_t player = cmpPlayerManager->GetPlayerByID(currEnt.playerID);
CmpPtr<ICmpPlayer> cmpPlayer(sim, player);
// Don't add entities for removed players.
if (cmpPlayer && cmpPlayer->IsRemoved())
continue;
entity_id_t ent = sim.AddEntity(currEnt.templateName, currEnt.entityID);
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<ICmpPosition> 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<ICmpOwnership> 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<ICmpObstruction> cmpObstruction(sim, ent);
if (cmpObstruction)
cmpObstruction->ResolveFoundationCollisions();
if (currEnt.playerID == m_PlayerID && (currEnt.templateName.ends_with(L"civil_centre") || m_StartingCameraTarget == INVALID_ENTITY))
{
// Focus on civil centre or first entity owned by player
m_StartingCameraTarget = currEnt.entityID;
}
}
}
co_return 0;
}
int CMapReader::ParseEnvironment()
{
// parse environment settings from map data
ScriptRequest rq(pSimulation2->GetScriptInterface());
const auto getEnvironmentProperty = [&](JS::HandleValue val, const char* prop, auto&& out)
{
if (!Script::GetProperty(rq, val, prop, std::forward<decltype(out)>(out)))
LOGWARNING("CMapReader::ParseEnvironment() failed to get '%s' property", prop);
};
JS::RootedValue envObj(rq.cx);
getEnvironmentProperty(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;
getEnvironmentProperty(envObj, "SkySet", skySet);
if (pSkyMan)
pSkyMan->SetSkySet(skySet);
CColor sunColor;
getEnvironmentProperty(envObj, "SunColor", sunColor);
m_LightEnv.m_SunColor = RGBColor(sunColor.r, sunColor.g, sunColor.b);
getEnvironmentProperty(envObj, "SunElevation", m_LightEnv.m_Elevation);
getEnvironmentProperty(envObj, "SunRotation", m_LightEnv.m_Rotation);
CColor ambientColor;
getEnvironmentProperty(envObj, "AmbientColor", ambientColor);
m_LightEnv.m_AmbientColor = RGBColor(ambientColor.r, ambientColor.g, ambientColor.b);
// Water properties
JS::RootedValue waterObj(rq.cx);
getEnvironmentProperty(envObj, "Water", &waterObj);
JS::RootedValue waterBodyObj(rq.cx);
getEnvironmentProperty(waterObj, "WaterBody", &waterBodyObj);
// Water level - necessary
float waterHeight;
getEnvironmentProperty(waterBodyObj, "Height", waterHeight);
CmpPtr<ICmpWaterManager> cmpWaterManager(*pSimulation2, SYSTEM_ENTITY);
ENSURE(cmpWaterManager);
cmpWaterManager->SetWaterLevel(entity_pos_t::FromFloat(waterHeight));
// If we have graphics, get rest of settings
if (pWaterMan)
{
getEnvironmentProperty(waterBodyObj, "Type", pWaterMan->m_WaterType);
if (pWaterMan->m_WaterType == L"default")
pWaterMan->m_WaterType = L"ocean";
getEnvironmentProperty(waterBodyObj, "Color", pWaterMan->m_WaterColor);
getEnvironmentProperty(waterBodyObj, "Tint", pWaterMan->m_WaterTint);
getEnvironmentProperty(waterBodyObj, "Waviness", pWaterMan->m_Waviness);
getEnvironmentProperty(waterBodyObj, "Murkiness", pWaterMan->m_Murkiness);
getEnvironmentProperty(waterBodyObj, "WindAngle", pWaterMan->m_WindAngle);
}
JS::RootedValue fogObject(rq.cx);
getEnvironmentProperty(envObj, "Fog", &fogObject);
getEnvironmentProperty(fogObject, "FogFactor", m_LightEnv.m_FogFactor);
getEnvironmentProperty(fogObject, "FogThickness", m_LightEnv.m_FogMax);
CColor fogColor;
getEnvironmentProperty(fogObject, "FogColor", fogColor);
m_LightEnv.m_FogColor = RGBColor(fogColor.r, fogColor.g, fogColor.b);
JS::RootedValue postprocObject(rq.cx);
getEnvironmentProperty(envObj, "Postproc", &postprocObject);
std::wstring postProcEffect;
getEnvironmentProperty(postprocObject, "PostprocEffect", postProcEffect);
if (pPostproc)
pPostproc->SetPostEffect(postProcEffect);
getEnvironmentProperty(postprocObject, "Brightness", m_LightEnv.m_Brightness);
getEnvironmentProperty(postprocObject, "Contrast", m_LightEnv.m_Contrast);
getEnvironmentProperty(postprocObject, "Saturation", m_LightEnv.m_Saturation);
getEnvironmentProperty(postprocObject, "Bloom", m_LightEnv.m_Bloom);
m_LightEnv.CalculateSunDirection();
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);
const auto getCameraProperty = [&](JS::HandleValue val, const char* prop, auto&& out)
{
if (!Script::GetProperty(rq, val, prop, std::forward<decltype(out)>(out)))
LOGWARNING("CMapReader::ParseCamera() failed to get '%s' property", prop);
};
JS::RootedValue cameraObj(rq.cx);
getCameraProperty(m_MapData, "Camera", &cameraObj);
if (!cameraObj.isUndefined())
{ // If camera property exists, read values
CFixedVector3D pos;
getCameraProperty(cameraObj, "Position", pos);
translation = pos;
getCameraProperty(cameraObj, "Rotation", rotation);
getCameraProperty(cameraObj, "Declination", declination);
}
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() = default;