Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions extras/ai-battle/HeadlessGame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "HeadlessGame.h"
#include "EventManager.h"
#include "GamePlayer.h"
#include "GlobalGameSettings.h"
#include "PlayerInfo.h"
#include "Savegame.h"
Expand All @@ -22,7 +23,7 @@
# include "Windows.h"
#endif

std::vector<PlayerInfo> GeneratePlayerInfo(const std::vector<AI::Info>& ais);
std::vector<PlayerInfo> GeneratePlayerInfo(const std::vector<AI::Info>& ais, const std::vector<Team>& teams);
std::string ToString(const std::chrono::milliseconds& time);
std::string HumanReadableNumber(unsigned num);

Expand All @@ -44,15 +45,23 @@ void printConsole(const char* fmt, ...);
#endif

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_),
const bfs::path& luaPath, const std::vector<Team>& teams)
: map_(map), game_(ggs, std::make_unique<EventManager>(0), GeneratePlayerInfo(ais, teams)), 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_);

// Establish the team alliances (ally + non-aggression pacts) exactly like GameClient::StartGame does
// for a fresh map. Without this, teammates have no pacts and are mutually attackable, so the AIs attack
// their own team; on replay GameClient *does* set up the pacts, so those recorded attack commands are
// handled differently and the replay desyncs (object-count divergence). MakeStartPacts is a no-op for
// teamless players, so this is safe regardless of whether --teams was given.
for(unsigned i = 0; i < world_.GetNumPlayers(); ++i)
world_.GetPlayer(i).MakeStartPacts();

if(!luaPath.empty())
{
if(!loader.LoadLuaScript(game_, localState_, luaPath))
Expand Down Expand Up @@ -234,7 +243,7 @@ void HeadlessGame::PrintState()
lastReportGf_ = em_.GetCurrentGF();
}

std::vector<PlayerInfo> GeneratePlayerInfo(const std::vector<AI::Info>& ais)
std::vector<PlayerInfo> GeneratePlayerInfo(const std::vector<AI::Info>& ais, const std::vector<Team>& teams)
{
std::vector<PlayerInfo> ret;
for(const AI::Info& ai : ais)
Expand All @@ -249,7 +258,7 @@ std::vector<PlayerInfo> GeneratePlayerInfo(const std::vector<AI::Info>& ais)
default: pi.name = "Dummy " + std::to_string(ret.size()); break;
}
pi.nation = Nation::Romans;
pi.team = Team::None;
pi.team = (ret.size() < teams.size()) ? teams[ret.size()] : Team::None;
pi.color = PLAYER_COLORS[ret.size() % PLAYER_COLORS.size()];
ret.push_back(pi);
}
Expand Down
5 changes: 4 additions & 1 deletion extras/ai-battle/HeadlessGame.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "Replay.h"
#include "ai/AIPlayer.h"
#include "gameTypes/AIInfo.h"
#include "gameTypes/TeamTypes.h"
#include <boost/filesystem.hpp>
#include <chrono>
#include <limits>
Expand All @@ -22,8 +23,10 @@ class EventManager;
class HeadlessGame
{
public:
/// teams: optional per-player team assignment (index -> Team). Players sharing a team get start
/// pacts (ally + non-aggression), so only opposing teams fight. Defaults to no teams.
HeadlessGame(const GlobalGameSettings& ggs, const boost::filesystem::path& map, const std::vector<AI::Info>& ais,
const boost::filesystem::path& luaPath = {});
const boost::filesystem::path& luaPath = {}, const std::vector<Team>& teams = {});
~HeadlessGame();

void Run(unsigned maxGF = std::numeric_limits<unsigned>::max());
Expand Down
147 changes: 94 additions & 53 deletions extras/ai-battle/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "ai/random.h"
#include "files.h"
#include "random/Random.h"
#include "gameTypes/TeamTypes.h"
#include "s25util/Log.h"
#include "s25util/StringConversion.h"
#include "s25util/System.h"
Expand All @@ -25,6 +26,7 @@
#include <boost/program_options.hpp>
#include <boost/property_tree/ini_parser.hpp>
#include <iomanip>
#include <sstream>
#if BOOST_VERSION >= 109000
# include <optional>
using std::optional;
Expand All @@ -45,26 +47,32 @@ static void loadAddonsFromIni(GlobalGameSettings& ggs, const bfs::path& iniPath)
boost::property_tree::ptree tree;
boost::property_tree::read_ini(iniPath.string(), tree);

if(tree.empty()) // empty file -> nothing to configure, that's fine
return;

// Anything else is intentional configuration, so surface mistakes as hard errors instead of
// silently ignoring them (a mistyped section or key/value would otherwise go unnoticed).
const auto addons = tree.get_child_optional("addons");
if(!addons)
{
bnw::cout << "Note: no [addons] section in " << iniPath << ", using defaults.\n";
return;
}
throw std::runtime_error("No [addons] section in " + iniPath.string());

unsigned loaded = 0;
for(const auto& entry : *addons)
{
AddonId id{};
unsigned value = 0;
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;
id = static_cast<AddonId>(s25util::fromStringClassic<unsigned>(entry.first));
value = entry.second.get_value<unsigned>();
} catch(const std::exception&)
{
// Unknown or invalid entry - skip silently
throw std::runtime_error("Invalid addon entry '" + entry.first + "' in " + iniPath.string());
}
if(!ggs.getAddon(id)) // unknown/unsupported addon id
throw std::runtime_error("Unknown addon id '" + entry.first + "' in " + iniPath.string());
ggs.setSelection(id, value);
++loaded;
}
bnw::cout << "Loaded " << loaded << " addon settings from " << iniPath << '\n';
}
Expand All @@ -74,28 +82,37 @@ 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;

po::options_description desc("Allowed options");
struct
{
std::string map;
std::vector<std::string> ais;
optional<std::string> teams;
std::string objective, wares;
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;
unsigned maxGF = std::numeric_limits<unsigned>::max();
} opts;
// clang-format off
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 (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)")
("map,m", po::value(&opts.map)->required(),"Map to load")
("ai", po::value(&opts.ais)->required(),"AI player(s) to add (aijh | dummy)")
("teams", po::value(&opts.teams),"Team assignment, e.g. \"0,1;2,3\" for a 2v2 (groups separated by ';', player indices by ','). Allied players get start pacts.")
("objective", po::value(&opts.objective)->default_value("domination"),"domination(default) | conquer")
("wares", po::value(&opts.wares)->default_value("normal"),"Starting wares: vlow | low | normal (default) | alot")
("settings", po::value(&opts.settings_path),"INI file with an [addons] section to configure addon settings (optional)")
("replay", po::value(&opts.replay_path),"Filename to write replay to (optional)")
("save", po::value(&opts.savegame_path),"Filename to write savegame to (optional)")
("lua", po::value(&opts.lua_path),"Lua script to execute during the game (optional)")
("random_init", po::value(&opts.random_init),"Seed value for the random number generator (optional)")
("random_ai_init", po::value(&opts.random_ai_init),"Seed value for the AI random number generator (optional)")
("maxGF", po::value(&opts.maxGF),"Maximum number of game frames to run (optional)")
("version", "Show version information and exit")
;
// clang-format on
Expand All @@ -113,9 +130,9 @@ int main(int argc, char** argv)
return 1;
}

po::variables_map options;
try
{
po::variables_map options;
po::store(po::command_line_parser(argc, argv).options(desc).run(), options);

if(options.count("help"))
Expand Down Expand Up @@ -145,8 +162,8 @@ int main(int argc, char** argv)
for(int i = 0; i < argc; ++i)
bnw::cout << argv[i] << " ";
bnw::cout << std::endl;
bnw::cout << "random_init: " << random_init << std::endl;
bnw::cout << "random_ai_init: " << random_ai_init << std::endl;
bnw::cout << "random_init: " << opts.random_init << std::endl;
bnw::cout << "random_ai_init: " << opts.random_ai_init << std::endl;
bnw::cout << std::endl;

RTTRCONFIG.Init();
Expand All @@ -155,44 +172,42 @@ int main(int argc, char** argv)
const bfs::path logDir = RTTRCONFIG.ExpandPath(s25::folders::logs);
bfs::create_directories(logDir);
LOG.setLogFilepath(logDir);
RANDOM.Init(random_init);
AI::getRandomGenerator().seed(random_ai_init);
RANDOM.Init(opts.random_init);
AI::getRandomGenerator().seed(opts.random_ai_init);

const bfs::path mapPath = RTTRCONFIG.ExpandPath(options["map"].as<std::string>());
const std::vector<AI::Info> ais = ParseAIOptions(options["ai"].as<std::vector<std::string>>());
const bfs::path mapPath = RTTRCONFIG.ExpandPath(opts.map);
const std::vector<AI::Info> ais = ParseAIOptions(opts.ais);

GlobalGameSettings ggs;
const auto objective = options["objective"].as<std::string>();
if(objective == "domination")
if(opts.objective == "domination")
ggs.objective = GameObjective::TotalDomination;
else if(objective == "conquer")
else if(opts.objective == "conquer")
ggs.objective = GameObjective::Conquer3_4;
else
{
bnw::cerr << "unknown objective: " << objective << std::endl;
bnw::cerr << "unknown objective: " << opts.objective << std::endl;
return 1;
}

const auto wares = options["wares"].as<std::string>();
if(wares == "vlow")
if(opts.wares == "vlow")
ggs.startWares = StartWares::VLow;
else if(wares == "low")
else if(opts.wares == "low")
ggs.startWares = StartWares::Low;
else if(wares == "normal")
else if(opts.wares == "normal")
ggs.startWares = StartWares::Normal;
else if(wares == "alot")
else if(opts.wares == "alot")
ggs.startWares = StartWares::ALot;
else
{
bnw::cerr << "Unknown wares value: " << wares << std::endl;
bnw::cerr << "Unknown wares value: " << opts.wares << std::endl;
return 1;
}

if(settings_path)
if(opts.settings_path)
{
loadAddonsFromIni(ggs, RTTRCONFIG.ExpandPath(*settings_path));
loadAddonsFromIni(ggs, RTTRCONFIG.ExpandPath(*opts.settings_path));

bnw::cout << "settings: " << RTTRCONFIG.ExpandPath(*settings_path) << std::endl;
bnw::cout << "settings: " << RTTRCONFIG.ExpandPath(*opts.settings_path) << std::endl;
bnw::cout << "addon selections (non-default only):" << std::endl;
for(unsigned i = 0; i < ggs.getNumAddons(); ++i)
{
Expand All @@ -213,14 +228,40 @@ int main(int argc, char** argv)
}
}

HeadlessGame game(ggs, mapPath, ais, lua_path ? RTTRCONFIG.ExpandPath(*lua_path) : bfs::path{});
if(replay_path)
game.RecordReplay(RTTRCONFIG.ExpandPath(*replay_path), random_init);
// Team assignment, e.g. "0,1;2,3". Player index -> Team (Team1, Team2, ...).
std::vector<Team> teams;
if(opts.teams)
{
std::stringstream groups(*opts.teams);
std::string group;
unsigned teamIdx = 0;
while(std::getline(groups, group, ';'))
{
const Team team = static_cast<Team>(static_cast<uint8_t>(Team::Team1) + teamIdx);
std::stringstream members(group);
std::string idx;
while(std::getline(members, idx, ','))
{
if(idx.empty())
continue;
const auto p = static_cast<unsigned>(std::stoul(idx));
if(p >= teams.size())
teams.resize(p + 1, Team::None);
teams[p] = team;
}
++teamIdx;
}
}

HeadlessGame game(ggs, mapPath, ais, opts.lua_path ? RTTRCONFIG.ExpandPath(*opts.lua_path) : bfs::path{},
teams);
if(opts.replay_path)
game.RecordReplay(RTTRCONFIG.ExpandPath(*opts.replay_path), opts.random_init);

game.Run(options["maxGF"].as<unsigned>());
game.Run(opts.maxGF);
game.Close();
if(savegame_path)
game.SaveGame(RTTRCONFIG.ExpandPath(*savegame_path));
if(opts.savegame_path)
game.SaveGame(RTTRCONFIG.ExpandPath(*opts.savegame_path));
} catch(const std::exception& e)
{
bnw::cerr << e.what() << std::endl;
Expand Down
32 changes: 24 additions & 8 deletions libs/s25main/GlobalGameSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,27 +152,43 @@ const Addon* GlobalGameSettings::getAddon(unsigned idx) const
return addons[idx].addon.get();
}

GlobalGameSettings::AddonWithState* GlobalGameSettings::getAddon(AddonId id)
const Addon* GlobalGameSettings::getAddon(AddonId idx, unsigned& status) const
{
auto it = helpers::find_if(addons, [id](const AddonWithState& cur) { return cur.addon->getId() == id; });
return it != addons.end() ? &*it : nullptr;
const AddonWithState* addon = findAddon(idx);
if(addon)
{
status = addon->status;
return addon->addon.get();
}
return nullptr;
}

const Addon* GlobalGameSettings::getAddon(AddonId idx) const
{
unsigned status;
return getAddon(idx, status);
}

GlobalGameSettings::AddonWithState* GlobalGameSettings::findAddon(AddonId id)
{
return const_cast<AddonWithState*>(static_cast<const GlobalGameSettings&>(*this).findAddon(id));
}

const GlobalGameSettings::AddonWithState* GlobalGameSettings::getAddon(AddonId id) const
const GlobalGameSettings::AddonWithState* GlobalGameSettings::findAddon(AddonId id) const
{
auto it = helpers::find_if(addons, [id](const AddonWithState& cur) { return cur.addon->getId() == id; });
return it != addons.end() ? &*it : nullptr;
}

bool GlobalGameSettings::isEnabled(AddonId id) const
{
const auto* addon = getAddon(id);
const auto* addon = findAddon(id);
return addon && addon->status != addon->addon->getDefaultStatus();
}

unsigned GlobalGameSettings::getSelection(AddonId id) const
{
const auto* addon = getAddon(id);
const auto* addon = findAddon(id);
return addon ? addon->status : 0;
}

Expand Down Expand Up @@ -300,9 +316,9 @@ void GlobalGameSettings::applyLegacyInexhaustibleMines()

void GlobalGameSettings::setSelection(AddonId id, unsigned selection)
{
auto* addon = getAddon(id);
auto* addon = findAddon(id);
if(!addon)
LOG.write(_("Addon %1$#x not found!\n"), LogTarget::FileAndStderr) % static_cast<unsigned>(id);
LOG.write(_("Addon %1$#x not found!\n"), LogTarget::FileAndStderr) % rttr::enum_cast(id);
else
addon->status = selection;
}
Expand Down
Loading
Loading