wesnoth/src/game_state.cpp
Pentarctagon 263e9d7d4f Replace MAKE_ENUM of CONTROLLER with something easier to use.
CONTROLLER is now defined in a single small header that can be easily read without needing to look through a bunch of Boost macros to figure out how it all works. Using the string values of the enum is now also much simpler since they're just constants of the side_controller class.
2022-01-25 16:57:29 -06:00

468 lines
14 KiB
C++

/*
Copyright (C) 2014 - 2021
by Chris Beck <render787@gmail.com>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program 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.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "game_state.hpp"
#include "actions/undo.hpp"
#include "game_board.hpp"
#include "game_data.hpp"
#include "game_events/manager.hpp"
#include "log.hpp"
#include "map/map.hpp"
#include "pathfind/pathfind.hpp"
#include "pathfind/teleport.hpp"
#include "play_controller.hpp"
#include "preferences/game.hpp"
#include "random_deterministic.hpp"
#include "reports.hpp"
#include "scripting/game_lua_kernel.hpp"
#include "synced_context.hpp"
#include "teambuilder.hpp"
#include "units/unit.hpp"
#include "whiteboard/manager.hpp"
#include "gui/dialogs/loading_screen.hpp"
#include "string_enums/side_controller.hpp"
#include <functional>
#include <SDL2/SDL_timer.h>
#include <algorithm>
#include <set>
static lg::log_domain log_engine("engine");
#define LOG_NG LOG_STREAM(info, log_engine)
#define DBG_NG LOG_STREAM(debug, log_engine)
#define ERR_NG LOG_STREAM(err, log_engine)
game_state::game_state(const config& level, play_controller& pc)
: gamedata_(level)
, board_(level)
, tod_manager_(level)
, pathfind_manager_(new pathfind::manager(level))
, reports_(new reports())
, lua_kernel_(new game_lua_kernel(*this, pc, *reports_))
, ai_manager_()
, events_manager_(new game_events::manager())
// TODO: this construct units (in dimiss undo action) but resrouces:: are not available yet,
// so we might want to move the innitialisation of undo_stack_ to game_state::init
, undo_stack_(new actions::undo_list(level.child("undo_stack")))
, player_number_(level["playing_team"].to_int() + 1)
, next_player_number_(level["next_player_number"].to_int(player_number_ + 1))
, do_healing_(level["do_healing"].to_bool(false))
, init_side_done_(level["init_side_done"].to_bool(false))
, start_event_fired_(!level["playing_team"].empty())
, server_request_number_(level["server_request_number"].to_int())
, first_human_team_(-1)
{
lua_kernel_->load_core();
if(const config& endlevel_cfg = level.child("end_level_data")) {
end_level_data el_data;
el_data.read(endlevel_cfg);
el_data.transient.carryover_report = false;
end_level_data_ = el_data;
}
}
game_state::game_state(const config& level, play_controller& pc, game_board& board)
: gamedata_(level)
, board_(board)
, tod_manager_(level)
, pathfind_manager_(new pathfind::manager(level))
, reports_(new reports())
, lua_kernel_(new game_lua_kernel(*this, pc, *reports_))
, ai_manager_()
, events_manager_(new game_events::manager())
, player_number_(level["playing_team"].to_int() + 1)
, next_player_number_(level["next_player_number"].to_int(player_number_ + 1))
, do_healing_(level["do_healing"].to_bool(false))
, end_level_data_()
, init_side_done_(level["init_side_done"].to_bool(false))
, start_event_fired_(!level["playing_team"].empty())
, server_request_number_(level["server_request_number"].to_int())
, first_human_team_(-1)
{
lua_kernel_->load_core();
events_manager_->read_scenario(level);
if(const config& endlevel_cfg = level.child("end_level_data")) {
end_level_data el_data;
el_data.read(endlevel_cfg);
el_data.transient.carryover_report = false;
end_level_data_ = el_data;
}
}
game_state::~game_state() {}
static int placing_score(const config& side, const gamemap& map, const map_location& pos)
{
int positions = 0, liked = 0;
const t_translation::ter_list terrain = t_translation::read_list(side["terrain_liked"].str());
for(int i = -8; i != 8; ++i) {
for(int j = -8; j != +8; ++j) {
const map_location pos2 = pos.plus(i, j);
if(map.on_board(pos2)) {
++positions;
if(std::count(terrain.begin(),terrain.end(),map[pos2])) {
++liked;
}
}
}
}
return (100*liked)/positions;
}
struct placing_info {
placing_info() :
side(0),
score(0),
pos()
{
}
int side, score;
map_location pos;
};
static bool operator<(const placing_info& a, const placing_info& b) { return a.score > b.score; }
void game_state::place_sides_in_preferred_locations(const config& level)
{
std::vector<placing_info> placings;
int num_pos = board_.map().num_valid_starting_positions();
int side_num = 1;
for(const config &side : level.child_range("side"))
{
for(int p = 1; p <= num_pos; ++p) {
const map_location& pos = board_.map().starting_position(p);
int score = placing_score(side, board_.map(), pos);
placing_info obj;
obj.side = side_num;
obj.score = score;
obj.pos = pos;
placings.push_back(obj);
}
++side_num;
}
std::stable_sort(placings.begin(),placings.end());
std::set<int> placed;
std::set<map_location> positions_taken;
for (std::vector<placing_info>::const_iterator i = placings.begin(); i != placings.end() && static_cast<int>(placed.size()) != side_num - 1; ++i) {
if(placed.count(i->side) == 0 && positions_taken.count(i->pos) == 0) {
placed.insert(i->side);
positions_taken.insert(i->pos);
board_.map().set_starting_position(i->side,i->pos);
LOG_NG << "placing side " << i->side << " at " << i->pos << std::endl;
}
}
}
void game_state::init(const config& level, play_controller & pc)
{
events_manager_->read_scenario(level);
gui2::dialogs::loading_screen::progress(loading_stage::init_teams);
if (level["modify_placing"].to_bool()) {
LOG_NG << "modifying placing..." << std::endl;
place_sides_in_preferred_locations(level);
}
LOG_NG << "initialized time of day regions... " << (SDL_GetTicks() - pc.ticks()) << std::endl;
for (const config &t : level.child_range("time_area")) {
tod_manager_.add_time_area(board_.map(),t);
}
LOG_NG << "initialized teams... " << (SDL_GetTicks() - pc.ticks()) << std::endl;
board_.teams().resize(level.child_count("side"));
if (player_number_ > static_cast<int>(board_.teams().size())) {
ERR_NG << "invalid player number " << player_number_ << " #sides=" << board_.teams().size() << "\n";
player_number_ = 1;
// in case there are no teams, using player_number_ migh still cause problems later.
}
std::vector<team_builder> team_builders;
// Note this isn't strictly necessary since team_builder declares a move constructor which will
// be used if a copy is needed (see the class documentation for why a copy causes crashes), but
// it can't hurt to be doubly safe.
team_builders.reserve(board_.teams().size());
int team_num = 0;
for (const config &side : level.child_range("side"))
{
if (first_human_team_ == -1) {
const std::string &controller = side["controller"];
if (controller == side_controller::human && side["is_local"].to_bool(true)) {
first_human_team_ = team_num;
}
}
++team_num;
team_builders.emplace_back(side, board_.get_team(team_num), level, board_, team_num);
team_builders.back().build_team_stage_one();
}
//Initialize the lua kernel before the units are created.
lua_kernel_->initialize(level);
{
//sync traits of start units and the random start time.
randomness::set_random_determinstic deterministic(gamedata_.rng());
tod_manager_.resolve_random(*randomness::generator);
for(team_builder& tb : team_builders) {
tb.build_team_stage_two();
}
for(std::size_t i = 0; i < board_.teams().size(); i++) {
// Labels from players in your ignore list default to hidden
if(preferences::is_ignored(board_.teams()[i].current_player())) {
std::string label_cat = "side:" + std::to_string(i + 1);
board_.hidden_label_categories().push_back(label_cat);
}
}
}
}
void game_state::set_game_display(game_display * gd)
{
lua_kernel_->set_game_display(gd);
}
void game_state::write(config& cfg) const
{
cfg["init_side_done"] = init_side_done_;
if(gamedata_.phase() == game_data::PLAY) {
cfg["playing_team"] = player_number_ - 1;
cfg["next_player_number"] = next_player_number_;
}
cfg["server_request_number"] = server_request_number_;
cfg["do_healing"] = do_healing_;
//Call the lua save_game functions
lua_kernel_->save_game(cfg);
//Write the game events.
events_manager_->write_events(cfg);
//Write the map, unit_map, and teams info
board_.write_config(cfg);
//Write the tod manager, and time areas
cfg.merge_with(tod_manager_.to_config());
//write out the current state of the map
cfg.merge_with(pathfind_manager_->to_config());
//Write the game data, including wml vars
gamedata_.write_snapshot(cfg);
// Preserve the undo stack so that fog/shroud clearing is kept accurate.
undo_stack_->write(cfg.add_child("undo_stack"));
if(end_level_data_) {
end_level_data_->write(cfg.add_child("end_level_data"));
}
}
namespace {
struct castle_cost_calculator : pathfind::cost_calculator
{
castle_cost_calculator(const gamemap& map, const team & view_team) :
map_(map),
viewer_(view_team),
use_shroud_(view_team.uses_shroud())
{}
virtual double cost(const map_location& loc, const double) const
{
if(!map_.is_castle(loc))
return 10000;
if ( use_shroud_ && viewer_.shrouded(loc) )
return 10000;
return 1;
}
private:
const gamemap& map_;
const team& viewer_;
const bool use_shroud_; // Allows faster checks when shroud is disabled.
};
}//anonymous namespace
/**
* Checks to see if a leader at @a leader_loc could recruit somewhere.
* This takes into account terrain, shroud (for side @a side), and the presence
* of visible units.
* The behavior for an invalid @a side is subject to change for future needs.
*/
bool game_state::can_recruit_from(const map_location& leader_loc, int side) const
{
const gamemap& map = board_.map();
if(!map.is_keep(leader_loc)) {
return false;
}
try {
return pathfind::find_vacant_tile(leader_loc, pathfind::VACANT_CASTLE, nullptr, &board_.get_team(side))
!= map_location::null_location();
} catch(const std::out_of_range&) {
// Invalid side specified.
// Currently this cannot happen, but it could conceivably be used in
// the future to request that shroud and visibility be ignored. Until
// that comes to pass, just return.
return false;
}
}
bool game_state::can_recruit_from(const unit& leader) const
{
return can_recruit_from(leader.get_location(), leader.side());
}
/**
* Checks to see if a leader at @a leader_loc could recruit on @a recruit_loc.
* This takes into account terrain, shroud (for side @a side), and whether or
* not there is already a visible unit at recruit_loc.
* The behavior for an invalid @a side is subject to change for future needs.
*/
bool game_state::can_recruit_on(const map_location& leader_loc, const map_location& recruit_loc, int side) const
{
const gamemap& map = board_.map();
if(!map.is_castle(recruit_loc)) {
return false;
}
if(!map.is_keep(leader_loc)) {
return false;
}
try {
const team& view_team = board_.get_team(side);
if(view_team.shrouded(recruit_loc)) {
return false;
}
if(board_.has_visible_unit(recruit_loc, view_team)) {
return false;
}
castle_cost_calculator calc(map, view_team);
// The limit computed in the third argument is more than enough for
// any convex castle on the map. Strictly speaking it could be
// reduced to sqrt(map.w()**2 + map.h()**2).
pathfind::plain_route rt =
pathfind::a_star_search(leader_loc, recruit_loc, map.w() + map.h(), calc, map.w(), map.h());
return !rt.steps.empty();
} catch(const std::out_of_range&) {
// Invalid side specified.
// Currently this cannot happen, but it could conceivably be used in
// the future to request that shroud and visibility be ignored. Until
// that comes to pass, just return.
return false;
}
}
bool game_state::can_recruit_on(const unit& leader, const map_location& recruit_loc) const
{
return can_recruit_on(leader.get_location(), recruit_loc, leader.side());
}
bool game_state::side_can_recruit_on(int side, map_location hex) const
{
unit_map::const_iterator leader = board_.units().find(hex);
if ( leader != board_.units().end() ) {
return leader->can_recruit() && leader->side() == side && can_recruit_from(*leader);
} else {
// Look for a leader who can recruit on last_hex.
for ( leader = board_.units().begin(); leader != board_.units().end(); ++leader) {
if ( leader->can_recruit() && leader->side() == side && can_recruit_on(*leader, hex) ) {
return true;
}
}
}
// No leader found who can recruit at last_hex.
return false;
}
game_events::wmi_manager& game_state::get_wml_menu_items()
{
return this->events_manager_->wml_menu_items();
}
const game_events::wmi_manager& game_state::get_wml_menu_items() const
{
return this->events_manager_->wml_menu_items();
}
namespace
{
//not really a 'choice' we just need to make sure to inform the server about this.
class add_side_wml_choice : public synced_context::server_choice
{
public:
add_side_wml_choice()
{
}
/** We are in a game with no mp server and need to do this choice locally */
virtual config local_choice() const
{
return config{};
}
/** The request which is sent to the mp server. */
virtual config request() const
{
return config{};
}
virtual const char* name() const
{
return "add_side_wml";
}
private:
};
} // end anon namespace
void game_state::add_side_wml(config cfg)
{
cfg["side"] = board_.teams().size() + 1;
//if we want to also allow setting the controller we must update the server code.
cfg["controller"] = side_controller::none;
//TODO: is this it? are there caches which must be cleared?
board_.teams().emplace_back();
board_.teams().back().build(cfg, board_.map(), cfg["gold"].to_int());
config choice = synced_context::ask_server_choice(add_side_wml_choice());
}