Skip to content
24 changes: 22 additions & 2 deletions extras/ai-battle/HeadlessGame.cpp
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -13,6 +13,7 @@
#include "world/MapLoader.h"
#include "gameTypes/MapInfo.h"
#include "gameData/GameConsts.h"
#include "s25util/colors.h"
#include <boost/nowide/iostream.hpp>
#include <chrono>
#include <cstdio>
Expand All @@ -27,6 +28,7 @@ std::string HumanReadableNumber(unsigned num);

namespace bfs = boost::filesystem;
namespace bnw = boost::nowide;

using bfs::canonical;

#ifdef WIN32
Expand All @@ -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<AI::Info>& ais)
HeadlessGame::HeadlessGame(const GlobalGameSettings& ggs, const bfs::path& map, const std::vector<AI::Info>& ais,
const bfs::path& luaPath)
: map_(map), game_(ggs, std::make_unique<EventManager>(0), GeneratePlayerInfo(ais)), world_(game_.world_),
em_(*static_cast<EventManager*>(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)
Expand Down Expand Up @@ -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_;
Expand Down Expand Up @@ -232,6 +251,7 @@ std::vector<PlayerInfo> GeneratePlayerInfo(const std::vector<AI::Info>& ais)
}
pi.nation = Nation::Romans;
pi.team = Team::None;
pi.color = PLAYER_COLORS[ret.size() % PLAYER_COLORS.size()];
ret.push_back(pi);
}
return ret;
Expand Down
16 changes: 14 additions & 2 deletions extras/ai-battle/HeadlessGame.h
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -21,7 +22,8 @@ class EventManager;
class HeadlessGame
{
public:
HeadlessGame(const GlobalGameSettings& ggs, const boost::filesystem::path& map, const std::vector<AI::Info>& ais);
HeadlessGame(const GlobalGameSettings& ggs, const boost::filesystem::path& map, const std::vector<AI::Info>& ais,
const boost::filesystem::path& luaPath = {});
~HeadlessGame();

void Run(unsigned maxGF = std::numeric_limits<unsigned>::max());
Expand All @@ -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_;
Expand All @@ -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_;
Expand Down
110 changes: 100 additions & 10 deletions extras/ai-battle/main.cpp
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -7,16 +7,23 @@
#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 <boost/filesystem.hpp>
#include <boost/nowide/args.hpp>
#include <boost/nowide/filesystem.hpp>
#include <boost/nowide/iostream.hpp>
#include <boost/program_options.hpp>
#include <boost/property_tree/ini_parser.hpp>
#include <iomanip>
#if BOOST_VERSION >= 109000
# include <optional>
using std::optional;
Expand All @@ -29,13 +36,47 @@ 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<AddonId>(s25util::fromStringClassic<unsigned>(entry.first));
const auto v = entry.second.get_value<unsigned>();
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();
bnw::args _(argc, argv);

optional<std::string> replay_path;
optional<std::string> savegame_path;
optional<std::string> lua_path;
optional<std::string> settings_path;
unsigned random_init = static_cast<unsigned>(std::chrono::high_resolution_clock::now().time_since_epoch().count());
unsigned random_ai_init = random_init;

Expand All @@ -44,20 +85,30 @@ int main(int argc, char** argv)
desc.add_options()
("help,h", "Show help")
("map,m", po::value<std::string>()->required(),"Map to load")
("ai", po::value<std::vector<std::string>>()->required(),"AI player(s) to add")
("objective", po::value<std::string>()->default_value("domination"),"domination(default)|conquer")
("ai", po::value<std::vector<std::string>>()->required(),"AI player(s) to add (aijh | dummy)")
("objective", po::value<std::string>()->default_value("domination"),"domination(default) | conquer")
("wares", po::value<std::string>()->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<unsigned>()->default_value(std::numeric_limits<unsigned>::max()),"Maximum number of game frames to run (optional)")
("version", "Show version information and exit")
;
// clang-format on

const auto printHelp = [&](std::ostream& os) {
os << desc
<< "\nNote: path arguments support the <RTTR_USERDATA> placeholder "
"(game data folder: SAVES, REPLAYS, MAPS, PRESETS)."
<< std::endl;
};

if(argc == 1)
{
bnw::cerr << desc << std::endl;
printHelp(bnw::cerr);
return 1;
}

Expand All @@ -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"))
Expand All @@ -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;
}

Expand Down Expand Up @@ -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<std::string>();
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<unsigned>(addon->getId()) << std::dec << "] " << addon->getName() << " = ";
if(const auto* listAddon = dynamic_cast<const AddonList*>(addon))
bnw::cout << listAddon->getOptionName(status);
else if(dynamic_cast<const AddonBool*>(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<unsigned>());
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;
Expand Down
2 changes: 1 addition & 1 deletion libs/libGamedata/lua/LuaInterfaceBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions libs/libGamedata/lua/LuaInterfaceBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand All @@ -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<std::string, std::string> translations_;
Expand Down
5 changes: 5 additions & 0 deletions libs/s25main/addons/AddonList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions libs/s25main/addons/AddonList.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class AddonList : public Addon
std::vector<std::string> options, unsigned defaultStatus = 0);

unsigned getNumOptions() const override;
const std::string& getOptionName(unsigned status) const;

std::unique_ptr<AddonGui> createGui(Window& window, bool readonly) const override;

Expand Down