/* Copyright (C) 2024 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 "MessageHandler.h"
#include "../CommandProc.h"
#include "../GameLoop.h"
#include "../MessagePasser.h"
#include "graphics/GameView.h"
#include "graphics/LOSTexture.h"
#include "graphics/MapIO.h"
#include "graphics/MapWriter.h"
#include "graphics/MiniMapTexture.h"
#include "graphics/Patch.h"
#include "graphics/Terrain.h"
#include "graphics/TerrainTextureEntry.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/bits.h"
#include "lib/file/vfs/vfs_path.h"
#include "lib/status.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/Loader.h"
#include "ps/World.h"
#include "renderer/Renderer.h"
#include "renderer/SceneRenderer.h"
#include "renderer/WaterManager.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/JSON.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpOwnership.h"
#include "simulation2/components/ICmpPlayer.h"
#include "simulation2/components/ICmpPlayerManager.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/components/ICmpVisual.h"
#include "simulation2/system/ParamNode.h"
#include
#include
#include
#ifdef _MSC_VER
# pragma warning(disable: 4458) // Declaration hides class member.
#endif
namespace
{
void InitGame()
{
if (g_Game)
{
delete g_Game;
g_Game = NULL;
}
g_Game = new CGame(false);
// Default to player 1 for playtesting
g_Game->SetPlayerID(1);
}
void StartGame(JS::MutableHandleValue attrs)
{
g_Game->StartGame(attrs, "");
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();
// Disable fog-of-war - this must be done before starting the game,
// as visual actors cache their visibility state on first render.
CmpPtr cmpRangeManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY);
if (cmpRangeManager)
cmpRangeManager->SetLosRevealAll(-1, true);
PSRETURN ret = g_Game->ReallyStartGame();
ENSURE(ret == PSRETURN_OK);
}
}
namespace AtlasMessage {
QUERYHANDLER(GenerateMap)
{
try
{
InitGame();
// Random map
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue settings(rq.cx);
Script::ParseJSON(rq, *msg->settings, &settings);
Script::SetProperty(rq, settings, "mapType", "random");
JS::RootedValue attrs(rq.cx);
Script::CreateObject(
rq,
&attrs,
"mapType", "random",
"script", *msg->filename,
"settings", settings);
StartGame(&attrs);
msg->status = 0;
}
catch (PSERROR_Game_World_MapLoadFailed&)
{
// Cancel loading
LDR_Cancel();
// Since map generation failed and we don't know why, use the blank map as a fallback
InitGame();
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
// Set up 8-element array of empty objects to satisfy init
JS::RootedValue playerData(rq.cx);
Script::CreateArray(rq, &playerData);
for (int i = 0; i < 8; ++i)
{
JS::RootedValue player(rq.cx);
Script::CreateObject(rq, &player);
Script::SetPropertyInt(rq, playerData, i, player);
}
JS::RootedValue settings(rq.cx);
Script::CreateObject(
rq,
&settings,
"mapType", "scenario",
"PlayerData", playerData);
JS::RootedValue attrs(rq.cx);
Script::CreateObject(
rq,
&attrs,
"mapType", "scenario",
"map", "maps/scenarios/_default",
"settings", settings);
StartGame(&attrs);
msg->status = -1;
}
}
MESSAGEHANDLER(LoadMap)
{
InitGame();
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
// Scenario
CStrW map = *msg->filename;
CStrW mapBase = map.BeforeLast(L".pmp"); // strip the file extension, if any
JS::RootedValue attrs(rq.cx);
Script::CreateObject(
rq,
&attrs,
"mapType", "scenario",
"map", mapBase);
StartGame(&attrs);
}
MESSAGEHANDLER(ImportHeightmap)
{
std::vector heightmap_source;
if (LoadHeightmapImageOs(*msg->filename, heightmap_source) != INFO::OK)
{
LOGERROR("Failed to decode heightmap.");
return;
}
// resize terrain to heightmap size
// Notice that the number of tiles/pixels per side of the heightmap image is
// one less than the number of vertices per side of the heightmap.
CTerrain& terrain = g_Game->GetWorld()->GetTerrain();
const ssize_t newSize = (sqrt(heightmap_source.size()) - 1) / PATCH_SIZE;
const ssize_t offset = (newSize - terrain.GetPatchesPerSide()) / 2;
terrain.ResizeAndOffset(newSize, offset, offset);
// copy heightmap data into map
u16* const heightmap = g_Game->GetWorld()->GetTerrain().GetHeightMap();
ENSURE(heightmap_source.size() == (std::size_t) SQR(g_Game->GetWorld()->GetTerrain().GetVerticesPerSide()));
std::copy(heightmap_source.begin(), heightmap_source.end(), heightmap);
// update simulation
CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY);
if (cmpTerrain)
cmpTerrain->ReloadTerrain();
g_Game->GetView()->GetLOSTexture().MakeDirty();
}
MESSAGEHANDLER(SaveMap)
{
CMapWriter writer;
VfsPath pathname = VfsPath(*msg->filename).ChangeExtension(L".pmp");
writer.SaveMap(pathname,
&g_Game->GetWorld()->GetTerrain(),
&g_Renderer.GetSceneRenderer().GetWaterManager(), &g_Renderer.GetSceneRenderer().GetSkyManager(),
&g_LightEnv, g_Game->GetView()->GetCamera(), g_Game->GetView()->GetCinema(),
&g_Renderer.GetPostprocManager(),
g_Game->GetSimulation2());
}
QUERYHANDLER(GetMapSettings)
{
msg->settings = g_Game->GetSimulation2()->GetMapSettingsString();
}
BEGIN_COMMAND(SetMapSettings)
{
std::string m_OldSettings, m_NewSettings;
void SetSettings(const std::string& settings)
{
g_Game->GetSimulation2()->SetMapSettings(settings);
}
void Do()
{
m_OldSettings = g_Game->GetSimulation2()->GetMapSettingsString();
m_NewSettings = *msg->settings;
SetSettings(m_NewSettings);
}
// TODO: we need some way to notify the Atlas UI when the settings are changed
// externally, otherwise this will have no visible effect
void Undo()
{
// SetSettings(m_OldSettings);
}
void Redo()
{
// SetSettings(m_NewSettings);
}
void MergeIntoPrevious(cSetMapSettings* prev)
{
prev->m_NewSettings = m_NewSettings;
}
};
END_COMMAND(SetMapSettings)
MESSAGEHANDLER(LoadPlayerSettings)
{
g_Game->GetSimulation2()->LoadPlayerSettings(msg->newplayers);
}
QUERYHANDLER(GetMapSizes)
{
msg->sizes = g_Game->GetSimulation2()->GetMapSizes();
}
QUERYHANDLER(RasterizeMinimap)
{
// TODO: remove the code duplication of the rasterization algorithm, using
// CMinimap version.
const CTerrain& terrain = g_Game->GetWorld()->GetTerrain();
const ssize_t dimension = terrain.GetVerticesPerSide() - 1;
const ssize_t bpp = 24;
const ssize_t imageDataSize = dimension * dimension * (bpp / 8);
std::vector imageBytes(imageDataSize);
float shallowPassageHeight = CMiniMapTexture::GetShallowPassageHeight();
ssize_t w = dimension;
ssize_t h = dimension;
const float waterHeight = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight;
for (ssize_t j = 0; j < h; ++j)
{
// Work backwards to vertically flip the image.
ssize_t position = 3 * (h - j - 1) * dimension;
for (ssize_t i = 0; i < w; ++i)
{
const float avgHeight = (terrain.GetVertexGroundLevel(i, j)
+ terrain.GetVertexGroundLevel(i + 1, j)
+ terrain.GetVertexGroundLevel(i, j + 1)
+ terrain.GetVertexGroundLevel(i + 1, j + 1)
) / 4.0f;
if (avgHeight < waterHeight && avgHeight > waterHeight - shallowPassageHeight)
{
// shallow water
imageBytes[position++] = 0x70;
imageBytes[position++] = 0x98;
imageBytes[position++] = 0xc0;
}
else if (avgHeight < waterHeight)
{
// Set water as constant color for consistency on different maps
imageBytes[position++] = 0x50;
imageBytes[position++] = 0x78;
imageBytes[position++] = 0xa0;
}
else
{
u32 color = std::numeric_limits::max();
const u32 hmap = static_cast(terrain.GetHeightMap()[j * dimension + i]) >> 8;
float scale = hmap / 3.0f + 170.0f / 255.0f;
CMiniPatch* const mp = terrain.GetTile(i, j);
if (mp)
{
CTerrainTextureEntry* tex = mp->GetTextureEntry();
if (tex)
color = tex->GetBaseColor();
}
// Convert
imageBytes[position++] = static_cast(static_cast(color & 0xff) * scale);
imageBytes[position++] = static_cast(static_cast((color >> 8) & 0xff) * scale);
imageBytes[position++] = static_cast(static_cast((color >> 16) & 0xff) * scale);
}
}
}
msg->imageBytes = std::move(imageBytes);
msg->dimension = dimension;
}
QUERYHANDLER(GetRMSData)
{
msg->data = g_Game->GetSimulation2()->GetRMSData();
}
QUERYHANDLER(ExpandBiomes)
{
std::vector unexpandedBiomes = *msg->biomes;
std::vector expandedBiomes;
for (const std::string& toExpand : unexpandedBiomes)
{
if (toExpand.empty())
{
LOGERROR("Got an empty biome");
continue;
}
if (toExpand.back() != '/')
{
expandedBiomes.push_back(toExpand);
continue;
}
VfsPaths biomesList;
if (vfs::GetPathnames(g_VFS, "maps/random/rmbiome/" + toExpand, L"*.json", biomesList) ==
INFO::OK)
{
std::transform(biomesList.begin(), biomesList.end(), std::back_inserter(expandedBiomes),
[&](const VfsPath& biome)
{
return toExpand + biome.Basename().string8();
});
}
else
LOGERROR("Error reading biome files in %s", toExpand);
}
msg->biomes = std::move(expandedBiomes);
}
QUERYHANDLER(GetCurrentMapSize)
{
msg->size = g_Game->GetWorld()->GetTerrain().GetTilesPerSide();
}
BEGIN_COMMAND(ResizeMap)
{
bool Within(const CFixedVector3D& pos, const int centerX, const int centerZ, const int radius)
{
int dx = abs(pos.X.ToInt_RoundToZero() - centerX);
if (dx > radius)
return false;
int dz = abs(pos.Z.ToInt_RoundToZero() - centerZ);
if (dz > radius)
return false;
if (dx + dz <= radius)
return true;
return dx * dx + dz * dz <= radius * radius;
}
struct DeletedObject
{
entity_id_t entityId;
CStr templateName;
player_id_t owner;
CFixedVector3D pos;
CFixedVector3D rot;
u32 actorSeed;
};
ssize_t m_OldPatches, m_NewPatches;
int m_OffsetX, m_OffsetY;
u16* m_Heightmap;
CPatch* m_Patches;
std::vector m_DeletedObjects;
std::vector> m_OldPositions;
std::vector> m_NewPositions;
cResizeMap() : m_Heightmap(nullptr), m_Patches(nullptr)
{
}
~cResizeMap()
{
delete[] m_Heightmap;
delete[] m_Patches;
}
void MakeDirty()
{
CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY);
if (cmpTerrain)
cmpTerrain->ReloadTerrain();
// The LOS texture won't normally get updated when running Atlas
// (since there's no simulation updates), so explicitly dirty it
g_Game->GetView()->GetLOSTexture().MakeDirty();
}
void ResizeTerrain(ssize_t patches, int offsetX, int offsetY)
{
CTerrain& terrain = g_Game->GetWorld()->GetTerrain();
terrain.ResizeAndOffset(patches, -offsetX, -offsetY);
}
void DeleteObjects(const std::vector& deletedObjects)
{
for (const DeletedObject& deleted : deletedObjects)
g_Game->GetSimulation2()->DestroyEntity(deleted.entityId);
g_Game->GetSimulation2()->FlushDestroyedEntities();
}
void RestoreObjects(const std::vector& deletedObjects)
{
CSimulation2& sim = *g_Game->GetSimulation2();
for (const DeletedObject& deleted : deletedObjects)
{
entity_id_t ent = sim.AddEntity(deleted.templateName.FromUTF8(), deleted.entityId);
if (ent == INVALID_ENTITY)
{
LOGERROR("Failed to load entity template '%s'", deleted.templateName.c_str());
}
else
{
CmpPtr cmpPosition(sim, deleted.entityId);
if (cmpPosition)
{
cmpPosition->JumpTo(deleted.pos.X, deleted.pos.Z);
cmpPosition->SetXZRotation(deleted.rot.X, deleted.rot.Z);
cmpPosition->SetYRotation(deleted.rot.Y);
}
CmpPtr cmpOwnership(sim, deleted.entityId);
if (cmpOwnership)
cmpOwnership->SetOwner(deleted.owner);
CmpPtr cmpVisual(sim, deleted.entityId);
if (cmpVisual)
cmpVisual->SetActorSeed(deleted.actorSeed);
}
}
}
void SetMovedEntitiesPosition(const std::vector>& movedObjects)
{
for (const std::pair& obj : movedObjects)
{
const entity_id_t id = obj.first;
const CFixedVector3D position = obj.second;
CmpPtr cmpPosition(*g_Game->GetSimulation2(), id);
ENSURE(cmpPosition);
cmpPosition->JumpTo(position.X, position.Z);
}
}
void Do()
{
CSimulation2& sim = *g_Game->GetSimulation2();
CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY);
ENSURE(cmpTemplateManager);
CmpPtr cmpTerrain(sim, SYSTEM_ENTITY);
if (!cmpTerrain)
{
m_OldPatches = m_NewPatches = 0;
m_OffsetX = m_OffsetY = 0;
}
else
{
m_OldPatches = static_cast(cmpTerrain->GetTilesPerSide() / PATCH_SIZE);
m_NewPatches = msg->tiles / PATCH_SIZE;
m_OffsetX = msg->offsetX / PATCH_SIZE;
// Need to flip direction of vertical offset, due to screen mapping order.
m_OffsetY = -(msg->offsetY / PATCH_SIZE);
CTerrain* terrain = cmpTerrain->GetCTerrain();
m_Heightmap = new u16[(m_OldPatches * PATCH_SIZE + 1) * (m_OldPatches * PATCH_SIZE + 1)];
std::copy_n(terrain->GetHeightMap(), (m_OldPatches * PATCH_SIZE + 1) * (m_OldPatches * PATCH_SIZE + 1), m_Heightmap);
m_Patches = new CPatch[m_OldPatches * m_OldPatches];
for (ssize_t j = 0; j < m_OldPatches; ++j)
for (ssize_t i = 0; i < m_OldPatches; ++i)
{
CPatch& src = *(terrain->GetPatch(i, j));
CPatch& dst = m_Patches[j * m_OldPatches + i];
std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]);
}
}
const int radiusInTerrainUnits = m_NewPatches * PATCH_SIZE * TERRAIN_TILE_SIZE / 2 * (1.f - 1e-6f);
// Opposite direction offset, as we move the destination onto the source, not the source into the destination.
const int mapCenterX = (m_OldPatches / 2 - m_OffsetX) * PATCH_SIZE * TERRAIN_TILE_SIZE;
const int mapCenterZ = (m_OldPatches / 2 - m_OffsetY) * PATCH_SIZE * TERRAIN_TILE_SIZE;
// The offset to move units by is opposite the direction the map is moved, and from the corner.
const int offsetX = ((m_NewPatches - m_OldPatches) / 2 + m_OffsetX) * PATCH_SIZE * TERRAIN_TILE_SIZE;
const int offsetZ = ((m_NewPatches - m_OldPatches) / 2 + m_OffsetY) * PATCH_SIZE * TERRAIN_TILE_SIZE;
const CFixedVector3D offset = CFixedVector3D(fixed::FromInt(offsetX), fixed::FromInt(0), fixed::FromInt(offsetZ));
const CSimulation2::InterfaceListUnordered& ents = sim.GetEntitiesWithInterfaceUnordered(IID_Selectable);
for (const std::pair& ent : ents)
{
const entity_id_t entityId = ent.first;
CmpPtr cmpPosition(sim, entityId);
if (cmpPosition && cmpPosition->IsInWorld() && Within(cmpPosition->GetPosition(), mapCenterX, mapCenterZ, radiusInTerrainUnits))
{
CFixedVector3D position = cmpPosition->GetPosition();
m_NewPositions.emplace_back(entityId, position + offset);
m_OldPositions.emplace_back(entityId, position);
}
else
{
DeletedObject deleted;
deleted.entityId = entityId;
deleted.templateName = cmpTemplateManager->GetCurrentTemplateName(entityId);
// If the entity has a position, but the ending position is not valid;
if (cmpPosition)
{
deleted.pos = cmpPosition->GetPosition();
deleted.rot = cmpPosition->GetRotation();
}
CmpPtr cmpOwnership(sim, entityId);
if (cmpOwnership)
deleted.owner = cmpOwnership->GetOwner();
CmpPtr cmpVisual(sim, deleted.entityId);
if (cmpVisual)
deleted.actorSeed = cmpVisual->GetActorSeed();
m_DeletedObjects.push_back(deleted);
}
}
DeleteObjects(m_DeletedObjects);
ResizeTerrain(m_NewPatches, m_OffsetX, m_OffsetY);
SetMovedEntitiesPosition(m_NewPositions);
MakeDirty();
}
void Undo()
{
if (m_Heightmap == nullptr || m_Patches == nullptr)
{
// If there previously was no data, just resize to old (probably not originally valid).
ResizeTerrain(m_OldPatches, -m_OffsetX, -m_OffsetY);
}
else
{
CSimulation2& sim = *g_Game->GetSimulation2();
CmpPtr cmpTerrain(sim, SYSTEM_ENTITY);
CTerrain* terrain = cmpTerrain->GetCTerrain();
terrain->Initialize(m_OldPatches, m_Heightmap);
// Copy terrain data back.
for (ssize_t j = 0; j < m_OldPatches; ++j)
for (ssize_t i = 0; i < m_OldPatches; ++i)
{
CPatch& src = m_Patches[j * m_OldPatches + i];
CPatch& dst = *(terrain->GetPatch(i, j));
std::copy_n(&src.m_MiniPatches[0][0], PATCH_SIZE * PATCH_SIZE, &dst.m_MiniPatches[0][0]);
}
}
RestoreObjects(m_DeletedObjects);
SetMovedEntitiesPosition(m_OldPositions);
MakeDirty();
}
void Redo()
{
DeleteObjects(m_DeletedObjects);
ResizeTerrain(m_NewPatches, m_OffsetX, m_OffsetY);
SetMovedEntitiesPosition(m_NewPositions);
MakeDirty();
}
};
END_COMMAND(ResizeMap)
QUERYHANDLER(VFSFileExists)
{
msg->exists = VfsFileExists(*msg->path);
}
QUERYHANDLER(VFSFileRealPath)
{
VfsPath pathname(*msg->path);
if (pathname.empty())
return;
OsPath realPathname;
if (g_VFS->GetRealPath(pathname, realPathname) == INFO::OK)
msg->realPath = realPathname.string();
}
static Status AddToFilenames(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData)
{
std::vector& filenames = *(std::vector*)cbData;
filenames.push_back(pathname.string().c_str());
return INFO::OK;
}
QUERYHANDLER(GetMapList)
{
#define GET_FILE_LIST(path, list) \
std::vector list; \
vfs::ForEachFile(g_VFS, path, AddToFilenames, (uintptr_t)&list, L"*.xml", vfs::DIR_RECURSIVE); \
msg->list = list;
GET_FILE_LIST(L"maps/scenarios/", scenarioFilenames);
GET_FILE_LIST(L"maps/skirmishes/", skirmishFilenames);
GET_FILE_LIST(L"maps/tutorials/", tutorialFilenames);
#undef GET_FILE_LIST
}
QUERYHANDLER(GetVictoryConditionData)
{
msg->data = g_Game->GetSimulation2()->GetVictoryConditiondData();
}
}