From b9973952faacb20e5e59d4330fb052034193a27a Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 30 Jun 2026 18:49:22 +0200 Subject: [PATCH] rttr:AddTerrain for MyMap.lua --- libs/libGamedata/lua/GameDataLoader.cpp | 47 +++++++++++++++++++++++-- libs/libGamedata/lua/GameDataLoader.h | 8 +++++ libs/rttrConfig/src/RttrConfig.cpp | 5 +++ libs/rttrConfig/src/RttrConfig.h | 2 ++ libs/s25main/lua/LuaInterfaceGame.cpp | 39 ++++++++++++++++++++ libs/s25main/lua/LuaInterfaceGame.h | 7 ++++ libs/s25main/world/MapLoader.cpp | 5 +-- 7 files changed, 108 insertions(+), 5 deletions(-) diff --git a/libs/libGamedata/lua/GameDataLoader.cpp b/libs/libGamedata/lua/GameDataLoader.cpp index e034d54060..bd32e8fb4c 100644 --- a/libs/libGamedata/lua/GameDataLoader.cpp +++ b/libs/libGamedata/lua/GameDataLoader.cpp @@ -96,19 +96,60 @@ void GameDataLoader::Include(const std::string& filepath) } } +void addLandscape(WorldDescription& worldDesc, const kaguya::LuaTable& data) +{ + worldDesc.landscapes.add(LandscapeDesc(CheckedLuaTable(data), worldDesc)); +} + +void addTerrainEdge(WorldDescription& worldDesc, const kaguya::LuaTable& data) +{ + worldDesc.edges.add(EdgeDesc(CheckedLuaTable(data), worldDesc)); +} + +void addTerrain(WorldDescription& worldDesc, const kaguya::LuaTable& data) +{ + TerrainDesc terrain(CheckedLuaTable(data), worldDesc); + + // Validate s2Id + if(terrain.s2Id != 0xFF) + { + // Bit 6 (0x40) is the harbour flag (libsiedler2::HARBOR_MASK) in the S2 map format. + // It is a per-node flag in the map file, not part of the terrain identity. + if(terrain.s2Id & 0x40) + { + throw GameDataLoadError( + helpers::format("Terrain '%1%' has s2Id 0x%2$x with the harbour bit (0x40) set. " + "This bit is reserved for per-node map data (libsiedler2::HARBOR_MASK) " + "and must not be used as part of the terrain ID.", + terrain.name, terrain.s2Id)); + } + // Check that no other terrain with the same s2Id + landscape combination exists. + if(worldDesc.terrain.find([s2Id = terrain.s2Id, landscape = terrain.landscape](const TerrainDesc& t) { + return t.s2Id == s2Id && t.landscape == landscape; + })) + { + throw GameDataLoadError(helpers::format("Duplicate s2Id 0x%1$x for landscape '%2%' in terrain '%3%'", + terrain.s2Id, worldDesc.landscapes.get(terrain.landscape).name, + terrain.name)); + } + } + + worldDesc.terrain.add(std::move(terrain)); +} + void GameDataLoader::AddLandscape(const kaguya::LuaTable& data) { - worldDesc_.landscapes.add(LandscapeDesc(data, worldDesc_)); + addLandscape(worldDesc_, data); } void GameDataLoader::AddTerrainEdge(const kaguya::LuaTable& data) { - worldDesc_.edges.add(EdgeDesc(data, worldDesc_)); + addTerrainEdge(worldDesc_, data); } void GameDataLoader::AddTerrain(const kaguya::LuaTable& data) { - worldDesc_.terrain.add(TerrainDesc(data, worldDesc_)); + addTerrain(worldDesc_, data); } void loadGameData(WorldDescription& worldDesc) diff --git a/libs/libGamedata/lua/GameDataLoader.h b/libs/libGamedata/lua/GameDataLoader.h index 01967574d4..8bb6adbf30 100644 --- a/libs/libGamedata/lua/GameDataLoader.h +++ b/libs/libGamedata/lua/GameDataLoader.h @@ -35,4 +35,12 @@ class GameDataLoader : public LuaInterfaceBase int curIncludeDepth_; }; +/// @name Shared helpers for adding world description items from Lua tables +/// These can be used by both GameDataLoader (game data init) and LuaInterfaceGame (companion scripts). +/// \{ +void addTerrain(WorldDescription& worldDesc, const kaguya::LuaTable& data); +void addLandscape(WorldDescription& worldDesc, const kaguya::LuaTable& data); +void addTerrainEdge(WorldDescription& worldDesc, const kaguya::LuaTable& data); +/// \} + void loadGameData(WorldDescription& worldDesc); diff --git a/libs/rttrConfig/src/RttrConfig.cpp b/libs/rttrConfig/src/RttrConfig.cpp index b42be52aa5..744a766711 100644 --- a/libs/rttrConfig/src/RttrConfig.cpp +++ b/libs/rttrConfig/src/RttrConfig.cpp @@ -121,6 +121,11 @@ void RttrConfig::overridePathMapping(const std::string& id, const boost::filesys pathMappings[id] = path; } +void RttrConfig::addPathMapping(const std::string& id, const boost::filesystem::path& path) +{ + pathMappings[id] = path; +} + bool RttrConfig::Init() { prefixPath_ = GetPrefixPath(); diff --git a/libs/rttrConfig/src/RttrConfig.h b/libs/rttrConfig/src/RttrConfig.h index 8589fde9b9..70048ebb17 100644 --- a/libs/rttrConfig/src/RttrConfig.h +++ b/libs/rttrConfig/src/RttrConfig.h @@ -27,6 +27,8 @@ class RttrConfig : public Singleton boost::filesystem::path ExpandPath(const std::string& path) const; /// Overwrite a given path mapping void overridePathMapping(const std::string& id, const boost::filesystem::path& path); + /// Register a new path mapping (used for dynamic prefixes like ) + void addPathMapping(const std::string& id, const boost::filesystem::path& path); }; #define RTTRCONFIG RttrConfig::inst() diff --git a/libs/s25main/lua/LuaInterfaceGame.cpp b/libs/s25main/lua/LuaInterfaceGame.cpp index 3ae2d0623c..f38cf0960c 100644 --- a/libs/s25main/lua/LuaInterfaceGame.cpp +++ b/libs/s25main/lua/LuaInterfaceGame.cpp @@ -5,10 +5,12 @@ #include "LuaInterfaceGame.h" #include "EventManager.h" #include "Game.h" +#include "RttrConfig.h" #include "WindowManager.h" #include "ai/AIInterface.h" #include "ai/AIPlayer.h" #include "ingameWindows/iwMissionStatement.h" +#include "lua/GameDataLoader.h" #include "lua/LuaHelpers.h" #include "lua/LuaPlayer.h" #include "lua/LuaWorld.h" @@ -206,6 +208,7 @@ KAGUYA_MEMBER_FUNCTION_OVERLOADS(SetMissionGoalWrapper, LuaInterfaceGame, SetMis void LuaInterfaceGame::Register(kaguya::State& state) { state["RTTRGame"].setClass(kaguya::UserdataMetatable() + .addFunction("AddTerrain", &LuaInterfaceGame::AddTerrain) .addFunction("ClearResources", &LuaInterfaceGame::ClearResources) .addFunction("GetGF", &LuaInterfaceGame::GetGF) .addFunction("FormatNumGFs", &LuaInterfaceGame::FormatNumGFs) @@ -259,6 +262,42 @@ bool LuaInterfaceGame::Deserialize(Serializer& luaSaveState) return true; } +void LuaInterfaceGame::SetMapDir(const boost::filesystem::path& mapDir) +{ + mapDir_ = mapDir; + RTTRCONFIG.addPathMapping("MAP", mapDir); +} + +void LuaInterfaceGame::AddTerrain(const kaguya::LuaTable& data) +{ + if(!mapDir_.empty()) + { + // Resolve relative texture paths to /filename + kaguya::LuaRef texRef = data["texture"]; + if(texRef.type() == LUA_TSTRING) + { + std::string texPath = texRef; + if(texPath.find(" prefix so validatePath and ExpandPath handle it naturally + kaguya::LuaTable mutableData = data; + mutableData["texture"] = std::string("/") + texPath; + } + } + } + addTerrain(gw.GetDescriptionWriteable(), data); +} + void LuaInterfaceGame::ClearResources() { for(unsigned p = 0; p < gw.GetNumPlayers(); p++) diff --git a/libs/s25main/lua/LuaInterfaceGame.h b/libs/s25main/lua/LuaInterfaceGame.h index e71758adcb..4f280aa598 100644 --- a/libs/s25main/lua/LuaInterfaceGame.h +++ b/libs/s25main/lua/LuaInterfaceGame.h @@ -7,6 +7,7 @@ #include "LuaInterfaceGameBase.h" #include "gameTypes/MapCoordinates.h" #include "gameTypes/PactTypes.h" +#include #include #include @@ -45,7 +46,12 @@ class LuaInterfaceGame : public LuaInterfaceGameBase // called if pact was created void EventPactCreated(PactType pt, unsigned char suggestedByPlayerId, unsigned char targetPlayerId, unsigned duration); + /// Set the directory from which relative texture paths in AddTerrain are resolved. + /// Registers so that ExpandPath and validatePath can resolve it. + void SetMapDir(const boost::filesystem::path& mapDir); + // Callable from Lua + void AddTerrain(const kaguya::LuaTable& data); void ClearResources(); unsigned GetGF() const; std::string FormatNumGFs(unsigned numGFs) const; @@ -63,6 +69,7 @@ class LuaInterfaceGame : public LuaInterfaceGameBase ILocalGameState& localGameState; GameWorld& gw; Game& game; + boost::filesystem::path mapDir_; ///< Base dir for resolving relative texture paths in AddTerrain LuaPlayer GetPlayer(int playerIdx); LuaWorld GetWorld(); }; diff --git a/libs/s25main/world/MapLoader.cpp b/libs/s25main/world/MapLoader.cpp index b1c8572ea3..ffe8af89a9 100644 --- a/libs/s25main/world/MapLoader.cpp +++ b/libs/s25main/world/MapLoader.cpp @@ -94,6 +94,7 @@ bool MapLoader::LoadLuaScript(Game& game, ILocalGameState& localgameState, const if(!bfs::exists(luaFilePath)) return false; auto lua = std::make_unique(game, localgameState); + lua->SetMapDir(luaFilePath.parent_path()); if(!lua->loadScript(luaFilePath) || !lua->CheckScriptVersion()) return false; game.SetLua(std::move(lua)); @@ -254,8 +255,8 @@ bool MapLoader::InitNodes(const libsiedler2::ArchivItem_Map& map, Exploration ex // Will be set later node.harborId.reset(); - node.t1 = getTerrainFromS2(t1 & 0x3F); // Only lower 6 bits - node.t2 = getTerrainFromS2(t2 & 0x3F); // Only lower 6 bits + node.t1 = getTerrainFromS2(t1 & ~0x40); // Clear harbour bit (bit 6) + node.t2 = getTerrainFromS2(t2 & ~0x40); // Clear harbour bit (bit 6) if(!node.t1 || !node.t2) return false;