diff --git a/extras/ai-battle/HeadlessGame.cpp b/extras/ai-battle/HeadlessGame.cpp index e4aedbdbd8..86c544ca25 100644 --- a/extras/ai-battle/HeadlessGame.cpp +++ b/extras/ai-battle/HeadlessGame.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org) +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) // // SPDX-License-Identifier: GPL-2.0-or-later @@ -13,6 +13,7 @@ #include "world/MapLoader.h" #include "gameTypes/MapInfo.h" #include "gameData/GameConsts.h" +#include "s25util/colors.h" #include #include #include @@ -27,6 +28,7 @@ std::string HumanReadableNumber(unsigned num); namespace bfs = boost::filesystem; namespace bnw = boost::nowide; + using bfs::canonical; #ifdef WIN32 @@ -41,13 +43,24 @@ void printConsole(const char* fmt, ...) __attribute__((format(printf, 1, 2))); void printConsole(const char* fmt, ...); #endif -HeadlessGame::HeadlessGame(const GlobalGameSettings& ggs, const bfs::path& map, const std::vector& ais) +HeadlessGame::HeadlessGame(const GlobalGameSettings& ggs, const bfs::path& map, const std::vector& ais, + const bfs::path& luaPath) : map_(map), game_(ggs, std::make_unique(0), GeneratePlayerInfo(ais)), world_(game_.world_), em_(*static_cast(game_.em_.get())) { MapLoader loader(world_); if(!loader.Load(map)) throw std::runtime_error("Could not load " + map.string()); + MapLoader::SetupResources(world_); + + if(!luaPath.empty()) + { + if(!loader.LoadLuaScript(game_, localState_, luaPath)) + throw std::runtime_error("Failed to load Lua script: " + luaPath.string()); + world_.GetLua().setSuppressStdout(true); + luaPath_ = luaPath; + bnw::cout << "Lua script loaded: " << luaPath << '\n'; + } players_.clear(); for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId) @@ -138,6 +151,12 @@ void HeadlessGame::RecordReplay(const bfs::path& path, unsigned random_init) mapInfo.mapData.CompressFromFile(mapInfo.filepath, &mapInfo.mapChecksum); mapInfo.type = MapType::OldMap; + if(!luaPath_.empty() && bfs::exists(luaPath_)) + { + mapInfo.luaFilepath = luaPath_; + mapInfo.luaData.CompressFromFile(luaPath_, &mapInfo.luaChecksum); + } + for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId) replay_.AddPlayer(world_.GetPlayer(playerId)); replay_.ggs = game_.ggs_; @@ -232,6 +251,7 @@ std::vector GeneratePlayerInfo(const std::vector& ais) } pi.nation = Nation::Romans; pi.team = Team::None; + pi.color = PLAYER_COLORS[ret.size() % PLAYER_COLORS.size()]; ret.push_back(pi); } return ret; diff --git a/extras/ai-battle/HeadlessGame.h b/extras/ai-battle/HeadlessGame.h index c52c906257..578b0a446c 100644 --- a/extras/ai-battle/HeadlessGame.h +++ b/extras/ai-battle/HeadlessGame.h @@ -1,10 +1,11 @@ -// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org) +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) // // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Game.h" +#include "ILocalGameState.h" #include "Replay.h" #include "ai/AIPlayer.h" #include "gameTypes/AIInfo.h" @@ -21,7 +22,8 @@ class EventManager; class HeadlessGame { public: - HeadlessGame(const GlobalGameSettings& ggs, const boost::filesystem::path& map, const std::vector& ais); + HeadlessGame(const GlobalGameSettings& ggs, const boost::filesystem::path& map, const std::vector& ais, + const boost::filesystem::path& luaPath = {}); ~HeadlessGame(); void Run(unsigned maxGF = std::numeric_limits::max()); @@ -33,6 +35,15 @@ class HeadlessGame private: void PrintState(); + struct LocalState : ILocalGameState + { + unsigned GetPlayerId() const override { return 0; } + bool IsHost() const override { return true; } + std::string FormatGFTime(unsigned) const override { return ""; } + void SystemChat(const std::string&) override {} + }; + + LocalState localState_; boost::filesystem::path map_; Game game_; GameWorld& world_; @@ -41,6 +52,7 @@ class HeadlessGame Replay replay_; boost::filesystem::path replayPath_; + boost::filesystem::path luaPath_; unsigned lastReportGf_ = 0; std::chrono::steady_clock::time_point gameStartTime_; diff --git a/extras/ai-battle/main.cpp b/extras/ai-battle/main.cpp index e20a56143b..b75ee4766b 100644 --- a/extras/ai-battle/main.cpp +++ b/extras/ai-battle/main.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org) +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) // // SPDX-License-Identifier: GPL-2.0-or-later @@ -7,9 +7,14 @@ #include "QuickStartGame.h" #include "RTTR_Version.h" #include "RttrConfig.h" +#include "addons/Addon.h" +#include "addons/AddonBool.h" +#include "addons/AddonList.h" +#include "addons/const_addons.h" #include "ai/random.h" #include "files.h" #include "random/Random.h" +#include "s25util/StringConversion.h" #include "s25util/System.h" #include @@ -17,6 +22,8 @@ #include #include #include +#include +#include #if BOOST_VERSION >= 109000 # include using std::optional; @@ -29,6 +36,38 @@ namespace bnw = boost::nowide; namespace bfs = boost::filesystem; namespace po = boost::program_options; +static void loadAddonsFromIni(GlobalGameSettings& ggs, const bfs::path& iniPath) +{ + if(!bfs::exists(iniPath)) + throw std::runtime_error("Settings file not found: " + iniPath.string()); + + boost::property_tree::ptree tree; + boost::property_tree::read_ini(iniPath.string(), tree); + + const auto addons = tree.get_child_optional("addons"); + if(!addons) + { + bnw::cout << "Note: no [addons] section in " << iniPath << ", using defaults.\n"; + return; + } + + unsigned loaded = 0; + for(const auto& entry : *addons) + { + try + { + const auto id = static_cast(s25util::fromStringClassic(entry.first)); + const auto v = entry.second.get_value(); + ggs.setSelection(id, v); + ++loaded; + } catch(const std::exception&) + { + // Unknown or invalid entry - skip silently + } + } + bnw::cout << "Loaded " << loaded << " addon settings from " << iniPath << '\n'; +} + int main(int argc, char** argv) { bnw::nowide_filesystem(); @@ -36,6 +75,8 @@ int main(int argc, char** argv) optional replay_path; optional savegame_path; + optional lua_path; + optional settings_path; unsigned random_init = static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count()); unsigned random_ai_init = random_init; @@ -44,10 +85,13 @@ int main(int argc, char** argv) desc.add_options() ("help,h", "Show help") ("map,m", po::value()->required(),"Map to load") - ("ai", po::value>()->required(),"AI player(s) to add") - ("objective", po::value()->default_value("domination"),"domination(default)|conquer") + ("ai", po::value>()->required(),"AI player(s) to add (aijh | dummy)") + ("objective", po::value()->default_value("domination"),"domination(default) | conquer") + ("wares", po::value()->default_value("normal"),"Starting wares: vlow | low | normal (default) | alot") + ("settings", po::value(&settings_path),"INI file with an [addons] section to configure addon settings (optional)") ("replay", po::value(&replay_path),"Filename to write replay to (optional)") ("save", po::value(&savegame_path),"Filename to write savegame to (optional)") + ("lua", po::value(&lua_path),"Lua script to execute during the game (optional)") ("random_init", po::value(&random_init),"Seed value for the random number generator (optional)") ("random_ai_init", po::value(&random_ai_init),"Seed value for the AI random number generator (optional)") ("maxGF", po::value()->default_value(std::numeric_limits::max()),"Maximum number of game frames to run (optional)") @@ -55,9 +99,16 @@ int main(int argc, char** argv) ; // clang-format on + const auto printHelp = [&](std::ostream& os) { + os << desc + << "\nNote: path arguments support the placeholder " + "(game data folder: SAVES, REPLAYS, MAPS, PRESETS)." + << std::endl; + }; + if(argc == 1) { - bnw::cerr << desc << std::endl; + printHelp(bnw::cerr); return 1; } @@ -68,7 +119,7 @@ int main(int argc, char** argv) if(options.count("help")) { - bnw::cout << desc << std::endl; + printHelp(bnw::cout); return 0; } if(options.count("version")) @@ -83,7 +134,7 @@ int main(int argc, char** argv) } catch(const std::exception& e) { bnw::cerr << "Error: " << e.what() << std::endl; - bnw::cerr << desc << std::endl; + printHelp(bnw::cerr); return 1; } @@ -116,15 +167,54 @@ int main(int argc, char** argv) return 1; } - ggs.objective = GameObjective::TotalDomination; - HeadlessGame game(ggs, mapPath, ais); + const auto wares = options["wares"].as(); + if(wares == "vlow") + ggs.startWares = StartWares::VLow; + else if(wares == "low") + ggs.startWares = StartWares::Low; + else if(wares == "normal") + ggs.startWares = StartWares::Normal; + else if(wares == "alot") + ggs.startWares = StartWares::ALot; + else + { + bnw::cerr << "Unknown wares value: " << wares << std::endl; + return 1; + } + + if(settings_path) + { + loadAddonsFromIni(ggs, RTTRCONFIG.ExpandPath(*settings_path)); + + bnw::cout << "settings: " << RTTRCONFIG.ExpandPath(*settings_path) << std::endl; + bnw::cout << "addon selections (non-default only):" << std::endl; + for(unsigned i = 0; i < ggs.getNumAddons(); ++i) + { + unsigned status = 0; + const Addon* addon = ggs.getAddon(i, status); + if(addon && status != addon->getDefaultStatus()) + { + bnw::cout << " [0x" << std::hex << std::setw(8) << std::setfill('0') + << static_cast(addon->getId()) << std::dec << "] " << addon->getName() << " = "; + if(const auto* listAddon = dynamic_cast(addon)) + bnw::cout << listAddon->getOptionName(status); + else if(dynamic_cast(addon)) + bnw::cout << (status ? "True" : "False"); + else + bnw::cout << status; + bnw::cout << std::endl; + } + } + } + + HeadlessGame game(ggs, mapPath, ais, lua_path ? RTTRCONFIG.ExpandPath(*lua_path) : bfs::path{}); if(replay_path) - game.RecordReplay(*replay_path, random_init); + game.RecordReplay(RTTRCONFIG.ExpandPath(*replay_path), random_init); game.Run(options["maxGF"].as()); game.Close(); if(savegame_path) - game.SaveGame(*savegame_path); + game.SaveGame(RTTRCONFIG.ExpandPath(*savegame_path)); } catch(const std::exception& e) { bnw::cerr << e.what() << std::endl; diff --git a/libs/libGamedata/lua/LuaInterfaceBase.cpp b/libs/libGamedata/lua/LuaInterfaceBase.cpp index 573fe1ad96..ae74da7659 100644 --- a/libs/libGamedata/lua/LuaInterfaceBase.cpp +++ b/libs/libGamedata/lua/LuaInterfaceBase.cpp @@ -168,5 +168,5 @@ bool LuaInterfaceBase::validateUTF8(const std::string& scriptTxt) void LuaInterfaceBase::log(const std::string& msg) { - logger_.write("%s\n") % msg; + logger_.write("%s\n", suppressStdout_ ? LogTarget::File : LogTarget::FileAndStdout) % msg; } diff --git a/libs/libGamedata/lua/LuaInterfaceBase.h b/libs/libGamedata/lua/LuaInterfaceBase.h index 3876f3fefc..5134299202 100644 --- a/libs/libGamedata/lua/LuaInterfaceBase.h +++ b/libs/libGamedata/lua/LuaInterfaceBase.h @@ -35,6 +35,7 @@ class LuaInterfaceBase /// Disable or re-enable throwing an exception on error. /// Note: If error throwing is disabled you have to use HasErrorOccurred to detect an error situation void setThrowOnError(bool doThrow); + void setSuppressStdout(bool suppress) { suppressStdout_ = suppress; } bool hasErrorOccurred() const { return errorOccured_; } void clearErrorOccured() { errorOccured_ = false; } @@ -60,6 +61,7 @@ class LuaInterfaceBase private: Log& logger_; + bool suppressStdout_ = false; /// Sticky flag to signal an occurred error during execution of lua code bool errorOccured_; std::map translations_; diff --git a/libs/s25main/addons/AddonList.cpp b/libs/s25main/addons/AddonList.cpp index d3b6d4f60f..824aaec4b5 100644 --- a/libs/s25main/addons/AddonList.cpp +++ b/libs/s25main/addons/AddonList.cpp @@ -26,6 +26,11 @@ unsigned AddonList::getNumOptions() const return options.size(); } +const std::string& AddonList::getOptionName(unsigned status) const +{ + return options.at(status); +} + AddonList::Gui::Gui(const AddonList& addon, Window& window, bool readonly) : AddonGui(addon, window, readonly) { DrawPoint cbPos(430, 0); diff --git a/libs/s25main/addons/AddonList.h b/libs/s25main/addons/AddonList.h index c0bd778a7c..c5f488d0fd 100644 --- a/libs/s25main/addons/AddonList.h +++ b/libs/s25main/addons/AddonList.h @@ -25,6 +25,7 @@ class AddonList : public Addon std::vector options, unsigned defaultStatus = 0); unsigned getNumOptions() const override; + const std::string& getOptionName(unsigned status) const; std::unique_ptr createGui(Window& window, bool readonly) const override;