wesnoth/src/scripting/game_lua_kernel.cpp
Subhraman Sarkar c3ef8084d5 wesnoth.terrain_types: add mvt_alias and def_alias
these expose `terrain_type::mvt_type()` and ``terrain_type::def_type()` respectively.
(#9969)
2025-03-14 08:55:18 +05:30

6322 lines
183 KiB
C++

/*
Copyright (C) 2009 - 2025
by Guillaume Melquiond <guillaume.melquiond@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.
*/
/**
* @file
* Provides a Lua interpreter, to be embedded in WML.
*
* @note Naming conventions:
* - intf_ functions are exported in the wesnoth domain,
* - impl_ functions are hidden inside metatables,
* - cfun_ functions are closures,
* - luaW_ functions are helpers in Lua style.
*/
#include "scripting/game_lua_kernel.hpp"
#include "actions/attack.hpp" // for battle_context_unit_stats, etc
#include "actions/advancement.hpp" // for advance_unit_at, etc
#include "actions/move.hpp" // for clear_shroud
#include "actions/vision.hpp" // for clear_shroud and create_jamming_map
#include "actions/undo.hpp" // for clear_shroud and create_jamming_map
#include "actions/undo_action.hpp" // for clear_shroud and create_jamming_map
#include "ai/composite/ai.hpp" // for ai_composite
#include "ai/composite/component.hpp" // for component, etc
#include "ai/composite/contexts.hpp" // for ai_context
#include "ai/lua/engine_lua.hpp" // for engine_lua
#include "ai/composite/rca.hpp" // for candidate_action
#include "ai/composite/stage.hpp" // for stage
#include "ai/configuration.hpp" // for configuration
#include "ai/lua/core.hpp" // for lua_ai_context, etc
#include "ai/manager.hpp" // for manager, holder
#include "attack_prediction.hpp" // for combatant
#include "chat_events.hpp" // for chat_handler, etc
#include "config.hpp" // for config, etc
#include "display_chat_manager.hpp" // for clear_chat_messages
#include "floating_label.hpp"
#include "formatter.hpp"
#include "game_board.hpp" // for game_board
#include "game_classification.hpp" // for game_classification, etc
#include "game_config.hpp" // for debug, base_income, etc
#include "game_config_manager.hpp" // for game_config_manager
#include "game_data.hpp" // for game_data, etc
#include "game_display.hpp" // for game_display
#include "game_errors.hpp" // for game_error
#include "game_events/conditional_wml.hpp" // for conditional_passed
#include "game_events/entity_location.hpp"
#include "game_events/handlers.hpp"
#include "game_events/manager_impl.hpp" // for pending_event_handler
#include "game_events/pump.hpp" // for queued_event
#include "preferences/preferences.hpp" // for encountered_units
#include "log.hpp" // for LOG_STREAM, logger, etc
#include "map/map.hpp" // for gamemap
#include "map/label.hpp"
#include "map/location.hpp" // for map_location
#include "mouse_events.hpp" // for mouse_handler
#include "mp_game_settings.hpp" // for mp_game_settings
#include "overlay.hpp"
#include "pathfind/pathfind.hpp" // for full_cost_map, plain_route, etc
#include "pathfind/teleport.hpp" // for get_teleport_locations, etc
#include "play_controller.hpp" // for play_controller
#include "preferences/preferences.hpp"
#include "recall_list_manager.hpp" // for recall_list_manager
#include "replay.hpp" // for get_user_choice, etc
#include "reports.hpp" // for register_generator, etc
#include "resources.hpp" // for whiteboard
#include "scripting/lua_attributes.hpp"
#include "scripting/lua_audio.hpp"
#include "scripting/lua_unit.hpp"
#include "scripting/lua_unit_attacks.hpp"
#include "scripting/lua_common.hpp"
#include "scripting/lua_cpp_function.hpp"
#include "scripting/lua_gui2.hpp" // for show_gamestate_inspector
#include "scripting/lua_pathfind_cost_calculator.hpp"
#include "scripting/lua_race.hpp"
#include "scripting/lua_team.hpp"
#include "scripting/lua_terrainmap.hpp"
#include "scripting/lua_unit_type.hpp"
#include "scripting/push_check.hpp"
#include "synced_commands.hpp"
#include "color.hpp" // for surface
#include "side_filter.hpp" // for side_filter
#include "sound.hpp" // for commit_music_changes, etc
#include "synced_context.hpp" // for synced_context, etc
#include "synced_user_choice.hpp"
#include "team.hpp" // for team, village_owner
#include "terrain/terrain.hpp" // for terrain_type
#include "terrain/filter.hpp" // for terrain_filter
#include "terrain/translation.hpp" // for read_terrain_code, etc
#include "time_of_day.hpp" // for time_of_day
#include "tod_manager.hpp" // for tod_manager
#include "tstring.hpp" // for t_string, operator+
#include "units/unit.hpp" // for unit
#include "units/animation_component.hpp" // for unit_animation_component
#include "units/udisplay.hpp"
#include "units/filter.hpp"
#include "units/map.hpp" // for unit_map, etc
#include "units/ptr.hpp" // for unit_const_ptr, unit_ptr
#include "units/types.hpp" // for unit_type_data, unit_types, etc
#include "utils/scope_exit.hpp"
#include "variable.hpp" // for vconfig, etc
#include "variable_info.hpp"
#include "video.hpp" // only for faked
#include "whiteboard/manager.hpp" // for whiteboard
#include "deprecation.hpp"
#include <functional> // for bind_t, bind
#include <array>
#include <cassert> // for assert
#include <cstring> // for strcmp
#include <iterator> // for distance, advance
#include <map> // for map, map<>::value_type, etc
#include <new> // for operator new
#include <set> // for set
#include <sstream> // for operator<<, basic_ostream, etc
#include <thread>
#include <utility> // for pair
#include <algorithm>
#include <vector> // for vector, etc
#ifdef DEBUG_LUA
#include "scripting/debug_lua.hpp"
#endif
static lg::log_domain log_scripting_lua("scripting/lua");
#define DBG_LUA LOG_STREAM(debug, log_scripting_lua)
#define LOG_LUA LOG_STREAM(info, log_scripting_lua)
#define WRN_LUA LOG_STREAM(warn, log_scripting_lua)
#define ERR_LUA LOG_STREAM(err, log_scripting_lua)
static lg::log_domain log_wml("wml");
#define ERR_WML LOG_STREAM(err, log_wml)
std::vector<config> game_lua_kernel::preload_scripts;
config game_lua_kernel::preload_config;
// Template which allows to push member functions to the lua kernel base into lua as C functions, using a shim
typedef int (game_lua_kernel::*member_callback)(lua_State *);
template <member_callback method>
int dispatch(lua_State *L) {
return ((lua_kernel_base::get_lua_kernel<game_lua_kernel>(L)).*method)(L);
}
// Pass a const bool also...
typedef int (game_lua_kernel::*member_callback2)(lua_State *, bool);
template <member_callback2 method, bool b>
int dispatch2(lua_State *L) {
return ((lua_kernel_base::get_lua_kernel<game_lua_kernel>(L)).*method)(L, b);
}
struct map_locker
{
map_locker(game_lua_kernel* kernel) : kernel_(kernel)
{
++kernel_->map_locked_;
}
~map_locker()
{
--kernel_->map_locked_;
}
game_lua_kernel* kernel_;
};
void game_lua_kernel::extract_preload_scripts(const game_config_view& game_config)
{
game_lua_kernel::preload_scripts.clear();
for (const config& cfg : game_config.child_range("lua")) {
game_lua_kernel::preload_scripts.push_back(cfg);
}
game_lua_kernel::preload_config = game_config.mandatory_child("game_config");
}
void game_lua_kernel::log_error(char const * msg, char const * context)
{
lua_kernel_base::log_error(msg, context);
lua_chat(context, msg);
}
void game_lua_kernel::lua_chat(const std::string& caption, const std::string& msg)
{
if (game_display_) {
game_display_->get_chat_manager().add_chat_message(std::time(nullptr), caption, 0, msg,
events::chat_handler::MESSAGE_PUBLIC, false);
}
}
/**
* Gets a vector of sides from side= attribute in a given config node.
* Promotes consistent behavior.
*/
std::vector<int> game_lua_kernel::get_sides_vector(const vconfig& cfg)
{
const config::attribute_value sides = cfg["side"];
const vconfig &ssf = cfg.child("filter_side");
if (!ssf.null()) {
if(!sides.empty()) { WRN_LUA << "ignoring duplicate side filter information (inline side=)"; }
side_filter filter(ssf, &game_state_);
return filter.get_teams();
}
side_filter filter(sides.str(), &game_state_);
return filter.get_teams();
}
namespace {
/**
* Temporary entry to a queued_event stack
*/
struct queued_event_context
{
typedef game_events::queued_event qe;
std::stack<qe const *> & stack_;
queued_event_context(qe const *new_qe, std::stack<qe const*> & stack)
: stack_(stack)
{
stack_.push(new_qe);
}
~queued_event_context()
{
stack_.pop();
}
};
}//unnamed namespace for queued_event_context
/**
* Gets currently viewing side.
* - Ret 1: integer specifying the currently viewing side
* - Ret 2: Bool whether the vision is not limited to that team, this can for example be true during replays.
*/
static int intf_get_viewing_side(lua_State *L)
{
if(const display* disp = display::get_singleton()) {
lua_pushinteger(L, disp->viewing_team().side());
lua_pushboolean(L, disp->show_everything());
return 2;
}
else {
return 0;
}
}
static int intf_handle_user_interact(lua_State *)
{
ai::manager::get_singleton().raise_user_interact();
return 0;
}
static const char animatorKey[] = "unit animator";
static int impl_animator_collect(lua_State* L) {
unit_animator& anim = *static_cast<unit_animator*>(luaL_checkudata(L, 1, animatorKey));
anim.~unit_animator();
return 0;
}
static int impl_add_animation(lua_State* L)
{
unit_animator& anim = *static_cast<unit_animator*>(luaL_checkudata(L, 1, animatorKey));
unit_ptr up = luaW_checkunit_ptr(L, 2, false);
unit& u = *up;
std::string which = luaL_checkstring(L, 3);
std::string hits_str = luaL_checkstring(L, 4);
strike_result::type hits = strike_result::get_enum(hits_str).value_or(strike_result::type::invalid);
map_location dest;
int v1 = 0, v2 = 0;
bool bars = false;
t_string text;
color_t color{255, 255, 255};
const_attack_ptr primary, secondary;
if(lua_istable(L, 5)) {
lua_getfield(L, 5, "target");
if(luaW_tolocation(L, -1, dest)) {
if(dest == u.get_location()) {
return luaL_argerror(L, 5, "target location must be different from animated unit's location");
} else if(!tiles_adjacent(dest, u.get_location())) {
return luaL_argerror(L, 5, "target location must be adjacent to the animated unit");
}
} else {
// luaW_tolocation may set the location to (0,0) if it fails
dest = map_location();
if(!lua_isnoneornil(L, -1)) {
return luaW_type_error(L, 5, "target", "location table");
}
}
lua_pop(L, 1);
lua_getfield(L, 5, "value");
if(lua_isnumber(L, -1)) {
v1 = lua_tointeger(L, -1);
} else if(lua_istable(L, -1)) {
lua_rawgeti(L, -1, 1);
v1 = lua_tointeger(L, -1);
lua_pop(L, 1);
lua_rawgeti(L, -1, 2);
v2 = lua_tointeger(L, -1);
lua_pop(L, 1);
} else if(!lua_isnoneornil(L, -1)) {
return luaW_type_error(L, 5, "value", "number or array of two numbers");
}
lua_pop(L, 1);
lua_getfield(L, 5, "with_bars");
if(lua_isboolean(L, -1)) {
bars = luaW_toboolean(L, -1);
} else if(!lua_isnoneornil(L, -1)) {
return luaW_type_error(L, 5, "with_bars", lua_typename(L, LUA_TBOOLEAN));
}
lua_pop(L, 1);
lua_getfield(L, 5, "text");
if(lua_isstring(L, -1)) {
text = lua_tostring(L, -1);
} else if(luaW_totstring(L, -1, text)) {
// Do nothing; luaW_totstring already assigned the value
} else if(!lua_isnoneornil(L, -1)) {
return luaW_type_error(L, 5, "text", lua_typename(L, LUA_TSTRING));
}
lua_pop(L, 1);
lua_getfield(L, 5, "color");
if(lua_istable(L, -1) && lua_rawlen(L, -1) == 3) {
int idx = lua_absindex(L, -1);
lua_rawgeti(L, idx, 1); // red @ -3
lua_rawgeti(L, idx, 2); // green @ -2
lua_rawgeti(L, idx, 3); // blue @ -1
color = color_t(lua_tointeger(L, -3), lua_tointeger(L, -2), lua_tointeger(L, -1));
lua_pop(L, 3);
} else if(!lua_isnoneornil(L, -1)) {
return luaW_type_error(L, 5, "color", "array of three numbers");
}
lua_pop(L, 1);
lua_getfield(L, 5, "primary");
primary = luaW_toweapon(L, -1);
if(!primary && !lua_isnoneornil(L, -1)) {
return luaW_type_error(L, 5, "primary", "weapon");
}
lua_pop(L, 1);
lua_getfield(L, 5, "secondary");
secondary = luaW_toweapon(L, -1);
if(!secondary && !lua_isnoneornil(L, -1)) {
return luaW_type_error(L, 5, "secondary", "weapon");
}
lua_pop(L, 1);
} else if(!lua_isnoneornil(L, 5)) {
return luaW_type_error(L, 5, "table of options");
}
anim.add_animation(up, which, u.get_location(), dest, v1, bars, text, color, hits, primary, secondary, v2);
return 0;
}
int game_lua_kernel::impl_run_animation(lua_State* L)
{
if(video::headless() || resources::controller->is_skipping_replay()) {
return 0;
}
events::command_disabler command_disabler;
unit_animator& anim = *static_cast<unit_animator*>(luaL_checkudata(L, 1, animatorKey));
play_controller_.play_slice();
anim.start_animations();
anim.wait_for_end();
anim.set_all_standing();
anim.clear();
return 0;
}
static int impl_clear_animation(lua_State* L)
{
unit_animator& anim = *static_cast<unit_animator*>(luaL_checkudata(L, 1, animatorKey));
anim.clear();
return 0;
}
static int impl_animator_get(lua_State* L)
{
const char* m = lua_tostring(L, 2);
return luaW_getmetafield(L, 1, m);
}
int game_lua_kernel::intf_create_animator(lua_State* L)
{
new(L) unit_animator;
if(luaL_newmetatable(L, animatorKey)) {
luaL_Reg metafuncs[] {
{"__gc", impl_animator_collect},
{"__index", impl_animator_get},
{"add", impl_add_animation},
{"run", &dispatch<&game_lua_kernel::impl_run_animation>},
{"clear", impl_clear_animation},
{nullptr, nullptr},
};
luaL_setfuncs(L, metafuncs, 0);
lua_pushstring(L, "__metatable");
lua_setfield(L, -2, animatorKey);
}
lua_setmetatable(L, -2);
return 1;
}
int game_lua_kernel::intf_gamestate_inspector(lua_State *L)
{
if (game_display_) {
vconfig cfg = vconfig::unconstructed_vconfig();
std::string name;
if(luaW_tovconfig(L, 1, cfg)) {
name = cfg["name"].str();
deprecated_message("gui.show_inspector(cfg)", DEP_LEVEL::INDEFINITE, {1, 19, 0}, "Instead of {name = 'title' }, pass just 'title'.");
} else {
name = luaL_optstring(L, 1, "");
}
return lua_gui2::show_gamestate_inspector(name, gamedata(), game_state_);
}
return 0;
}
/**
* Gets the unit at the given location or with the given id.
* - Arg 1: location
* OR
* - Arg 1: string ID
* - Ret 1: full userdata with __index pointing to impl_unit_get and
* __newindex pointing to impl_unit_set.
*/
int game_lua_kernel::intf_get_unit(lua_State *L)
{
map_location loc;
if(lua_isstring(L, 1) && !lua_isnumber(L, 1)) {
std::string id = luaL_checkstring(L, 1);
for(const unit& u : units()) {
if(u.id() == id) {
luaW_pushunit(L, u.underlying_id());
return 1;
}
}
return 0;
}
if(!luaW_tolocation(L, 1, loc)) {
return luaL_argerror(L, 1, "expected string or location");
}
unit_map::const_iterator ui = units().find(loc);
if (!ui.valid()) return 0;
luaW_pushunit(L, ui->underlying_id());
return 1;
}
/**
* Gets the unit displayed in the sidebar.
* - Ret 1: full userdata with __index pointing to impl_unit_get and
* __newindex pointing to impl_unit_set.
*/
int game_lua_kernel::intf_get_displayed_unit(lua_State *L)
{
if (!game_display_) {
return 0;
}
unit_map::const_iterator ui = board().find_visible_unit(
game_display_->displayed_unit_hex(),
game_display_->viewing_team(),
game_display_->show_everything());
if (!ui.valid()) return 0;
luaW_pushunit(L, ui->underlying_id());
return 1;
}
/**
* Gets all the units matching a given filter.
* - Arg 1: optional table containing a filter
* - Arg 2: optional location (to find all units that would match on that location)
* OR unit (to find all units that would match adjacent to that unit)
* - Ret 1: table containing full userdata with __index pointing to
* impl_unit_get and __newindex pointing to impl_unit_set.
*/
int game_lua_kernel::intf_get_units(lua_State *L)
{
vconfig filter = luaW_checkvconfig(L, 1, true);
unit_filter filt(filter);
std::vector<const unit*> units;
if(unit* u_adj = luaW_tounit(L, 2)) {
if(!u_adj) {
return luaL_argerror(L, 2, "unit not found");
}
units = filt.all_matches_with_unit(*u_adj);
} else if(!lua_isnoneornil(L, 2)) {
map_location loc;
luaW_tolocation(L, 2, loc);
if(!loc.valid()) {
return luaL_argerror(L, 2, "invalid location");
}
units = filt.all_matches_at(loc);
} else {
units = filt.all_matches_on_map();
}
// Go through all the units while keeping the following stack:
// 1: return table, 2: userdata
lua_settop(L, 0);
lua_newtable(L);
int i = 1;
for (const unit * ui : units) {
luaW_pushunit(L, ui->underlying_id());
lua_rawseti(L, 1, i);
++i;
}
return 1;
}
/**
* Matches a unit against the given filter.
* - Arg 1: full userdata.
* - Arg 2: table containing a filter
* - Arg 3: optional location OR optional "adjacent" unit
* - Ret 1: boolean.
*/
int game_lua_kernel::intf_match_unit(lua_State *L)
{
lua_unit& u = *luaW_checkunit_ref(L, 1);
vconfig filter = luaW_checkvconfig(L, 2, true);
if (filter.null()) {
lua_pushboolean(L, true);
return 1;
}
if(unit* u_adj = luaW_tounit(L, 3)) {
if(int side = u.on_recall_list()) {
WRN_LUA << "wesnoth.units.matches called with a secondary unit (3rd argument), ";
WRN_LUA << "but unit to match was on recall list. ";
WRN_LUA << "Thus the 3rd argument is ignored.";
team &t = board().get_team(side);
scoped_recall_unit auto_store("this_unit", t.save_id_or_number(), t.recall_list().find_index(u->id()));
lua_pushboolean(L, unit_filter(filter).matches(*u, map_location()));
return 1;
}
if (!u_adj) {
return luaL_argerror(L, 3, "unit not found");
}
lua_pushboolean(L, unit_filter(filter).matches(*u, *u_adj));
} else if(int side = u.on_recall_list()) {
map_location loc;
luaW_tolocation(L, 3, loc); // If argument 3 isn't a location, loc is unchanged
team &t = board().get_team(side);
scoped_recall_unit auto_store("this_unit", t.save_id_or_number(), t.recall_list().find_index(u->id()));
lua_pushboolean(L, unit_filter(filter).matches(*u, loc));
return 1;
} else {
map_location loc = u->get_location();
luaW_tolocation(L, 3, loc); // If argument 3 isn't a location, loc is unchanged
lua_pushboolean(L, unit_filter(filter).matches(*u, loc));
}
return 1;
}
/**
* Gets the numeric ids of all the units matching a given filter on the recall lists.
* - Arg 1: optional table containing a filter
* - Ret 1: table containing full userdata with __index pointing to
* impl_unit_get and __newindex pointing to impl_unit_set.
*/
int game_lua_kernel::intf_get_recall_units(lua_State *L)
{
vconfig filter = luaW_checkvconfig(L, 1, true);
// Go through all the units while keeping the following stack:
// 1: return table, 2: userdata
lua_settop(L, 0);
lua_newtable(L);
int i = 1, s = 1;
const unit_filter ufilt(filter);
for (team &t : teams())
{
for (unit_ptr & u : t.recall_list())
{
if (!filter.null()) {
scoped_recall_unit auto_store("this_unit",
t.save_id_or_number(), t.recall_list().find_index(u->id()));
if (!ufilt( *u, map_location() ))
continue;
}
luaW_pushunit(L, s, u->underlying_id());
lua_rawseti(L, 1, i);
++i;
}
++s;
}
return 1;
}
/**
* Fires an event.
* - Arg 1: string containing the event name or id.
* - Arg 2: optional first location.
* - Arg 3: optional second location.
* - Arg 4: optional WML table used used as the event data
* Typically this contains [first] as the [weapon] tag and [second] as the [second_weapon] tag.
* - Ret 1: boolean indicating whether the event was processed or not.
*/
int game_lua_kernel::intf_fire_event(lua_State *L, const bool by_id)
{
char const *m = luaL_checkstring(L, 1);
int pos = 2;
map_location l1, l2;
config data;
if (luaW_tolocation(L, 2, l1)) {
if (luaW_tolocation(L, 3, l2)) {
pos = 4;
} else {
pos = 3;
}
}
luaW_toconfig(L, pos, data);
// Support WML names for some common data
if(data.has_child("primary_attack")) {
data.add_child("first", data.mandatory_child("primary_attack"));
data.remove_children("primary_attack");
}
if(data.has_child("secondary_attack")) {
data.add_child("second", data.mandatory_child("secondary_attack"));
data.remove_children("secondary_attack");
}
bool b = false;
if (by_id) {
b = std::get<0>(play_controller_.pump().fire("", m, l1, l2, data));
}
else {
b = std::get<0>(play_controller_.pump().fire(m, l1, l2, data));
}
lua_pushboolean(L, b);
return 1;
}
/**
* Fires a wml menu item.
* - Arg 1: id of the item. it is not possible to fire items that don't have ids with this function.
* - Arg 2: optional first location.
* - Ret 1: boolean, true indicating that the event was fired successfully
*
* NOTE: This is not an "official" feature, it may currently cause assertion failures if used with
* menu items which have "needs_select". It is not supported right now to use it this way.
* The purpose of this function right now is to make it possible to have automated sanity tests for
* the wml menu items system.
*/
int game_lua_kernel::intf_fire_wml_menu_item(lua_State *L)
{
char const *m = luaL_checkstring(L, 1);
map_location l1 = luaW_checklocation(L, 2);
bool b = game_state_.get_wml_menu_items().fire_item(m, l1, gamedata(), game_state_, units());
lua_pushboolean(L, b);
return 1;
}
/**
* Gets a WML variable.
* - Arg 1: string containing the variable name.
* - Arg 2: optional bool indicating if tables for containers should be left empty.
* - Ret 1: value of the variable, if any.
*/
int game_lua_kernel::intf_get_variable(lua_State *L)
{
char const *m = luaL_checkstring(L, 1);
variable_access_const v = gamedata().get_variable_access_read(m);
return luaW_pushvariable(L, v) ? 1 : 0;
}
/**
* Sets a WML variable.
* - Arg 1: string containing the variable name.
* - Arg 2: boolean/integer/string/table containing the value.
*/
int game_lua_kernel::intf_set_variable(lua_State *L)
{
const std::string m = luaL_checkstring(L, 1);
if(m.empty()) return luaL_argerror(L, 1, "empty variable name");
if (lua_isnoneornil(L, 2)) {
gamedata().clear_variable(m);
return 0;
}
variable_access_create v = gamedata().get_variable_access_write(m);
luaW_checkvariable(L, v, 2);
return 0;
}
int game_lua_kernel::intf_create_side(lua_State *L)
{
config cfg = luaW_checkconfig(L, 1);
cfg["side"] = teams().size() + 1;
game_state_.add_side_wml(cfg);
lua_pushinteger(L, teams().size());
return 1;
}
int game_lua_kernel::intf_set_menu_item(lua_State *L)
{
game_state_.get_wml_menu_items().set_item(luaL_checkstring(L, 1), luaW_checkvconfig(L,2));
return 0;
}
int game_lua_kernel::intf_clear_menu_item(lua_State *L)
{
std::string ids(luaL_checkstring(L, 1));
for(const std::string& id : utils::split(ids, ',', utils::STRIP_SPACES)) {
if(id.empty()) {
WRN_LUA << "[clear_menu_item] has been given an empty id=, ignoring";
continue;
}
game_state_.get_wml_menu_items().erase(id);
}
return 0;
}
/**
* Toggle shroud on some locations
* Arg 1: Side number
* Arg 2: List of locations on which to place/remove shroud
*/
int game_lua_kernel::intf_toggle_shroud(lua_State *L, bool place_shroud)
{
team& t = luaW_checkteam(L, 1, board());
if(lua_istable(L, 2)) {
std::set<map_location> locs = luaW_check_locationset(L, 2);
for (const map_location& loc : locs)
{
if (place_shroud) {
t.place_shroud(loc);
} else {
t.clear_shroud(loc);
}
}
} else {
return luaL_argerror(L, 2, "expected list of locations");
}
game_display_->labels().recalculate_shroud();
game_display_->recalculate_minimap();
game_display_->invalidate_all();
return 0;
}
/**
* Overrides the shroud entirely. All locations are shrouded, except for the ones passed in as argument 2.
* Arg 1: Side number
* Arg 2: List of locations that should be unshrouded
*/
int game_lua_kernel::intf_override_shroud(lua_State *L)
{
team& t = luaW_checkteam(L, 1, board());
if(lua_istable(L, 2)) {
std::set<map_location> locs = luaW_check_locationset(L, 2);
t.reshroud();
for(const map_location& loc : locs) {
t.clear_shroud(loc);
}
} else {
return luaW_type_error(L, 2, "list of locations");
}
game_display_->labels().recalculate_shroud();
game_display_->recalculate_minimap();
game_display_->invalidate_all();
return 0;
}
/**
* Highlights the given location on the map.
* - Arg 1: location.
*/
int game_lua_kernel::intf_highlight_hex(lua_State *L)
{
if (!game_display_) {
return 0;
}
const map_location loc = luaW_checklocation(L, 1);
if(!map().on_board(loc)) return luaL_argerror(L, 1, "not on board");
game_display_->highlight_hex(loc);
game_display_->display_unit_hex(loc);
return 0;
}
/**
* Returns whether the first side is an enemy of the second one.
* - Args 1,2: side numbers.
* - Ret 1: boolean.
*/
int game_lua_kernel::intf_is_enemy(lua_State *L)
{
unsigned side_1, side_2;
if(team* t = luaW_toteam(L, 1)) {
side_1 = t->side();
} else {
side_1 = luaL_checkinteger(L, 1);
}
if(team* t = luaW_toteam(L, 2)) {
side_2 = t->side();
} else {
side_2 = luaL_checkinteger(L, 2);
}
if (side_1 > teams().size() || side_2 > teams().size()) return 0;
lua_pushboolean(L, board().get_team(side_1).is_enemy(side_2));
return 1;
}
/**
* Gets whether gamemap scrolling is disabled for the user.
* - Ret 1: boolean.
*/
int game_lua_kernel::intf_view_locked(lua_State *L)
{
if (!game_display_) {
return 0;
}
lua_pushboolean(L, game_display_->view_locked());
return 1;
}
/**
* Sets whether gamemap scrolling is disabled for the user.
* - Arg 1: boolean, specifying the new locked/unlocked status.
*/
int game_lua_kernel::intf_lock_view(lua_State *L)
{
bool lock = luaW_toboolean(L, 1);
if (game_display_) {
game_display_->set_view_locked(lock);
}
return 0;
}
static void luaW_push_tod(lua_State* L, const time_of_day& tod)
{
lua_newtable(L);
lua_pushstring(L, tod.id.c_str());
lua_setfield(L, -2, "id");
lua_pushinteger(L, tod.lawful_bonus);
lua_setfield(L, -2, "lawful_bonus");
lua_pushinteger(L, tod.bonus_modified);
lua_setfield(L, -2, "bonus_modified");
lua_pushstring(L, tod.image.c_str());
lua_setfield(L, -2, "image");
luaW_pushtstring(L, tod.name);
lua_setfield(L, -2, "name");
lua_pushstring(L, tod.sounds.c_str());
lua_setfield(L, -2, "sound");
lua_pushstring(L, tod.image_mask.c_str());
lua_setfield(L, -2, "mask");
lua_pushinteger(L, tod.color.r);
lua_setfield(L, -2, "red");
lua_pushinteger(L, tod.color.g);
lua_setfield(L, -2, "green");
lua_pushinteger(L, tod.color.b);
lua_setfield(L, -2, "blue");
}
// A schedule object is an index with a special metatable.
// The global schedule uses index -1
void game_lua_kernel::luaW_push_schedule(lua_State* L, int area_index)
{
lua_newuserdatauv(L, 0, 1);
lua_pushinteger(L, area_index);
lua_setiuservalue(L, -2, 1);
if(luaL_newmetatable(L, "schedule")) {
static luaL_Reg const schedule_meta[] {
{"__index", &dispatch<&game_lua_kernel::impl_schedule_get>},
{"__newindex", &dispatch<&game_lua_kernel::impl_schedule_set>},
{"__dir", &dispatch<&game_lua_kernel::impl_schedule_dir>},
{"__len", &dispatch<&game_lua_kernel::impl_schedule_len>},
{ nullptr, nullptr }
};
luaL_setfuncs(L, schedule_meta, 0);
}
lua_setmetatable(L, -2);
}
static int luaW_check_schedule(lua_State* L, int idx)
{
int save_top = lua_gettop(L);
luaL_checkudata(L, idx, "schedule");
lua_getiuservalue(L, idx, 1);
int i = luaL_checkinteger(L, -1);
lua_settop(L, save_top);
return i;
}
struct schedule_tag {
game_lua_kernel& ref;
int area_index;
schedule_tag(game_lua_kernel& k) : ref(k) {}
auto& tod_man() const { return ref.tod_man(); }
};
#define SCHEDULE_GETTER(name, type) LATTR_GETTER(name, type, schedule_tag, sched)
#define SCHEDULE_SETTER(name, type) LATTR_SETTER(name, type, schedule_tag, sched)
#define SCHEDULE_VALID(name) LATTR_VALID(name, schedule_tag, sched)
luaW_Registry scheduleReg{"schedule"};
template<> struct lua_object_traits<schedule_tag> {
inline static auto metatable = "schedule";
inline static schedule_tag get(lua_State* L, int n) {
schedule_tag sched{lua_kernel_base::get_lua_kernel<game_lua_kernel>(L)};
sched.area_index = luaW_check_schedule(L, n);
return sched;
}
};
int game_lua_kernel::impl_schedule_get(lua_State *L)
{
int area_index = luaW_check_schedule(L, 1);
if(lua_isnumber(L, 2)) {
const auto& times = area_index < 0 ? tod_man().times() : tod_man().times(area_index);
int i = lua_tointeger(L, 2) - 1;
if(i < 0 || i >= static_cast<int>(times.size())) {
return luaL_argerror(L, 2, "invalid time of day index");
}
luaW_push_tod(L, times[i]);
return 1;
}
return scheduleReg.get(L);
}
int game_lua_kernel::impl_schedule_len(lua_State *L)
{
int area_index = luaW_check_schedule(L, 1);
const auto& times = area_index < 0 ? tod_man().times() : tod_man().times(area_index);
lua_pushinteger(L, times.size());
return 1;
}
int game_lua_kernel::impl_schedule_set(lua_State *L)
{
int area_index = luaW_check_schedule(L, 1);
if(lua_isnumber(L, 2)) {
std::vector<time_of_day> times = area_index < 0 ? tod_man().times() : tod_man().times(area_index);
int i = lua_tointeger(L, 2) - 1;
if(i < 0 || i >= static_cast<int>(times.size())) {
return luaL_argerror(L, 2, "invalid time of day index");
}
config time_cfg = luaW_checkconfig(L, 3);
times[i] = time_of_day(time_cfg);
if(area_index < 0) {
tod_man().replace_schedule(times);
} else {
tod_man().replace_local_schedule(times, area_index);
}
}
return scheduleReg.set(L);
}
int game_lua_kernel::impl_schedule_dir(lua_State *L) {
return scheduleReg.dir(L);
}
namespace {
SCHEDULE_GETTER("time_of_day", std::string) {
if(sched.area_index >= 0) {
return sched.tod_man().get_area_time_of_day(sched.area_index).id;
}
return sched.tod_man().get_time_of_day().id;
}
SCHEDULE_SETTER("time_of_day", std::string) {
const auto& times = sched.area_index < 0 ? sched.tod_man().times() : sched.tod_man().times(sched.area_index);
auto iter = std::find_if(times.begin(), times.end(), [&value](const time_of_day& tod) {
return tod.id == value;
});
if(iter == times.end()) {
std::ostringstream err;
err << "invalid time of day ID for ";
if(sched.area_index < 0) {
err << "global schedule";
} else {
const std::string& id = sched.tod_man().get_area_id(sched.area_index);
if(id.empty()) {
const auto& hexes = sched.tod_man().get_area_by_index(sched.area_index);
if(hexes.empty()) {
err << "anonymous empty time area";
} else {
err << "anonymous time area at (" << hexes.begin()->wml_x() << ',' << hexes.begin()->wml_y() << ")";
}
} else {
err << "time area with id=" << id;
}
}
lua_push(L, err.str());
throw lua_error(L);
}
int n = std::distance(times.begin(), iter);
if(sched.area_index < 0) {
sched.tod_man().set_current_time(n);
} else {
sched.tod_man().set_current_time(n, sched.area_index);
}
}
SCHEDULE_VALID("liminal_bonus") {
return sched.area_index < 0;
}
SCHEDULE_GETTER("liminal_bonus", utils::optional<int>) {
if(sched.area_index >= 0) return utils::nullopt;
return sched.tod_man().get_max_liminal_bonus();
}
SCHEDULE_SETTER("liminal_bonus", utils::optional<int>) {
if(sched.area_index >= 0) {
throw luaL_error(L, "liminal_bonus can only be set on the global schedule");
}
if(value) {
sched.tod_man().set_max_liminal_bonus(*value);
} else {
sched.tod_man().reset_max_liminal_bonus();
}
}
SCHEDULE_VALID("id") {
return sched.area_index >= 0;
}
SCHEDULE_GETTER("id", utils::optional<std::string>) {
if(sched.area_index < 0) return utils::nullopt;
return sched.tod_man().get_area_id(sched.area_index);
}
SCHEDULE_SETTER("id", std::string) {
if(sched.area_index < 0) {
throw luaL_error(L, "can't set id of global schedule");
}
sched.tod_man().set_area_id(sched.area_index, value);
}
SCHEDULE_VALID("hexes") {
return sched.area_index >= 0;
}
SCHEDULE_GETTER("hexes", utils::optional<std::set<map_location>>) {
if(sched.area_index < 0) return utils::nullopt;
return sched.tod_man().get_area_by_index(sched.area_index);
}
SCHEDULE_SETTER("hexes", std::set<map_location>) {
if(sched.area_index < 0) {
throw luaL_error(L, "can't set hexes of global schedule");
}
sched.tod_man().replace_area_locations(sched.area_index, value);
}
}
/**
* Gets details about a terrain.
* - Arg 1: terrain code string.
* - Ret 1: table.
*/
int game_lua_kernel::impl_get_terrain_info(lua_State *L)
{
char const *m = luaL_checkstring(L, 2);
t_translation::terrain_code t = t_translation::read_terrain_code(m);
if (t == t_translation::NONE_TERRAIN || !board().map().tdata()->is_known(t)) return 0;
const terrain_type& info = board().map().tdata()->get_terrain_info(t);
lua_newtable(L);
lua_pushstring(L, info.id().c_str());
lua_setfield(L, -2, "id");
luaW_pushtstring(L, info.name());
lua_setfield(L, -2, "name");
luaW_pushtstring(L, info.editor_name());
lua_setfield(L, -2, "editor_name");
luaW_pushtstring(L, info.description());
lua_setfield(L, -2, "description");
lua_push(L, info.icon_image());
lua_setfield(L, -2, "icon");
lua_push(L, info.editor_image());
lua_setfield(L, -2, "editor_image");
lua_pushinteger(L, info.light_bonus(0));
lua_setfield(L, -2, "light");
lua_pushboolean(L, info.is_village());
lua_setfield(L, -2, "village");
lua_pushboolean(L, info.is_castle());
lua_setfield(L, -2, "castle");
lua_pushboolean(L, info.is_keep());
lua_setfield(L, -2, "keep");
lua_pushinteger(L, info.gives_healing());
lua_setfield(L, -2, "healing");
// movement alias
lua_newtable(L);
int idx = 1;
for (const auto& terrain : info.mvt_type()) {
const terrain_type& base = board().map().tdata()->get_terrain_info(terrain);
if (!base.id().empty()) {
lua_pushstring(L, t_translation::write_terrain_code(base.number()).c_str());
lua_rawseti(L, -2, idx++);
}
}
lua_setfield(L, -2, "mvt_alias");
// defense alias
lua_newtable(L);
idx = 1;
for (const auto& terrain : info.def_type()) {
const terrain_type& base = board().map().tdata()->get_terrain_info(terrain);
if (!base.id().empty()) {
lua_pushstring(L, t_translation::write_terrain_code(base.number()).c_str());
lua_rawseti(L, -2, idx++);
}
}
lua_setfield(L, -2, "def_alias");
return 1;
}
/**
* Gets a list of known terrain codes.
* - Ret 1: array of terrain codes
*/
int game_lua_kernel::impl_get_terrain_list(lua_State *L)
{
auto codes = board().map().tdata()->list();
std::vector<std::string> terrains;
terrains.reserve(codes.size());
for(auto code : codes) {
terrains.push_back(t_translation::write_terrain_code(code));
}
lua_push(L, terrains);
return 1;
}
/**
* Gets time of day information.
* - Arg 1: schedule object, location, time area ID, or nil
* - Arg 2: optional turn number
* - Ret 1: table.
*/
template<bool consider_illuminates>
int game_lua_kernel::intf_get_time_of_day(lua_State *L)
{
int for_turn = tod_man().turn();
map_location loc = map_location();
if(luaW_tolocation(L, 1, loc)) {
if(!board().map().on_board_with_border(loc)) {
return luaL_argerror(L, 1, "coordinates are not on board");
}
} else if(lua_isstring(L, 1)) {
auto area = tod_man().get_area_by_id(lua_tostring(L, 1));
if(area.empty()) {
return luaL_error(L, "invalid or empty time_area ID");
}
// We just need SOME location in that area, it doesn't matter which one.
loc = *area.begin();
} else if(!lua_isnil(L, 1)) {
auto area = tod_man().get_area_by_index(luaW_check_schedule(L, 1));
if(area.empty()) {
return luaL_error(L, "empty time_area");
}
// We just need SOME location in that area, it doesn't matter which one.
loc = *area.begin();
}
if(lua_isnumber(L, 2)) {
for_turn = luaL_checkinteger(L, 2);
int number_of_turns = tod_man().number_of_turns();
if(for_turn < 1 || (number_of_turns != -1 && for_turn > number_of_turns)) {
return luaL_argerror(L, 2, "turn number out of range");
}
}
const time_of_day& tod = consider_illuminates ?
tod_man().get_illuminated_time_of_day(board().units(), board().map(), loc, for_turn) :
tod_man().get_time_of_day(loc, for_turn);
luaW_push_tod(L, tod);
return 1;
}
/**
* Gets the side of a village owner.
* - Arg 1: map location.
* - Ret 1: integer.
*/
int game_lua_kernel::intf_get_village_owner(lua_State *L)
{
map_location loc = luaW_checklocation(L, 1);
if (!board().map().is_village(loc))
return 0;
int side = board().village_owner(loc);
if (!side) return 0;
lua_pushinteger(L, side);
return 1;
}
/**
* Sets the owner of a village.
* - Arg 1: map location.
* - Arg 2: integer for the side or empty to remove ownership.
*/
int game_lua_kernel::intf_set_village_owner(lua_State *L)
{
map_location loc = luaW_checklocation(L, 1);
if(!board().map().is_village(loc)) {
return 0;
}
const int old_side_num = board().village_owner(loc);
const int new_side_num = lua_isnoneornil(L, 2) ? 0 : luaL_checkinteger(L, 2);
team* old_side = nullptr;
team* new_side = nullptr;
if(old_side_num == new_side_num) {
return 0;
}
try {
old_side = &board().get_team(old_side_num);
} catch(const std::out_of_range&) {
// old_side_num is invalid, most likely because the village wasn't captured.
old_side = nullptr;
}
try {
new_side = &board().get_team(new_side_num);
} catch(const std::out_of_range&) {
// new_side_num is invalid.
new_side = nullptr;
}
// The new side was valid, but already defeated. Do nothing.
if(new_side && board().team_is_defeated(*new_side)) {
return 0;
}
// Even if the new side is not valid, we still want to remove the village from the old side.
// This covers the case where new_side_num equals 0. The behavior in that case is to simply
// un-assign the village from the old side, which of course we also want to happen if the new
// side IS valid. If the village in question hadn't been captured, this won't fire (old_side
// will be a nullptr).
if(old_side) {
old_side->lose_village(loc);
}
// If the new side was valid, re-assign the village.
if(new_side) {
new_side->get_village(loc, old_side_num, (luaW_toboolean(L, 3) ? &gamedata() : nullptr));
}
return 0;
}
/**
* Returns the currently overed tile.
* - Ret 1: x.
* - Ret 2: y.
*/
int game_lua_kernel::intf_get_mouseover_tile(lua_State *L)
{
if (!game_display_) {
return 0;
}
const map_location &loc = game_display_->mouseover_hex();
if (!board().map().on_board(loc)) return 0;
lua_pushinteger(L, loc.wml_x());
lua_pushinteger(L, loc.wml_y());
return 2;
}
/**
* Returns the currently selected tile.
* - Ret 1: x.
* - Ret 2: y.
*/
int game_lua_kernel::intf_get_selected_tile(lua_State *L)
{
if (!game_display_) {
return 0;
}
const map_location &loc = game_display_->selected_hex();
if (!board().map().on_board(loc)) return 0;
lua_pushinteger(L, loc.wml_x());
lua_pushinteger(L, loc.wml_y());
return 2;
}
/**
* Gets a table for an resource tag.
* - Arg 1: userdata (ignored).
* - Arg 2: string containing id of the desired resource
* - Ret 1: config for the era
*/
static int intf_get_resource(lua_State *L)
{
std::string m = luaL_checkstring(L, 1);
if(auto res = game_config_manager::get()->game_config().find_child("resource","id",m)) {
luaW_pushconfig(L, *res);
return 1;
}
else {
return luaL_argerror(L, 1, ("Cannot find resource with id '" + m + "'").c_str());
}
}
/**
* Gets a table for an era tag.
* - Arg 1: userdata (ignored).
* - Arg 2: string containing id of the desired era
* - Ret 1: config for the era
*/
static int intf_get_era(lua_State *L)
{
std::string m = luaL_checkstring(L, 1);
if(auto res = game_config_manager::get()->game_config().find_child("era","id",m)) {
luaW_pushconfig(L, *res);
return 1;
}
else {
return luaL_argerror(L, 1, ("Cannot find era with id '" + m + "'").c_str());
}
return 1;
}
extern luaW_Registry& gameConfigReg();
static auto& dummy = gameConfigReg(); // just to ensure it's constructed.
struct game_config_glk_tag {
game_lua_kernel& ref;
game_config_glk_tag(lua_kernel_base& k) : ref(dynamic_cast<game_lua_kernel&>(k)) {}
auto& pc() const { return ref.play_controller_; }
auto& gamedata() const { return ref.gamedata(); }
auto& disp() const { return ref.game_display_; }
};
#define GAME_CONFIG_SIMPLE_SETTER(name) \
GAME_CONFIG_SETTER(#name, decltype(game_config::name), game_lua_kernel) { \
(void) k; \
game_config::name = value; \
}
namespace {
GAME_CONFIG_GETTER("do_healing", bool, game_lua_kernel) {
game_config_glk_tag k2{k.ref};
return k2.pc().gamestate().do_healing_;
}
GAME_CONFIG_SETTER("do_healing", bool, game_lua_kernel) {
game_config_glk_tag k2{k.ref};
k2.pc().gamestate().do_healing_ = value;}
GAME_CONFIG_GETTER("theme", std::string, game_lua_kernel) {
game_config_glk_tag k2{k.ref};
return k2.gamedata().get_theme();
}
GAME_CONFIG_SETTER("theme", std::string, game_lua_kernel) {
game_config_glk_tag k2{k.ref};
k2.gamedata().set_theme(value);
k2.disp()->set_theme(value);
}
using traits_map = std::map<std::string, config>;
GAME_CONFIG_GETTER("global_traits", traits_map, game_lua_kernel) {
(void)k;
std::map<std::string, config> result;
for(const config& trait : unit_types.traits()) {
//It seems the engine never checks the id field for emptiness or duplicates
//However, the worst that could happen is that the trait read later overwrites the older one,
//and this is not the right place for such checks.
result.emplace(trait["id"], trait);
}
return result;
}
GAME_CONFIG_SIMPLE_SETTER(base_income);
GAME_CONFIG_SIMPLE_SETTER(village_income);
GAME_CONFIG_SIMPLE_SETTER(village_support);
GAME_CONFIG_SIMPLE_SETTER(poison_amount);
GAME_CONFIG_SIMPLE_SETTER(rest_heal_amount);
GAME_CONFIG_SIMPLE_SETTER(recall_cost);
GAME_CONFIG_SIMPLE_SETTER(kill_experience);
GAME_CONFIG_SIMPLE_SETTER(combat_experience);
}
namespace {
static config find_addon(const std::string& type, const std::string& id)
{
return game_config_manager::get()->game_config().find_mandatory_child(type, "id", id);
}
}
static int impl_end_level_data_get(lua_State* L)
{
const end_level_data& data = *static_cast<end_level_data*>(lua_touserdata(L, 1));
const char* m = luaL_checkstring(L, 2);
return_bool_attrib("linger_mode", data.transient.linger_mode);
return_bool_attrib("reveal_map", data.transient.reveal_map);
return_bool_attrib("carryover_report", data.transient.carryover_report);
return_bool_attrib("prescenario_save", data.prescenario_save);
return_bool_attrib("replay_save", data.replay_save);
return_bool_attrib("proceed_to_next_level", data.proceed_to_next_level);
return_bool_attrib("is_victory", data.is_victory);
return_bool_attrib("is_loss", !data.is_victory);
return_cstring_attrib("result", data.is_victory ? level_result::victory : "loss"); // to match wesnoth.end_level()
return_string_attrib("test_result", data.test_result);
return_cfg_attrib("__cfg", data.to_config_full());
return 0;
}
namespace {
struct end_level_committer {
end_level_committer(end_level_data& data, play_controller& pc) : data_(data), pc_(pc) {}
~end_level_committer() {
pc_.set_end_level_data(data_);
}
private:
end_level_data& data_;
play_controller& pc_;
};
}
int game_lua_kernel::impl_end_level_data_set(lua_State* L)
{
end_level_data& data = *static_cast<end_level_data*>(lua_touserdata(L, 1));
const char* m = luaL_checkstring(L, 2);
end_level_committer commit(data, play_controller_);
modify_bool_attrib("linger_mode", data.transient.linger_mode = value);
modify_bool_attrib("reveal_map", data.transient.reveal_map = value);
modify_bool_attrib("carryover_report", data.transient.carryover_report = value);
modify_bool_attrib("prescenario_save", data.prescenario_save = value);
modify_bool_attrib("replay_save", data.replay_save = value);
modify_string_attrib("test_result", data.test_result = value);
return 0;
}
static int impl_end_level_data_collect(lua_State* L)
{
end_level_data* data = static_cast<end_level_data*>(lua_touserdata(L, 1));
data->~end_level_data();
return 0;
}
static int impl_mp_settings_get(lua_State* L)
{
void* p = lua_touserdata(L, lua_upvalueindex(1));
const mp_game_settings& settings = static_cast<play_controller*>(p)->get_mp_settings();
if(lua_type(L, 2) == LUA_TNUMBER) {
// Simulates a WML table with one [options] child and a variable number of [addon] children
// TODO: Deprecate this -> mp_settings.options and mp_settings.addons
size_t i = luaL_checkinteger(L, 2);
if(i == 1) {
lua_createtable(L, 2, 0);
lua_pushstring(L, "options");
lua_seti(L, -2, 1);
luaW_pushconfig(L, settings.options);
lua_seti(L, -2, 2);
return 1;
} else if(i >= 2) {
i -= 2;
if(i < settings.addons.size()) {
auto iter = settings.addons.begin();
std::advance(iter, i);
config cfg;
iter->second.write(cfg);
cfg["id"] = iter->first;
lua_createtable(L, 2, 0);
lua_pushstring(L, "addon");
lua_seti(L, -2, 1);
luaW_pushconfig(L, cfg);
lua_seti(L, -2, 2);
return 1;
}
}
} else {
char const *m = luaL_checkstring(L, 2);
return_string_attrib("scenario", settings.name);
return_string_attrib("game_name", settings.name);
return_string_attrib("hash", settings.hash);
return_string_attrib("mp_era_name", settings.mp_era_name);
return_string_attrib("mp_scenario", settings.mp_scenario);
return_string_attrib("mp_scenario_name", settings.mp_scenario_name);
return_string_attrib("mp_campaign", settings.mp_campaign);
return_string_attrib("side_users", utils::join_map(settings.side_users));
return_int_attrib("experience_modifier", settings.xp_modifier);
return_bool_attrib("mp_countdown", settings.mp_countdown);
return_int_attrib("mp_countdown_init_time", settings.mp_countdown_init_time.count());
return_int_attrib("mp_countdown_turn_bonus", settings.mp_countdown_turn_bonus.count());
return_int_attrib("mp_countdown_reservoir_bonus", settings.mp_countdown_reservoir_time.count());
return_int_attrib("mp_countdown_action_bonus", settings.mp_countdown_action_bonus.count());
return_int_attrib("mp_num_turns", settings.num_turns);
return_int_attrib("mp_village_gold", settings.village_gold);
return_int_attrib("mp_village_support", settings.village_support);
return_bool_attrib("mp_fog", settings.fog_game);
return_bool_attrib("mp_shroud", settings.shroud_game);
return_bool_attrib("mp_use_map_settings", settings.use_map_settings);
return_bool_attrib("mp_random_start_time", settings.random_start_time);
return_bool_attrib("observer", settings.allow_observers);
return_bool_attrib("allow_observers", settings.allow_observers);
return_bool_attrib("private_replay", settings.private_replay);
return_bool_attrib("shuffle_sides", settings.shuffle_sides);
return_string_attrib("random_faction_mode", random_faction_mode::get_string(settings.mode));
return_cfgref_attrib("options", settings.options);
if(strcmp(m, "savegame") == 0) {
auto savegame = settings.saved_game;
if(savegame == saved_game_mode::type::no) {
lua_pushboolean(L, false);
} else {
lua_push(L, saved_game_mode::get_string(savegame));
}
return 1;
}
if(strcmp(m, "side_players") == 0) {
lua_push(L, settings.side_users);
return 1;
}
if(strcmp(m, "addons") == 0) {
for(const auto& [id, addon] : settings.addons) {
lua_createtable(L, 0, 4);
lua_push(L, id);
lua_setfield(L, -2, "id");
lua_push(L, addon.name);
lua_setfield(L, -2, "name");
lua_pushboolean(L, addon.required);
lua_setfield(L, -2, "required");
if(addon.min_version) {
luaW_getglobal(L, "wesnoth", "version");
lua_push(L, addon.min_version->str());
lua_call(L, 1, 1);
lua_setfield(L, -2, "min_version");
}
if(addon.version) {
luaW_getglobal(L, "wesnoth", "version");
lua_push(L, addon.version->str());
lua_call(L, 1, 1);
lua_setfield(L, -2, "version");
}
lua_createtable(L, addon.content.size(), 0);
for(const auto& content : addon.content) {
lua_createtable(L, 0, 3);
lua_push(L, content.id);
lua_setfield(L, -2, "id");
lua_push(L, content.name);
lua_setfield(L, -2, "name");
lua_push(L, content.type);
lua_setfield(L, -2, "type");
lua_seti(L, -2, lua_rawlen(L, -2) + 1);
}
lua_setfield(L, -2, "content");
}
return 1;
}
// Deprecated things that were moved out of mp_settings and into game_classification
const game_classification& game = static_cast<play_controller*>(p)->get_classification();
return_string_attrib_deprecated("mp_era", "wesnoth.scenario.mp_settings", INDEFINITE, "1.17", "Use wesnoth.scenario.era.id instead", game.era_id);
return_string_attrib_deprecated("active_mods", "wesnoth.scenario.mp_settings", INDEFINITE, "1.17", "Use wesnoth.scenario.modifications instead (returns an array of modification tables)", utils::join(game.active_mods));
// Expose the raw config; this is a way to ensure any new stuff can be accessed even if someone forgot to add it here.
return_cfgref_attrib("__cfg", settings.to_config());
}
return 0;
}
static int impl_mp_settings_len(lua_State* L)
{
void* p = lua_touserdata(L, lua_upvalueindex(1));
const mp_game_settings& settings = static_cast<play_controller*>(p)->get_mp_settings();
lua_pushinteger(L, settings.addons.size() + 1);
return 1;
}
struct scenario_tag {
game_lua_kernel& ref;
scenario_tag(game_lua_kernel& k) : ref(k) {}
auto& tod_man() const { return ref.tod_man(); }
auto& gamedata() const { return ref.gamedata(); }
auto& pc() const { return ref.play_controller_; }
auto& cls() const { return ref.play_controller_.get_classification(); }
auto end_level_set() const { return &dispatch<&game_lua_kernel::impl_end_level_data_set>; }
};
#define SCENARIO_GETTER(name, type) LATTR_GETTER(name, type, scenario_tag, k)
#define SCENARIO_SETTER(name, type) LATTR_SETTER(name, type, scenario_tag, k)
#define SCENARIO_VALID(name) LATTR_VALID(name, scenario_tag, k)
luaW_Registry scenarioReg{"scenario"};
template<> struct lua_object_traits<scenario_tag> {
inline static auto metatable = "scenario";
inline static scenario_tag get(lua_State* L, int) {
return lua_kernel_base::get_lua_kernel<game_lua_kernel>(L);
}
};
namespace {
SCENARIO_GETTER("turns", int) {
return k.tod_man().number_of_turns();
}
SCENARIO_SETTER("turns", int) {
k.tod_man().set_number_of_turns_by_wml(value);
}
SCENARIO_GETTER("next", std::string) {
return k.gamedata().next_scenario();
}
SCENARIO_SETTER("next", std::string) {
k.gamedata().set_next_scenario(value);
}
SCENARIO_GETTER("id", std::string) {
return k.gamedata().get_id();
}
SCENARIO_GETTER("name", t_string) {
return k.pc().get_scenario_name();
}
SCENARIO_GETTER("defeat_music", std::vector<std::string>) {
return k.gamedata().get_defeat_music();
}
SCENARIO_SETTER("defeat_music", std::vector<std::string>) {
k.gamedata().set_defeat_music(value);
}
SCENARIO_GETTER("victory_music", std::vector<std::string>) {
return k.gamedata().get_victory_music();
}
SCENARIO_SETTER("victory_music", std::vector<std::string>) {
k.gamedata().set_victory_music(value);
}
SCENARIO_GETTER("resources", std::vector<config>) {
std::vector<config> resources;
for(const std::string& rsrc : utils::split(k.pc().get_loaded_resources())) {
resources.push_back(find_addon("resource", rsrc));
}
return resources;
}
SCENARIO_GETTER("type", std::string) {
return campaign_type::get_string(k.cls().type);
}
SCENARIO_GETTER("difficulty", std::string) {
return k.cls().difficulty;
}
SCENARIO_GETTER("show_credits", bool) {
return k.cls().end_credits;
}
SCENARIO_SETTER("show_credits", bool) {
k.cls().end_credits = value;
}
SCENARIO_GETTER("end_text", t_string) {
return k.cls().end_text;
}
SCENARIO_SETTER("end_text", t_string) {
k.cls().end_text = value;
}
SCENARIO_GETTER("end_text_duration", int) {
return k.cls().end_text_duration.count();
}
SCENARIO_SETTER("end_text_duration", int) {
k.cls().end_text_duration = std::chrono::milliseconds{value};
}
SCENARIO_VALID("campaign") {
return !k.cls().campaign.empty();
}
SCENARIO_GETTER("campaign", utils::optional<config>) {
if(k.cls().campaign.empty()) return utils::nullopt;
return find_addon("campaign", k.cls().campaign);
}
SCENARIO_GETTER("modifications", std::vector<config>) {
std::vector<config> mods;
for(const std::string& mod : k.cls().active_mods) {
mods.push_back(find_addon("modification", mod));
}
return mods;
}
SCENARIO_GETTER("end_level_data", lua_index_raw) {
if (!k.pc().is_regular_game_end()) {
lua_pushnil(L);
return lua_index_raw(L);
}
auto data = k.pc().get_end_level_data();
new(L) end_level_data(data);
if(luaL_newmetatable(L, "end level data")) {
static luaL_Reg const callbacks[] {
{ "__index", &impl_end_level_data_get},
{ "__newindex", k.end_level_set()},
{ "__gc", &impl_end_level_data_collect},
{ nullptr, nullptr }
};
luaL_setfuncs(L, callbacks, 0);
}
lua_setmetatable(L, -2);
return lua_index_raw(L);
}
SCENARIO_SETTER("end_level_data", vconfig) {
end_level_data data;
data.proceed_to_next_level = value["proceed_to_next_level"].to_bool(true);
data.transient.carryover_report = value["carryover_report"].to_bool(true);
data.prescenario_save = value["save"].to_bool(true);
data.replay_save = value["replay_save"].to_bool(true);
data.transient.linger_mode = value["linger_mode"].to_bool(true) && !k.ref.teams().empty();
data.transient.reveal_map = value["reveal_map"].to_bool(k.pc().reveal_map_default());
data.is_victory = value["result"] == level_result::victory;
data.test_result = value["test_result"].str();
k.pc().set_end_level_data(data);
}
SCENARIO_VALID("mp_settings") {
return k.cls().is_multiplayer();
}
SCENARIO_GETTER("mp_settings", lua_index_raw) {
if(!k.cls().is_multiplayer()) {
lua_pushnil(L);
return lua_index_raw(L);
}
lua_newuserdatauv(L, 0, 0);
if(luaL_newmetatable(L, "mp settings")) {
lua_pushlightuserdata(L, &k.pc());
lua_pushcclosure(L, impl_mp_settings_get, 1);
lua_setfield(L, -2, "__index");
lua_pushlightuserdata(L, &k.pc());
lua_pushcclosure(L, impl_mp_settings_len, 1);
lua_setfield(L, -2, "__len");
lua_pushstring(L, "mp settings");
lua_setfield(L, -2, "__metatable");
}
lua_setmetatable(L, -2);
return lua_index_raw(L);
}
SCENARIO_VALID("era") {
return k.cls().is_multiplayer();
}
SCENARIO_GETTER("era", utils::optional<config>) {
if(!k.cls().is_multiplayer()) return utils::nullopt;
return find_addon("era", k.cls().era_id);
}
}
/**
* Gets some scenario data (__index metamethod).
* - Arg 1: userdata (ignored).
* - Arg 2: string containing the name of the property.
* - Ret 1: something containing the attribute.
*/
int game_lua_kernel::impl_scenario_get(lua_State *L)
{
DBG_LUA << "impl_scenario_get";
return scenarioReg.get(L);
}
/**
* Sets some scenario data (__newindex metamethod).
* - Arg 1: userdata (ignored).
* - Arg 2: string containing the name of the property.
* - Arg 3: something containing the attribute.
*/
int game_lua_kernel::impl_scenario_set(lua_State *L)
{
DBG_LUA << "impl_scenario_set";
return scenarioReg.set(L);
}
/**
* Get a list of scenario data (__dir metamethod).
*/
int game_lua_kernel::impl_scenario_dir(lua_State *L)
{
DBG_LUA << "impl_scenario_dir";
return scenarioReg.dir(L);
}
/**
converts synced_context::get_synced_state() to a string.
*/
std::string game_lua_kernel::synced_state()
{
//maybe return "initial" for game_data::INITIAL?
if(gamedata().phase() == game_data::PRELOAD || gamedata().phase() == game_data::INITIAL)
{
return "preload";
}
switch(synced_context::get_synced_state())
{
case synced_context::LOCAL_CHOICE:
return "local_choice";
case synced_context::SYNCED:
return "synced";
case synced_context::UNSYNCED:
return "unsynced";
default:
throw game::game_error("Found corrupt synced_context::synced_state");
}
}
struct current_tag {
game_lua_kernel& ref;
current_tag(game_lua_kernel& k) : ref(k) {}
auto& pc() const { return ref.play_controller_; }
auto ss() const { return ref.synced_state(); }
auto& gd() const { return ref.gamedata(); }
auto& ev() const { return ref.get_event_info(); }
void push_schedule(lua_State* L) const { ref.luaW_push_schedule(L, -1); }
};
#define CURRENT_GETTER(name, type) LATTR_GETTER(name, type, current_tag, k)
luaW_Registry currentReg{"current"};
template<> struct lua_object_traits<current_tag> {
inline static auto metatable = "current";
inline static game_lua_kernel& get(lua_State* L, int) {
return lua_kernel_base::get_lua_kernel<game_lua_kernel>(L);
}
};
namespace {
CURRENT_GETTER("side", int) {
return k.pc().current_side();
}
CURRENT_GETTER("turn", int) {
return k.pc().turn();
}
CURRENT_GETTER("synced_state", std::string) {
return k.ss();
}
CURRENT_GETTER("user_can_invoke_commands", bool) {
return !events::commands_disabled && k.gd().phase() == game_data::TURN_PLAYING;
}
CURRENT_GETTER("map", lua_index_raw) {
(void)k;
intf_terrainmap_get(L);
return lua_index_raw(L);
}
CURRENT_GETTER("schedule", lua_index_raw) {
k.push_schedule(L);
return lua_index_raw(L);
}
CURRENT_GETTER("user_is_replaying", bool) {
return k.pc().is_replay();
}
CURRENT_GETTER("event_context", config) {
const game_events::queued_event &ev = k.ev();
config cfg;
cfg["name"] = ev.name;
cfg["id"] = ev.id;
cfg.add_child("data", ev.data);
if (auto weapon = ev.data.optional_child("first")) {
cfg.add_child("weapon", *weapon);
}
if (auto weapon = ev.data.optional_child("second")) {
cfg.add_child("second_weapon", *weapon);
}
const config::attribute_value di = ev.data["damage_inflicted"];
if(!di.empty()) {
cfg["damage_inflicted"] = di;
}
if (ev.loc1.valid()) {
cfg["x1"] = ev.loc1.filter_loc().wml_x();
cfg["y1"] = ev.loc1.filter_loc().wml_y();
// The position of the unit involved in this event, currently the only case where this is different from x1/y1 are enter/exit_hex events
cfg["unit_x"] = ev.loc1.wml_x();
cfg["unit_y"] = ev.loc1.wml_y();
}
if (ev.loc2.valid()) {
cfg["x2"] = ev.loc2.filter_loc().wml_x();
cfg["y2"] = ev.loc2.filter_loc().wml_y();
}
return cfg;
}
}
/**
* Gets some data about current point of game (__index metamethod).
* - Arg 1: userdata (ignored).
* - Arg 2: string containing the name of the property.
* - Ret 1: something containing the attribute.
*/
int game_lua_kernel::impl_current_get(lua_State *L)
{
return currentReg.get(L);
}
/**
* Gets a list of date about current point of game (__dir metamethod).
*/
int game_lua_kernel::impl_current_dir(lua_State *L)
{
return currentReg.dir(L);
}
/**
* Displays a message in the chat window and in the logs.
* - Arg 1: optional message header.
* - Arg 2 (or 1): message.
*/
int game_lua_kernel::intf_message(lua_State *L)
{
t_string m = luaW_checktstring(L, 1);
t_string h = m;
if (lua_isnone(L, 2)) {
h = "Lua";
} else {
m = luaW_checktstring(L, 2);
}
lua_chat(h, m);
LOG_LUA << "Script says: \"" << m << "\"";
return 0;
}
int game_lua_kernel::intf_zoom(lua_State* L)
{
if(!game_display_) {
return 0;
}
double factor = luaL_checknumber(L, 1);
bool relative = luaW_toboolean(L, 2);
if(relative) {
factor *= game_display_->get_zoom_factor();
}
// Passing true explicitly to avoid casting to int.
// Without doing one of the two, the call is ambiguous.
game_display_->set_zoom(factor * game_config::tile_size, true);
lua_pushnumber(L, game_display_->get_zoom_factor());
return 1;
}
/**
* Removes all messages from the chat window.
*/
int game_lua_kernel::intf_clear_messages(lua_State*)
{
if (game_display_) {
game_display_->get_chat_manager().clear_chat_messages();
}
return 0;
}
int game_lua_kernel::intf_end_turn(lua_State* L)
{
//note that next_player_number = 1, next_player_number = nteams+1 both set the next team to be the first team
//but the later will make the turn counter change aswell fire turn end events accoringly etc.
if (!lua_isnoneornil(L, 1)) {
int max = 2 * teams().size();
int npn = luaL_checkinteger(L, 1);
if (npn <= 0 || npn > max) {
return luaL_argerror(L, 1, "side number out of range");
}
resources::controller->gamestate().next_player_number_ = npn;
}
play_controller_.force_end_turn();
return 0;
}
/**
* Evaluates a boolean WML conditional.
* - Arg 1: WML table.
* - Ret 1: boolean.
*/
static int intf_eval_conditional(lua_State *L)
{
vconfig cond = luaW_checkvconfig(L, 1);
bool b = game_events::conditional_passed(cond);
lua_pushboolean(L, b);
return 1;
}
/**
* Finds a path between two locations.
* - Arg 1: source location. (Or Arg 1: unit.)
* - Arg 2: destination.
* - Arg 3: optional cost function or
* table (optional fields: ignore_units, ignore_teleport, max_cost, viewing_side).
* - Ret 1: array of pairs containing path steps.
* - Ret 2: path cost.
*/
int game_lua_kernel::intf_find_path(lua_State *L)
{
int arg = 1;
map_location src, dst;
const unit* u = nullptr;
int viewing_side = 0;
if (lua_isuserdata(L, arg))
{
u = &luaW_checkunit(L, arg);
src = u->get_location();
viewing_side = u->side();
++arg;
}
else
{
src = luaW_checklocation(L, arg);
unit_map::const_unit_iterator ui = units().find(src);
if (ui.valid()) {
u = ui.get_shared_ptr().get();
viewing_side = u->side();
}
++arg;
}
dst = luaW_checklocation(L, arg);
if (!board().map().on_board(src))
return luaL_argerror(L, 1, "invalid location");
if (!board().map().on_board(dst))
return luaL_argerror(L, arg, "invalid location");
++arg;
const gamemap &map = board().map();
bool ignore_units = false, see_all = false, ignore_teleport = false;
double stop_at = 10000;
std::unique_ptr<pathfind::cost_calculator> calc;
if (lua_istable(L, arg))
{
ignore_units = luaW_table_get_def<bool>(L, arg, "ignore_units", false);
see_all = luaW_table_get_def<bool>(L, arg, "ignore_visibility", false);
ignore_teleport = luaW_table_get_def<bool>(L, arg, "ignore_teleport", false);
stop_at = luaW_table_get_def<double>(L, arg, "max_cost", stop_at);
lua_pushstring(L, "viewing_side");
lua_rawget(L, arg);
if (!lua_isnil(L, -1)) {
int i = luaL_checkinteger(L, -1);
if (i >= 1 && i <= static_cast<int>(teams().size())) viewing_side = i;
else {
// If there's a unit, we have a valid side, so fall back to legacy behaviour.
// If we don't have a unit, legacy behaviour would be a crash, so let's not.
if(u) see_all = true;
deprecated_message("wesnoth.paths.find_path with viewing_side=0 (or an invalid side)", DEP_LEVEL::FOR_REMOVAL, {1, 17, 0}, "To consider fogged and hidden units, use ignore_visibility=true instead.");
}
}
lua_pop(L, 1);
lua_pushstring(L, "calculate");
lua_rawget(L, arg);
if(lua_isfunction(L, -1)) {
calc.reset(new lua_pathfind_cost_calculator(L, lua_gettop(L)));
}
// Don't pop, the lua_pathfind_cost_calculator requires it to stay on the stack.
}
else if (lua_isfunction(L, arg))
{
deprecated_message("wesnoth.paths.find_path with cost_function as last argument", DEP_LEVEL::FOR_REMOVAL, {1, 17, 0}, "Use calculate=cost_function inside the path options table instead.");
calc.reset(new lua_pathfind_cost_calculator(L, arg));
}
pathfind::teleport_map teleport_locations;
if(!ignore_teleport) {
if(viewing_side == 0) {
lua_warning(L, "wesnoth.paths.find_path: ignore_teleport=false requires a valid viewing_side; continuing with ignore_teleport=true", false);
ignore_teleport = true;
} else {
teleport_locations = pathfind::get_teleport_locations(*u, board().get_team(viewing_side), see_all, ignore_units);
}
}
if (!calc) {
if(!u) {
return luaL_argerror(L, 1, "unit not found OR custom cost function not provided");
}
calc.reset(new pathfind::shortest_path_calculator(*u, board().get_team(viewing_side),
teams(), map, ignore_units, false, see_all));
}
pathfind::plain_route res = pathfind::a_star_search(src, dst, stop_at, *calc, map.w(), map.h(),
&teleport_locations);
int nb = res.steps.size();
lua_createtable(L, nb, 0);
for (int i = 0; i < nb; ++i)
{
luaW_pushlocation(L, res.steps[i]);
lua_rawseti(L, -2, i + 1);
}
lua_pushinteger(L, res.move_cost);
return 2;
}
/**
* Finds all the locations reachable by a unit.
* - Arg 1: source location OR unit.
* - Arg 2: optional table (optional fields: ignore_units, ignore_teleport, additional_turns, viewing_side).
* - Ret 1: array of triples (coordinates + remaining movement).
*/
int game_lua_kernel::intf_find_reach(lua_State *L)
{
int arg = 1;
const unit* u = nullptr;
if (lua_isuserdata(L, arg))
{
u = &luaW_checkunit(L, arg);
++arg;
}
else
{
map_location src = luaW_checklocation(L, arg);
unit_map::const_unit_iterator ui = units().find(src);
if (!ui.valid())
return luaL_argerror(L, 1, "unit not found");
u = ui.get_shared_ptr().get();
++arg;
}
int viewing_side = u->side();
bool ignore_units = false, see_all = false, ignore_teleport = false;
int additional_turns = 0;
if (lua_istable(L, arg))
{
ignore_units = luaW_table_get_def<bool>(L, arg, "ignore_units", false);
see_all = luaW_table_get_def<bool>(L, arg, "ignore_visibility", false);
ignore_teleport = luaW_table_get_def<bool>(L, arg, "ignore_teleport", false);
additional_turns = luaW_table_get_def<int>(L, arg, "max_cost", additional_turns);
lua_pushstring(L, "viewing_side");
lua_rawget(L, arg);
if (!lua_isnil(L, -1)) {
int i = luaL_checkinteger(L, -1);
if (i >= 1 && i <= static_cast<int>(teams().size())) viewing_side = i;
else {
// If there's a unit, we have a valid side, so fall back to legacy behaviour.
// If we don't have a unit, legacy behaviour would be a crash, so let's not.
if(u) see_all = true;
deprecated_message("wesnoth.find_reach with viewing_side=0 (or an invalid side)", DEP_LEVEL::FOR_REMOVAL, {1, 17, 0}, "To consider fogged and hidden units, use ignore_visibility=true instead.");
}
}
lua_pop(L, 1);
}
const team& viewing_team = board().get_team(viewing_side);
pathfind::paths res(*u, ignore_units, !ignore_teleport,
viewing_team, additional_turns, see_all, ignore_units);
int nb = res.destinations.size();
lua_createtable(L, nb, 0);
for (int i = 0; i < nb; ++i)
{
pathfind::paths::step &s = res.destinations[i];
luaW_push_namedtuple(L, {"x", "y", "moves_left"});
lua_pushinteger(L, s.curr.wml_x());
lua_rawseti(L, -2, 1);
lua_pushinteger(L, s.curr.wml_y());
lua_rawseti(L, -2, 2);
lua_pushinteger(L, s.move_left);
lua_rawseti(L, -2, 3);
lua_rawseti(L, -2, i + 1);
}
return 1;
}
/**
* Finds all the locations for which a given unit would remove the fog (if there was fog on the map).
*
* - Arg 1: source location OR unit.
* - Ret 1: array of triples (coordinates + remaining vision points).
*/
int game_lua_kernel::intf_find_vision_range(lua_State *L)
{
int arg = 1;
const unit* u = nullptr;
if (lua_isuserdata(L, arg))
{
u = &luaW_checkunit(L, arg);
++arg;
}
else
{
map_location src = luaW_checklocation(L, arg);
unit_map::const_unit_iterator ui = units().find(src);
if (!ui.valid())
return luaL_argerror(L, 1, "unit not found");
u = ui.get_shared_ptr().get();
++arg;
}
if(!u)
{
return luaL_error(L, "wesnoth.find_vision_range: requires a valid unit");
}
std::map<map_location, int> jamming_map;
actions::create_jamming_map(jamming_map, resources::gameboard->get_team(u->side()));
pathfind::vision_path res(*u, u->get_location(), jamming_map);
lua_createtable(L, res.destinations.size() + res.edges.size(), 0);
for(const auto& d : res.destinations) {
luaW_push_namedtuple(L, {"x", "y", "vision_left"});
lua_pushinteger(L, d.curr.wml_x());
lua_rawseti(L, -2, 1);
lua_pushinteger(L, d.curr.wml_y());
lua_rawseti(L, -2, 2);
lua_pushinteger(L, d.move_left);
lua_rawseti(L, -2, 3);
lua_rawseti(L, -2, lua_rawlen(L, -2) + 1);
}
for(const auto& e : res.edges) {
luaW_push_namedtuple(L, {"x", "y", "vision_left"});
lua_pushinteger(L, e.wml_x());
lua_rawseti(L, -2, 1);
lua_pushinteger(L, e.wml_y());
lua_rawseti(L, -2, 2);
lua_pushinteger(L, -1);
lua_rawseti(L, -2, 3);
lua_rawseti(L, -2, lua_rawlen(L, -2) + 1);
}
return 1;
}
template<typename T> // This is only a template so I can avoid typing out the long typename. >_>
static int load_fake_units(lua_State* L, int arg, T& fake_units)
{
for (int i = 1, i_end = lua_rawlen(L, arg); i <= i_end; ++i)
{
map_location src;
lua_rawgeti(L, arg, i);
int entry = lua_gettop(L);
if (!lua_istable(L, entry)) {
goto error;
}
if (!luaW_tolocation(L, entry, src)) {
goto error;
}
lua_rawgeti(L, entry, 3);
if (!lua_isnumber(L, -1)) {
lua_getfield(L, entry, "side");
if (!lua_isnumber(L, -1)) {
goto error;
}
}
int side = lua_tointeger(L, -1);
lua_rawgeti(L, entry, 4);
if (!lua_isstring(L, -1)) {
lua_getfield(L, entry, "type");
if (!lua_isstring(L, -1)) {
goto error;
}
}
std::string unit_type = lua_tostring(L, -1);
fake_units.emplace_back(src, side, unit_type);
lua_settop(L, entry - 1);
}
return 0;
error:
return luaL_argerror(L, arg, "unit type table malformed - each entry should be either array of 4 elements or table with keys x, y, side, type");
}
/**
* Is called with one or more units and builds a cost map.
* - Arg 1: source location. (Or Arg 1: unit. Or Arg 1: table containing a filter)
* - Arg 2: optional array of tables with 4 elements (coordinates + side + unit type string)
* - Arg 3: optional table (optional fields: ignore_units, ignore_teleport, viewing_side, debug).
* - Arg 4: optional table: standard location filter.
* - Ret 1: array of triples (coordinates + array of tuples(summed cost + reach counter)).
*/
int game_lua_kernel::intf_find_cost_map(lua_State *L)
{
int arg = 1;
unit* unit = luaW_tounit(L, arg, true);
vconfig filter = vconfig::unconstructed_vconfig();
luaW_tovconfig(L, arg, filter);
std::vector<const ::unit*> real_units;
typedef std::vector<std::tuple<map_location, int, std::string>> unit_type_vector;
unit_type_vector fake_units;
if (unit) // 1. arg - unit
{
real_units.push_back(unit);
}
else if (!filter.null()) // 1. arg - filter
{
for(const ::unit* match : unit_filter(filter).all_matches_on_map()) {
if(match->get_location().valid()) {
real_units.push_back(match);
}
}
}
else // 1. arg - coordinates
{
map_location src = luaW_checklocation(L, arg);
unit_map::const_unit_iterator ui = units().find(src);
if (ui.valid())
{
real_units.push_back(&(*ui));
}
}
++arg;
if (lua_istable(L, arg)) // 2. arg - optional types
{
load_fake_units(L, arg, fake_units);
++arg;
}
if(real_units.empty() && fake_units.empty())
{
return luaL_argerror(L, 1, "unit(s) not found");
}
int viewing_side = 0;
bool ignore_units = true, see_all = true, ignore_teleport = false, debug = false, use_max_moves = false;
if (lua_istable(L, arg)) // 4. arg - options
{
lua_pushstring(L, "ignore_units");
lua_rawget(L, arg);
if (!lua_isnil(L, -1))
{
ignore_units = luaW_toboolean(L, -1);
}
lua_pop(L, 1);
lua_pushstring(L, "ignore_teleport");
lua_rawget(L, arg);
if (!lua_isnil(L, -1))
{
ignore_teleport = luaW_toboolean(L, -1);
}
lua_pop(L, 1);
lua_pushstring(L, "viewing_side");
lua_rawget(L, arg);
if (!lua_isnil(L, -1))
{
int i = luaL_checkinteger(L, -1);
if (i >= 1 && i <= static_cast<int>(teams().size()))
{
viewing_side = i;
see_all = false;
}
}
lua_pushstring(L, "debug");
lua_rawget(L, arg);
if (!lua_isnil(L, -1))
{
debug = luaW_toboolean(L, -1);
}
lua_pop(L, 1);
lua_pushstring(L, "use_max_moves");
lua_rawget(L, arg);
if (!lua_isnil(L, -1))
{
use_max_moves = luaW_toboolean(L, -1);
}
lua_pop(L, 1);
++arg;
}
// 5. arg - location filter
filter = vconfig::unconstructed_vconfig();
std::set<map_location> location_set;
luaW_tovconfig(L, arg, filter);
if (filter.null())
{
filter = vconfig(config(), true);
}
filter_context & fc = game_state_;
const terrain_filter t_filter(filter, &fc, false);
t_filter.get_locations(location_set, true);
++arg;
// build cost_map
const team& viewing_team = viewing_side
? board().get_team(viewing_side)
: board().teams()[0];
pathfind::full_cost_map cost_map(
ignore_units, !ignore_teleport, viewing_team, see_all, ignore_units);
for (const ::unit* const u : real_units)
{
cost_map.add_unit(*u, use_max_moves);
}
for (const unit_type_vector::value_type& fu : fake_units)
{
const unit_type* ut = unit_types.find(std::get<2>(fu));
cost_map.add_unit(std::get<0>(fu), ut, std::get<1>(fu));
}
if (debug)
{
if (game_display_) {
game_display_->labels().clear_all();
for (const map_location& loc : location_set)
{
std::stringstream s;
s << cost_map.get_pair_at(loc).first;
s << " / ";
s << cost_map.get_pair_at(loc).second;
game_display_->labels().set_label(loc, s.str());
}
}
}
// create return value
lua_createtable(L, location_set.size(), 0);
int counter = 1;
for (const map_location& loc : location_set)
{
luaW_push_namedtuple(L, {"x", "y", "cost", "reach"});
lua_pushinteger(L, loc.wml_x());
lua_rawseti(L, -2, 1);
lua_pushinteger(L, loc.wml_y());
lua_rawseti(L, -2, 2);
lua_pushinteger(L, cost_map.get_pair_at(loc).first);
lua_rawseti(L, -2, 3);
lua_pushinteger(L, cost_map.get_pair_at(loc).second);
lua_rawseti(L, -2, 4);
lua_rawseti(L, -2, counter);
++counter;
}
return 1;
}
const char* labelKey = "floating label";
static int* luaW_check_floating_label(lua_State* L, int idx)
{
return reinterpret_cast<int*>(luaL_checkudata(L, idx, labelKey));
}
static int impl_floating_label_getmethod(lua_State* L)
{
const char* m = luaL_checkstring(L, 2);
return_bool_attrib("valid", *luaW_check_floating_label(L, 1) != 0);
return luaW_getmetafield(L, 1, m);
}
int game_lua_kernel::intf_remove_floating_label(lua_State* L)
{
int* handle = luaW_check_floating_label(L, 1);
std::chrono::milliseconds fade{luaL_optinteger(L, 2, -1)};
if(*handle != 0) {
// Passing -1 as the second argument means it uses the fade time that was set when the label was created
font::remove_floating_label(*handle, fade);
}
*handle = 0;
return 0;
}
int game_lua_kernel::intf_move_floating_label(lua_State* L)
{
int* handle = luaW_check_floating_label(L, 1);
if(*handle != 0) {
font::move_floating_label(*handle, luaL_checknumber(L, 2), luaL_checknumber(L, 3));
}
return 0;
}
/**
* Arg 1: text - string
* Arg 2: options table
* - size: font size
* - max_width: max width for word wrapping
* - color: font color
* - bgcolor: background color
* - bgalpha: background opacity
* - duration: display duration (integer or the string "unlimited")
* - fade_time: duration of fade-out
* - location: screen offset
* - valign: vertical alignment and anchoring - "top", "center", or "bottom"
* - halign: horizontal alignment and anchoring - "left", "center", or "right"
* Returns: label handle
*/
int game_lua_kernel::intf_set_floating_label(lua_State* L, bool spawn)
{
t_string text = luaW_checktstring(L, 1);
int size = font::SIZE_SMALL;
int width = 0;
double width_ratio = 0;
color_t color = font::LABEL_COLOR, bgcolor{0, 0, 0, 0};
int lifetime = 2'000, fadeout = 100;
font::ALIGN alignment = font::ALIGN::CENTER_ALIGN, vertical_alignment = font::ALIGN::CENTER_ALIGN;
// This is actually a relative screen location in pixels, but map_location already supports
// everything needed to read in a pair of coordinates.
// Depending on the chosen alignment, it may be relative to centre, an edge centre, or a corner.
map_location loc{0, 0, wml_loc()};
if(lua_istable(L, 2)) {
if(luaW_tableget(L, 2, "size")) {
size = luaL_checkinteger(L, -1);
}
if(luaW_tableget(L, 2, "max_width")) {
int found_number;
width = lua_tointegerx(L, -1, &found_number);
if(!found_number) {
auto value = luaW_tostring(L, -1);
try {
if(!value.empty() && value.back() == '%') {
value.remove_suffix(1);
width_ratio = std::stoi(std::string(value)) / 100.0;
} else throw std::invalid_argument(value.data());
} catch(std::invalid_argument&) {
return luaL_argerror(L, -1, "max_width should be integer or percentage");
}
}
}
if(luaW_tableget(L, 2, "color")) {
if(lua_isstring(L, -1)) {
color = color_t::from_hex_string(lua_tostring(L, -1));
} else {
auto vec = lua_check<std::vector<int>>(L, -1);
if(vec.size() != 3) {
int idx = lua_absindex(L, -1);
if(luaW_tableget(L, idx, "r") && luaW_tableget(L, idx, "g") && luaW_tableget(L, idx, "b")) {
color.r = luaL_checkinteger(L, -3);
color.g = luaL_checkinteger(L, -2);
color.b = luaL_checkinteger(L, -1);
} else {
return luaL_error(L, "floating label text color should be a hex string, an array of 3 integers, or a table with r,g,b keys");
}
} else {
color.r = vec[0];
color.g = vec[1];
color.b = vec[2];
}
}
}
if(luaW_tableget(L, 2, "bgcolor")) {
if(lua_isstring(L, -1)) {
bgcolor = color_t::from_hex_string(lua_tostring(L, -1));
} else {
auto vec = lua_check<std::vector<int>>(L, -1);
if(vec.size() != 3) {
int idx = lua_absindex(L, -1);
if(luaW_tableget(L, idx, "r") && luaW_tableget(L, idx, "g") && luaW_tableget(L, idx, "b")) {
bgcolor.r = luaL_checkinteger(L, -3);
bgcolor.g = luaL_checkinteger(L, -2);
bgcolor.b = luaL_checkinteger(L, -1);
} else {
return luaL_error(L, "floating label background color should be a hex string, an array of 3 integers, or a table with r,g,b keys");
}
} else {
bgcolor.r = vec[0];
bgcolor.g = vec[1];
bgcolor.b = vec[2];
}
bgcolor.a = ALPHA_OPAQUE;
}
if(luaW_tableget(L, 2, "bgalpha")) {
bgcolor.a = luaL_checkinteger(L, -1);
}
}
if(luaW_tableget(L, 2, "duration")) {
int found_number;
lifetime = lua_tointegerx(L, -1, &found_number);
if(!found_number) {
auto value = luaW_tostring(L, -1);
if(value == "unlimited") {
lifetime = -1;
} else {
return luaL_argerror(L, -1, "duration should be integer or 'unlimited'");
}
}
}
if(luaW_tableget(L, 2, "fade_time")) {
fadeout = lua_tointeger(L, -1);
}
if(luaW_tableget(L, 2, "location")) {
loc = luaW_checklocation(L, -1);
}
if(luaW_tableget(L, 2, "halign")) {
static const char* options[] = {"left", "center", "right"};
alignment = font::ALIGN(luaL_checkoption(L, -1, nullptr, options));
}
if(luaW_tableget(L, 2, "valign")) {
static const char* options[] = {"top", "center", "bottom"};
vertical_alignment = font::ALIGN(luaL_checkoption(L, -1, nullptr, options));
}
}
int* handle = nullptr;
if(spawn) {
// Creating a new label, allocate a new handle
handle = new(L)int();
} else {
// First argument is the label handle
handle = luaW_check_floating_label(L, 1);
}
int handle_idx = lua_gettop(L);
if(*handle != 0) {
font::remove_floating_label(*handle);
}
const SDL_Rect rect = game_display_->map_outside_area();
if(width_ratio > 0) {
width = static_cast<int>(std::round(rect.w * width_ratio));
}
int x = 0, y = 0;
switch(alignment) {
case font::ALIGN::LEFT_ALIGN:
x = rect.x + loc.wml_x();
break;
case font::ALIGN::CENTER_ALIGN:
x = rect.x + rect.w / 2 + loc.wml_x();
break;
case font::ALIGN::RIGHT_ALIGN:
x = rect.x + rect.w - loc.wml_x();
break;
}
switch(vertical_alignment) {
case font::ALIGN::LEFT_ALIGN: // top
y = rect.y + loc.wml_y();
break;
case font::ALIGN::CENTER_ALIGN:
y = rect.y + rect.h / 2 + loc.wml_y();
break;
case font::ALIGN::RIGHT_ALIGN: // bottom
// The size * 1.5 adjustment avoids the text being cut off if placed at y = 0
// This is necessary because the text is positioned by the top edge but we want it to
// seem like it's positioned by the bottom edge.
// This wouldn't work for multiline text, but we don't expect that to be common in this API anyway.
y = rect.y + rect.h - loc.wml_y() - static_cast<int>(size * 1.5);
break;
}
using std::chrono::milliseconds;
font::floating_label flabel(text);
flabel.set_font_size(size);
flabel.set_color(color);
flabel.set_bg_color(bgcolor);
flabel.set_alignment(alignment);
flabel.set_position(x, y);
flabel.set_lifetime(milliseconds{lifetime}, milliseconds{fadeout});
flabel.set_clip_rect(rect);
// Adjust fallback width (map area) to avoid text escaping when location != 0
if(width > 0) {
flabel.set_width(std::min(width, rect.w - loc.wml_x()));
} else {
flabel.set_width(rect.w - loc.wml_x());
}
*handle = font::add_floating_label(flabel);
lua_settop(L, handle_idx);
if(luaL_newmetatable(L, labelKey)) {
// Initialize the metatable
static const luaL_Reg methods[] = {
{"remove", &dispatch<&game_lua_kernel::intf_remove_floating_label>},
{"move", &dispatch<&game_lua_kernel::intf_move_floating_label>},
{"replace", &dispatch2<&game_lua_kernel::intf_set_floating_label, false>},
{"__index", &impl_floating_label_getmethod},
{ nullptr, nullptr }
};
luaL_setfuncs(L, methods, 0);
luaW_table_set(L, -1, "__metatable", std::string(labelKey));
}
lua_setmetatable(L, handle_idx);
lua_settop(L, handle_idx);
return 1;
}
void game_lua_kernel::put_unit_helper(const map_location& loc)
{
if(game_display_) {
game_display_->invalidate(loc);
}
resources::whiteboard->on_kill_unit();
}
/**
* Places a unit on the map.
* - Arg 1: (optional) location.
* - Arg 2: Unit (WML table or proxy), or nothing/nil to delete.
* OR
* - Arg 1: Unit (WML table or proxy)
* - Arg 2: (optional) location
* - Arg 3: (optional) boolean
*/
int game_lua_kernel::intf_put_unit(lua_State *L)
{
if(map_locked_) {
return luaL_error(L, "Attempted to move a unit while the map is locked");
}
map_location loc;
if (luaW_tolocation(L, 2, loc)) {
if (!map().on_board(loc)) {
return luaL_argerror(L, 2, "invalid location");
}
}
if((luaW_isunit(L, 1))) {
lua_unit& u = *luaW_checkunit_ref(L, 1);
if(u.on_map() && u->get_location() == loc) {
return 0;
}
if (!loc.valid()) {
loc = u->get_location();
if (!map().on_board(loc))
return luaL_argerror(L, 1, "invalid location");
}
put_unit_helper(loc);
u.put_map(loc);
u.get_shared()->anim_comp().set_standing();
} else if(!lua_isnoneornil(L, 1)) {
const vconfig* vcfg = nullptr;
config cfg = luaW_checkconfig(L, 1, vcfg);
if (!map().on_board(loc)) {
loc.set_wml_x(cfg["x"].to_int());
loc.set_wml_y(cfg["y"].to_int());
if (!map().on_board(loc))
return luaL_argerror(L, 2, "invalid location");
}
unit_ptr u = unit::create(cfg, true, vcfg);
units().erase(loc);
put_unit_helper(loc);
u->set_location(loc);
units().insert(u);
}
// Fire event if using the deprecated version or if the final argument is not false
// If the final boolean argument is omitted, the actual final argument (the unit or location) will always yield true.
if(luaW_toboolean(L, -1)) {
play_controller_.pump().fire("unit_placed", loc);
}
return 0;
}
/**
* Erases a unit from the map
* - Arg 1: Unit to erase OR Location to erase unit
*/
int game_lua_kernel::intf_erase_unit(lua_State *L)
{
if(map_locked_) {
return luaL_error(L, "Attempted to remove a unit while the map is locked");
}
map_location loc;
if(luaW_isunit(L, 1)) {
lua_unit& u = *luaW_checkunit_ref(L, 1);
if (u.on_map()) {
loc = u->get_location();
if (!map().on_board(loc)) {
return luaL_argerror(L, 1, "invalid location");
}
} else if (int side = u.on_recall_list()) {
team &t = board().get_team(side);
// Should it use underlying ID instead?
t.recall_list().erase_if_matches_id(u->id());
} else {
return luaL_argerror(L, 1, "can't erase private units");
}
} else if (luaW_tolocation(L, 1, loc)) {
if (!map().on_board(loc)) {
return luaL_argerror(L, 1, "invalid location");
}
} else {
return luaL_argerror(L, 1, "expected unit or location");
}
units().erase(loc);
resources::whiteboard->on_kill_unit();
return 0;
}
/**
* Puts a unit on a recall list.
* - Arg 1: WML table or unit.
* - Arg 2: (optional) side.
*/
int game_lua_kernel::intf_put_recall_unit(lua_State *L)
{
if(map_locked_) {
return luaL_error(L, "Attempted to move a unit while the map is locked");
}
lua_unit *lu = nullptr;
unit_ptr u = unit_ptr();
int side = lua_tointeger(L, 2);
if (static_cast<unsigned>(side) > teams().size()) side = 0;
if(luaW_isunit(L, 1)) {
lu = luaW_checkunit_ref(L, 1);
u = lu->get_shared();
if(lu->on_recall_list() && lu->on_recall_list() == side) {
return luaL_argerror(L, 1, "unit already on recall list");
}
} else {
const vconfig* vcfg = nullptr;
config cfg = luaW_checkconfig(L, 1, vcfg);
u = unit::create(cfg, true, vcfg);
}
if (!side) {
side = u->side();
} else {
u->set_side(side);
}
team &t = board().get_team(side);
// Avoid duplicates in the recall list.
std::size_t uid = u->underlying_id();
t.recall_list().erase_by_underlying_id(uid);
t.recall_list().add(u);
if (lu) {
if (lu->on_map()) {
units().erase(u->get_location());
resources::whiteboard->on_kill_unit();
u->anim_comp().clear_haloes();
}
lu->lua_unit::~lua_unit();
new(lu) lua_unit(side, uid);
}
return 0;
}
/**
* Extracts a unit from the map or a recall list and gives it to Lua.
* - Arg 1: unit userdata.
*/
int game_lua_kernel::intf_extract_unit(lua_State *L)
{
if(map_locked_) {
return luaL_error(L, "Attempted to remove a unit while the map is locked");
}
lua_unit* lu = luaW_checkunit_ref(L, 1);
unit_ptr u = lu->get_shared();
if (lu->on_map()) {
u = units().extract(u->get_location());
assert(u);
u->anim_comp().clear_haloes();
} else if (int side = lu->on_recall_list()) {
team &t = board().get_team(side);
unit_ptr v = u->clone();
t.recall_list().erase_if_matches_id(u->id());
u = v;
} else {
return 0;
}
lu->lua_unit::~lua_unit();
new(lu) lua_unit(u);
return 0;
}
/**
* Finds a vacant tile.
* - Arg 1: location.
* - Arg 2: optional unit for checking movement type.
* - Rets 1,2: location.
*/
int game_lua_kernel::intf_find_vacant_tile(lua_State *L)
{
map_location loc = luaW_checklocation(L, 1);
unit_ptr u;
if (!lua_isnoneornil(L, 2)) {
if(luaW_isunit(L, 2)) {
u = luaW_checkunit_ptr(L, 2, false);
} else {
const vconfig* vcfg = nullptr;
config cfg = luaW_checkconfig(L, 2, vcfg);
u = unit::create(cfg, false, vcfg);
}
}
map_location res = find_vacant_tile(loc, pathfind::VACANT_ANY, u.get());
if (!res.valid()) return 0;
lua_pushinteger(L, res.wml_x());
lua_pushinteger(L, res.wml_y());
return 2;
}
/**
* Floats some text on the map.
* - Arg 1: location.
* - Arg 2: string.
* - Arg 3: color.
*/
int game_lua_kernel::intf_float_label(lua_State *L)
{
map_location loc = luaW_checklocation(L, 1);
color_t color = font::LABEL_COLOR;
t_string text = luaW_checktstring(L, 2);
if (!lua_isnoneornil(L, 3)) {
color = color_t::from_rgb_string(luaL_checkstring(L, 3));
}
if (game_display_) {
game_display_->float_label(loc, text, color);
}
return 0;
}
/**
* Creates a unit from its WML description.
* - Arg 1: WML table.
* - Ret 1: unit userdata.
*/
static int intf_create_unit(lua_State *L)
{
const vconfig* vcfg = nullptr;
config cfg = luaW_checkconfig(L, 1, vcfg);
unit_ptr u = unit::create(cfg, true, vcfg);
luaW_pushunit(L, u);
return 1;
}
/**
* Copies a unit.
* - Arg 1: unit userdata.
* - Ret 1: unit userdata.
*/
static int intf_copy_unit(lua_State *L)
{
unit& u = luaW_checkunit(L, 1);
luaW_pushunit(L, u.clone());
return 1;
}
/**
* Returns unit resistance against a given attack type.
* - Arg 1: unit userdata.
* - Arg 2: string containing the attack type.
* - Arg 3: boolean indicating if attacker.
* - Arg 4: optional location.
* - Ret 1: integer.
*/
static int intf_unit_resistance(lua_State *L)
{
const unit& u = luaW_checkunit(L, 1);
char const *m = luaL_checkstring(L, 2);
bool a = false;
map_location loc = u.get_location();
if(lua_isboolean(L, 3)) {
a = luaW_toboolean(L, 3);
if(!lua_isnoneornil(L, 4)) {
loc = luaW_checklocation(L, 4);
}
} else if(!lua_isnoneornil(L, 3)) {
loc = luaW_checklocation(L, 3);
}
lua_pushinteger(L, 100 - u.resistance_against(m, a, loc));
return 1;
}
/**
* Returns unit movement cost on a given terrain.
* - Arg 1: unit userdata.
* - Arg 2: string containing the terrain type.
* - Ret 1: integer.
*/
static int intf_unit_movement_cost(lua_State *L)
{
const unit& u = luaW_checkunit(L, 1);
t_translation::terrain_code t;
map_location loc;
if(luaW_tolocation(L, 2, loc)) {
t = static_cast<const game_board*>(resources::gameboard)->map()[loc];
} else if(lua_isstring(L, 2)) {
char const *m = luaL_checkstring(L, 2);
t = t_translation::read_terrain_code(m);
} else return luaW_type_error(L, 2, "location or terrain string");
lua_pushinteger(L, u.movement_cost(t));
return 1;
}
/**
* Returns unit vision cost on a given terrain.
* - Arg 1: unit userdata.
* - Arg 2: string containing the terrain type.
* - Ret 1: integer.
*/
static int intf_unit_vision_cost(lua_State *L)
{
const unit& u = luaW_checkunit(L, 1);
t_translation::terrain_code t;
map_location loc;
if(luaW_tolocation(L, 2, loc)) {
t = static_cast<const game_board*>(resources::gameboard)->map()[loc];
} else if(lua_isstring(L, 2)) {
char const *m = luaL_checkstring(L, 2);
t = t_translation::read_terrain_code(m);
} else return luaW_type_error(L, 2, "location or terrain string");
lua_pushinteger(L, u.vision_cost(t));
return 1;
}
/**
* Returns unit jamming cost on a given terrain.
* - Arg 1: unit userdata.
* - Arg 2: string containing the terrain type.
* - Ret 1: integer.
*/
static int intf_unit_jamming_cost(lua_State *L)
{
const unit& u = luaW_checkunit(L, 1);
t_translation::terrain_code t;
map_location loc;
if(luaW_tolocation(L, 2, loc)) {
t = static_cast<const game_board*>(resources::gameboard)->map()[loc];
} else if(lua_isstring(L, 2)) {
char const *m = luaL_checkstring(L, 2);
t = t_translation::read_terrain_code(m);
} else return luaW_type_error(L, 2, "location or terrain string");
lua_pushinteger(L, u.jamming_cost(t));
return 1;
}
/**
* Returns unit defense on a given terrain.
* - Arg 1: unit userdata.
* - Arg 2: string containing the terrain type.
* - Ret 1: integer.
*/
static int intf_unit_defense(lua_State *L)
{
const unit& u = luaW_checkunit(L, 1);
t_translation::terrain_code t;
map_location loc;
if(luaW_tolocation(L, 2, loc)) {
t = static_cast<const game_board*>(resources::gameboard)->map()[loc];
} else if(lua_isstring(L, 2)) {
char const *m = luaL_checkstring(L, 2);
t = t_translation::read_terrain_code(m);
} else return luaW_type_error(L, 2, "location or terrain string");
lua_pushinteger(L, 100 - u.defense_modifier(t));
return 1;
}
/**
* Returns true if the unit has the given ability enabled.
* - Arg 1: unit userdata.
* - Arg 2: string.
* - Ret 1: boolean.
*/
int game_lua_kernel::intf_unit_ability(lua_State *L)
{
const unit& u = luaW_checkunit(L, 1);
char const *m = luaL_checkstring(L, 2);
lua_pushboolean(L, u.get_ability_bool(m));
return 1;
}
/**
* Changes a unit to the given unit type.
* - Arg 1: unit userdata.
* - Arg 2: unit type name
* - Arg 3: (optional) unit variation name
*/
static int intf_transform_unit(lua_State *L)
{
unit& u = luaW_checkunit(L, 1);
char const *m = luaL_checkstring(L, 2);
const unit_type *utp = unit_types.find(m);
if (!utp) return luaL_argerror(L, 2, "unknown unit type");
if(lua_isstring(L, 3)) {
const std::string& m2 = lua_tostring(L, 3);
if(!utp->has_variation(m2)) return luaL_argerror(L, 2, "unknown unit variation");
utp = &utp->get_variation(m2);
}
u.advance_to(*utp);
return 0;
}
/**
* Puts a table at the top of the stack with some combat result.
*/
static void luaW_pushsimdata(lua_State *L, const combatant &cmb)
{
int n = cmb.hp_dist.size();
lua_createtable(L, 0, 4);
lua_pushnumber(L, cmb.poisoned);
lua_setfield(L, -2, "poisoned");
lua_pushnumber(L, cmb.slowed);
lua_setfield(L, -2, "slowed");
lua_pushnumber(L, cmb.untouched);
lua_setfield(L, -2, "untouched");
lua_pushnumber(L, cmb.average_hp());
lua_setfield(L, -2, "average_hp");
lua_createtable(L, n, 0);
for (int i = 0; i < n; ++i) {
lua_pushnumber(L, cmb.hp_dist[i]);
lua_rawseti(L, -2, i);
}
lua_setfield(L, -2, "hp_chance");
}
/**
* Puts a table at the top of the stack with information about the combatants' weapons.
*/
static void luaW_pushsimweapon(lua_State *L, const battle_context_unit_stats &bcustats)
{
lua_createtable(L, 0, 16);
lua_pushnumber(L, bcustats.num_blows);
lua_setfield(L, -2, "num_blows");
lua_pushnumber(L, bcustats.damage);
lua_setfield(L, -2, "damage");
lua_pushnumber(L, bcustats.chance_to_hit);
lua_setfield(L, -2, "chance_to_hit");
lua_pushboolean(L, bcustats.poisons);
lua_setfield(L, -2, "poisons");
lua_pushboolean(L, bcustats.slows);
lua_setfield(L, -2, "slows");
lua_pushboolean(L, bcustats.petrifies);
lua_setfield(L, -2, "petrifies");
lua_pushboolean(L, bcustats.plagues);
lua_setfield(L, -2, "plagues");
lua_pushstring(L, bcustats.plague_type.c_str());
lua_setfield(L, -2, "plague_type");
lua_pushnumber(L, bcustats.rounds);
lua_setfield(L, -2, "rounds");
lua_pushboolean(L, bcustats.firststrike);
lua_setfield(L, -2, "firststrike");
lua_pushboolean(L, bcustats.drains);
lua_setfield(L, -2, "drains");
lua_pushnumber(L, bcustats.drain_constant);
lua_setfield(L, -2, "drain_constant");
lua_pushnumber(L, bcustats.drain_percent);
lua_setfield(L, -2, "drain_percent");
//if we called simulate_combat without giving an explicit weapon this can be useful.
lua_pushnumber(L, bcustats.attack_num);
lua_setfield(L, -2, "attack_num"); // DEPRECATED
lua_pushnumber(L, bcustats.attack_num + 1);
lua_setfield(L, -2, "number");
//this is nullptr when there is no counter weapon
if(bcustats.weapon != nullptr)
{
lua_pushstring(L, bcustats.weapon->id().c_str());
lua_setfield(L, -2, "name");
luaW_pushweapon(L, bcustats.weapon);
lua_setfield(L, -2, "weapon");
}
}
/**
* Simulates a combat between two units.
* - Arg 1: attacker userdata.
* - Arg 2: optional weapon index.
* - Arg 3: defender userdata.
* - Arg 4: optional weapon index.
*
* - Ret 1: attacker results.
* - Ret 2: defender results.
* - Ret 3: info about the attacker weapon.
* - Ret 4: info about the defender weapon.
*/
int game_lua_kernel::intf_simulate_combat(lua_State *L)
{
int arg_num = 1, att_w = -1, def_w = -1;
unit_const_ptr att = luaW_checkunit(L, arg_num).shared_from_this();
++arg_num;
if (lua_isnumber(L, arg_num)) {
att_w = lua_tointeger(L, arg_num) - 1;
if (att_w < 0 || att_w >= static_cast<int>(att->attacks().size()))
return luaL_argerror(L, arg_num, "weapon index out of bounds");
++arg_num;
}
unit_const_ptr def = luaW_checkunit(L, arg_num).shared_from_this();
++arg_num;
if (lua_isnumber(L, arg_num)) {
def_w = lua_tointeger(L, arg_num) - 1;
if (def_w < 0 || def_w >= static_cast<int>(def->attacks().size()))
return luaL_argerror(L, arg_num, "weapon index out of bounds");
++arg_num;
}
battle_context context(units(), att->get_location(),
def->get_location(), att_w, def_w, 0.0, nullptr, att, def);
luaW_pushsimdata(L, context.get_attacker_combatant());
luaW_pushsimdata(L, context.get_defender_combatant());
luaW_pushsimweapon(L, context.get_attacker_stats());
luaW_pushsimweapon(L, context.get_defender_stats());
return 4;
}
/**
* Plays a sound, possibly repeated.
* - Arg 1: string.
* - Arg 2: optional integer.
*/
int game_lua_kernel::intf_play_sound(lua_State *L)
{
if (play_controller_.is_skipping_replay()) return 0;
char const *m = luaL_checkstring(L, 1);
int repeats = luaL_optinteger(L, 2, 0);
sound::play_sound(m, sound::SOUND_FX, repeats);
return 0;
}
/**
* Sets an achievement as being completed.
* - Arg 1: string - content_for.
* - Arg 2: string - id.
*/
int game_lua_kernel::intf_set_achievement(lua_State *L)
{
const char* content_for = luaL_checkstring(L, 1);
const char* id = luaL_checkstring(L, 2);
for(achievement_group& group : game_config_manager::get()->get_achievements()) {
if(group.content_for_ == content_for) {
for(achievement& achieve : group.achievements_) {
if(achieve.id_ == id) {
// already achieved
if(achieve.achieved_) {
return 0;
}
// found the achievement - mark it as completed
if(!play_controller_.is_replay()) {
prefs::get().set_achievement(content_for, id);
}
achieve.achieved_ = true;
// progressable achievements can also check for current progress equals -1
if(achieve.max_progress_ != 0) {
achieve.current_progress_ = -1;
}
if(achieve.sound_path_ != "") {
sound::play_sound(achieve.sound_path_, sound::SOUND_FX);
}
// show the achievement popup
luaW_getglobal(L, "gui", "show_popup");
luaW_pushtstring(L, achieve.name_completed_);
luaW_pushtstring(L, achieve.description_completed_);
lua_pushstring(L, achieve.icon_completed_.c_str());
luaW_pcall(L, 3, 0, 0);
return 0;
}
}
// achievement not found - existing achievement group but non-existing achievement id
ERR_LUA << "Achievement " << id << " not found for achievement group " << content_for;
return 0;
}
}
// achievement group not found
ERR_LUA << "Achievement group " << content_for << " not found";
return 0;
}
/**
* Returns whether an achievement has been completed.
* - Arg 1: string - content_for.
* - Arg 2: string - id.
* - Ret 1: boolean.
*/
int game_lua_kernel::intf_has_achievement(lua_State *L)
{
const char* content_for = luaL_checkstring(L, 1);
const char* id = luaL_checkstring(L, 2);
if(resources::controller->is_networked_mp() && synced_context::is_synced()) {
ERR_LUA << "Returning false for whether a player has completed an achievement due to being networked multiplayer.";
lua_pushboolean(L, false);
} else {
lua_pushboolean(L, prefs::get().achievement(content_for, id));
}
return 1;
}
/**
* Returns information on a single achievement, or no data if the achievement is not found.
* - Arg 1: string - content_for.
* - Arg 2: string - id.
* - Ret 1: WML table returned by the function.
*/
int game_lua_kernel::intf_get_achievement(lua_State *L)
{
const char* content_for = luaL_checkstring(L, 1);
const char* id = luaL_checkstring(L, 2);
config cfg;
for(const auto& group : game_config_manager::get()->get_achievements()) {
if(group.content_for_ == content_for) {
for(const auto& achieve : group.achievements_) {
if(achieve.id_ == id) {
// found the achievement - return it as a config
cfg["id"] = achieve.id_;
cfg["name"] = achieve.name_;
cfg["name_completed"] = achieve.name_completed_;
cfg["description"] = achieve.description_;
cfg["description_completed"] = achieve.description_completed_;
cfg["icon"] = achieve.icon_;
cfg["icon_completed"] = achieve.icon_completed_;
cfg["hidden"] = achieve.hidden_;
cfg["achieved"] = achieve.achieved_;
cfg["max_progress"] = achieve.max_progress_;
cfg["current_progress"] = achieve.current_progress_;
for(const auto& sub_ach : achieve.sub_achievements_) {
config& sub = cfg.add_child("sub_achievement");
sub["id"] = sub_ach.id_;
sub["description"] = sub_ach.description_;
sub["icon"] = sub_ach.icon_;
sub["achieved"] = sub_ach.achieved_;
}
luaW_pushconfig(L, cfg);
return 1;
}
}
// return empty config - existing achievement group but non-existing achievement id
ERR_LUA << "Achievement " << id << " not found for achievement group " << content_for;
luaW_pushconfig(L, cfg);
return 1;
}
}
// return empty config - non-existing achievement group
ERR_LUA << "Achievement group " << content_for << " not found";
luaW_pushconfig(L, cfg);
return 1;
}
/**
* Progresses the provided achievement.
* - Arg 1: string - content_for.
* - Arg 2: string - achievement id.
* - Arg 3: int - the amount to progress the achievement.
* - Arg 4: int - the limit the achievement can progress by
* - Ret 1: int - the achievement's current progress after adding amount or -1 if not a progressable achievement (including if it's already achieved)
* - Ret 2: int - the achievement's max progress or -1 if not a progressable achievement
*/
int game_lua_kernel::intf_progress_achievement(lua_State *L)
{
const char* content_for = luaL_checkstring(L, 1);
const char* id = luaL_checkstring(L, 2);
int amount = luaL_checkinteger(L, 3);
int limit = luaL_optinteger(L, 4, 999999999);
for(achievement_group& group : game_config_manager::get()->get_achievements()) {
if(group.content_for_ == content_for) {
for(achievement& achieve : group.achievements_) {
if(achieve.id_ == id) {
// check that this is a progressable achievement
if(achieve.max_progress_ == 0 || achieve.sub_achievements_.size() > 0) {
ERR_LUA << "Attempted to progress achievement " << id << " for achievement group " << content_for << ", is not a progressible achievement.";
lua_pushinteger(L, -1);
lua_pushinteger(L, -1);
return 2;
}
if(!achieve.achieved_) {
int progress = 0;
if(!play_controller_.is_replay()) {
progress = prefs::get().progress_achievement(content_for, id, limit, achieve.max_progress_, amount);
}
if(progress >= achieve.max_progress_) {
intf_set_achievement(L);
achieve.current_progress_ = -1;
} else {
achieve.current_progress_ = progress;
}
lua_pushinteger(L, progress);
} else {
lua_pushinteger(L, -1);
}
lua_pushinteger(L, achieve.max_progress_);
return 2;
}
}
// achievement not found - existing achievement group but non-existing achievement id
lua_push(L, "Achievement " + std::string(id) + " not found for achievement group " + content_for);
return lua_error(L);
}
}
// achievement group not found
lua_push(L, "Achievement group " + std::string(content_for) + " not found");
return lua_error(L);
}
/**
* Returns whether an achievement has been completed.
* - Arg 1: string - content_for.
* - Arg 2: string - achievement id.
* - Arg 3: string - sub-achievement id
* - Ret 1: boolean.
*/
int game_lua_kernel::intf_has_sub_achievement(lua_State *L)
{
const char* content_for = luaL_checkstring(L, 1);
const char* id = luaL_checkstring(L, 2);
const char* sub_id = luaL_checkstring(L, 3);
if(resources::controller->is_networked_mp() && synced_context::is_synced()) {
ERR_LUA << "Returning false for whether a player has completed an achievement due to being networked multiplayer.";
lua_pushboolean(L, false);
} else {
lua_pushboolean(L, prefs::get().sub_achievement(content_for, id, sub_id));
}
return 1;
}
/**
* Marks a single sub-achievement as completed.
* - Arg 1: string - content_for.
* - Arg 2: string - achievement id.
* - Arg 3: string - sub-achievement id
*/
int game_lua_kernel::intf_set_sub_achievement(lua_State *L)
{
const char* content_for = luaL_checkstring(L, 1);
const char* id = luaL_checkstring(L, 2);
const char* sub_id = luaL_checkstring(L, 3);
for(achievement_group& group : game_config_manager::get()->get_achievements()) {
if(group.content_for_ == content_for) {
for(achievement& achieve : group.achievements_) {
if(achieve.id_ == id) {
// the whole achievement is already completed
if(achieve.achieved_) {
return 0;
}
for(sub_achievement& sub_ach : achieve.sub_achievements_) {
if(sub_ach.id_ == sub_id) {
// this particular sub-achievement is already achieved
if(sub_ach.achieved_) {
return 0;
} else {
if(!play_controller_.is_replay()) {
prefs::get().set_sub_achievement(content_for, id, sub_id);
}
sub_ach.achieved_ = true;
achieve.current_progress_++;
if(achieve.current_progress_ == achieve.max_progress_) {
intf_set_achievement(L);
}
return 0;
}
}
}
// sub-achievement not found - existing achievement group and achievement but non-existing sub-achievement id
lua_push(L, "Sub-achievement " + std::string(id) + " not found for achievement" + id + " in achievement group " + content_for);
return lua_error(L);
}
}
// achievement not found - existing achievement group but non-existing achievement id
lua_push(L, "Achievement " + std::string(id) + " not found for achievement group " + content_for);
return lua_error(L);
}
}
// achievement group not found
lua_push(L, "Achievement group " + std::string(content_for) + " not found");
return lua_error(L);
}
/**
* Scrolls to given tile.
* - Arg 1: location.
* - Arg 2: boolean preventing scroll to fog.
* - Arg 3: boolean specifying whether to warp instantly.
* - Arg 4: boolean specifying whether to skip if already onscreen
*/
int game_lua_kernel::intf_scroll_to_tile(lua_State *L)
{
map_location loc = luaW_checklocation(L, 1);
bool check_fogged = luaW_toboolean(L, 2);
game_display::SCROLL_TYPE scroll = luaW_toboolean(L, 4)
? luaW_toboolean(L, 3)
? game_display::ONSCREEN_WARP
: game_display::ONSCREEN
: luaW_toboolean(L, 3)
? game_display::WARP
: game_display::SCROLL
;
if (game_display_) {
game_display_->scroll_to_tile(loc, scroll, check_fogged);
}
return 0;
}
/**
* Selects and highlights the given location on the map.
* - Arg 1: location.
* - Args 2,3: booleans
*/
int game_lua_kernel::intf_select_unit(lua_State *L)
{
events::command_disabler command_disabler;
if(lua_isnoneornil(L, 1)) {
play_controller_.get_mouse_handler_base().select_hex(map_location::null_location(), false, false, false, true);
return 0;
}
const map_location loc = luaW_checklocation(L, 1);
if(!map().on_board(loc)) return luaL_argerror(L, 1, "not on board");
bool highlight = true;
if(!lua_isnoneornil(L, 2))
highlight = luaW_toboolean(L, 2);
const bool fire_event = luaW_toboolean(L, 3);
play_controller_.get_mouse_handler_base().select_hex(
loc, false, highlight, fire_event);
return 0;
}
/**
* Deselects any highlighted hex on the map.
* No arguments or return values
*/
int game_lua_kernel::intf_deselect_hex(lua_State*)
{
if(game_display_) {
game_display_->highlight_hex(map_location::null_location());
}
return 0;
}
/**
* Return true if a replay is in progress but the player has chosen to skip it
*/
int game_lua_kernel::intf_is_skipping_messages(lua_State *L)
{
bool skipping = play_controller_.is_skipping_replay() || play_controller_.is_skipping_story();
if (!skipping) {
skipping = game_state_.events_manager_->pump().context_skip_messages();
}
lua_pushboolean(L, skipping);
return 1;
}
/**
* Set whether to skip messages
* Arg 1 (optional) - boolean
*/
int game_lua_kernel::intf_skip_messages(lua_State *L)
{
bool skip = true;
if (!lua_isnone(L, 1)) {
skip = luaW_toboolean(L, 1);
}
game_state_.events_manager_->pump().context_skip_messages(skip);
return 0;
}
namespace
{
struct lua_synchronize : mp_sync::user_choice
{
lua_State *L;
int user_choice_index;
int random_choice_index;
int ai_choice_index;
std::string desc;
lua_synchronize(lua_State *l, const std::string& descr, int user_index, int random_index = 0, int ai_index = 0)
: L(l)
, user_choice_index(user_index)
, random_choice_index(random_index)
, ai_choice_index(ai_index != 0 ? ai_index : user_index)
, desc(descr)
{}
virtual config query_user(int side) const override
{
bool is_local_ai = lua_kernel_base::get_lua_kernel<game_lua_kernel>(L).board().get_team(side).is_local_ai();
config cfg;
query_lua(side, is_local_ai ? ai_choice_index : user_choice_index, cfg);
return cfg;
}
virtual config random_choice(int side) const override
{
config cfg;
if(random_choice_index != 0 && lua_isfunction(L, random_choice_index)) {
query_lua(side, random_choice_index, cfg);
}
return cfg;
}
virtual std::string description() const override
{
return desc;
}
void query_lua(int side, int function_index, config& cfg) const
{
lua_pushvalue(L, function_index);
lua_pushnumber(L, side);
if (luaW_pcall(L, 1, 1, false)) {
if(!luaW_toconfig(L, -1, cfg)) {
static const char* msg = "function returned to wesnoth.sync.[multi_]evaluate a table which was partially invalid";
lua_kernel_base::get_lua_kernel<game_lua_kernel>(L).log_error(msg);
lua_warning(L, msg, false);
}
}
}
//Although lua's sync_choice can show a dialog, (and will in most cases)
//we return false to enable other possible things that do not contain UI things.
//it's in the responsibility of the umc dev to not show dialogs during prestart events.
virtual bool is_visible() const override { return false; }
};
}//unnamed namespace for lua_synchronize
/**
* Ensures a value is synchronized among all the clients.
* - Arg 1: optional string specifying the type id of the choice.
* - Arg 2: function to compute the value, called if the client is the master.
* - Arg 3: optional function, called instead of the first function if the user is not human.
* - Arg 4: optional integer specifying, on which side the function should be evaluated.
* - Ret 1: WML table returned by the function.
*/
static int intf_synchronize_choice(lua_State *L)
{
std::string tagname = "input";
t_string desc = _("input");
int human_func = 0;
int ai_func = 0;
int side_for;
int nextarg = 1;
if(!lua_isfunction(L, nextarg) && luaW_totstring(L, nextarg, desc) ) {
++nextarg;
}
if(lua_isfunction(L, nextarg)) {
human_func = nextarg++;
}
else {
return luaL_argerror(L, nextarg, "expected a function");
}
if(lua_isfunction(L, nextarg)) {
ai_func = nextarg++;
}
side_for = lua_tointeger(L, nextarg);
config cfg = mp_sync::get_user_choice(tagname, lua_synchronize(L, desc, human_func, 0, ai_func), side_for);
luaW_pushconfig(L, cfg);
return 1;
}
/**
* Ensures a value is synchronized among all the clients.
* - Arg 1: optional string the id of this type of user input, may only contain characters a-z and '_'
* - Arg 2: function to compute the value, called if the client is the master.
* - Arg 3: an optional function to compute the value, if the side was null/empty controlled.
* - Arg 4: an array of integers specifying, on which side the function should be evaluated.
* - Ret 1: a map int -> WML tabls.
*/
static int intf_synchronize_choices(lua_State *L)
{
std::string tagname = "input";
t_string desc = _("input");
int human_func = 0;
int null_func = 0;
std::vector<int> sides_for;
int nextarg = 1;
if(!lua_isfunction(L, nextarg) && luaW_totstring(L, nextarg, desc) ) {
++nextarg;
}
if(lua_isfunction(L, nextarg)) {
human_func = nextarg++;
}
else {
return luaL_argerror(L, nextarg, "expected a function");
}
if(lua_isfunction(L, nextarg)) {
null_func = nextarg++;
};
sides_for = lua_check<std::vector<int>>(L, nextarg++);
lua_push(L, mp_sync::get_user_choice_multiple_sides(tagname, lua_synchronize(L, desc, human_func, null_func), std::set<int>(sides_for.begin(), sides_for.end())));
return 1;
}
/**
* Calls a function in an unsynced context (this specially means that all random calls used by that function will be unsynced).
* This is usually used together with an unsynced if like 'if controller != network'
* - Arg 1: function that will be called during the unsynced context.
*/
static int intf_do_unsynced(lua_State *L)
{
set_scontext_unsynced sync;
lua_pushvalue(L, 1);
luaW_pcall(L, 0, 0, false);
return 0;
}
/**
* Gets all the locations matching a given filter.
* - Arg 1: WML table.
* - Arg 2: Optional reference unit (teleport_unit)
* - Ret 1: array of integer pairs.
*/
int game_lua_kernel::intf_get_locations(lua_State *L)
{
vconfig filter = luaW_checkvconfig(L, 1);
std::set<map_location> res;
filter_context & fc = game_state_;
const terrain_filter t_filter(filter, &fc, false);
if(luaW_isunit(L, 2)) {
t_filter.get_locations(res, *luaW_tounit(L, 2), true);
} else {
t_filter.get_locations(res, true);
}
luaW_push_locationset(L, res);
return 1;
}
/**
* Matches a location against the given filter.
* - Arg 1: location.
* - Arg 2: WML table.
* - Arg 3: Optional reference unit (teleport_unit)
* - Ret 1: boolean.
*/
int game_lua_kernel::intf_match_location(lua_State *L)
{
map_location loc = luaW_checklocation(L, 1);
vconfig filter = luaW_checkvconfig(L, 2, true);
if (filter.null()) {
lua_pushboolean(L, true);
return 1;
}
filter_context & fc = game_state_;
const terrain_filter t_filter(filter, &fc, false);
if(luaW_isunit(L, 3)) {
lua_pushboolean(L, t_filter.match(loc, *luaW_tounit(L, 3)));
} else {
lua_pushboolean(L, t_filter.match(loc));
}
return 1;
}
/**
* Matches a side against the given filter.
* - Args 1: side number.
* - Arg 2: WML table.
* - Ret 1: boolean.
*/
int game_lua_kernel::intf_match_side(lua_State *L)
{
vconfig filter = luaW_checkvconfig(L, 2, true);
if (filter.null()) {
lua_pushboolean(L, true);
return 1;
}
filter_context & fc = game_state_;
side_filter s_filter(filter, &fc);
if(team* t = luaW_toteam(L, 1)) {
lua_pushboolean(L, s_filter.match(*t));
} else {
unsigned side = luaL_checkinteger(L, 1) - 1;
if (side >= teams().size()) return 0;
lua_pushboolean(L, s_filter.match(side + 1));
}
return 1;
}
int game_lua_kernel::intf_set_side_id(lua_State *L)
{
int team_i;
if(team* t = luaW_toteam(L, 1)) {
team_i = t->side();
} else {
team_i = luaL_checkinteger(L, 1);
}
std::string flag = luaL_optlstring(L, 2, "", nullptr);
std::string color = luaL_optlstring(L, 3, "", nullptr);
if(flag.empty() && color.empty()) {
return 0;
}
if(team_i < 1 || static_cast<std::size_t>(team_i) > teams().size()) {
return luaL_error(L, "set_side_id: side number %d out of range", team_i);
}
team& side = board().get_team(team_i);
if(!color.empty()) {
side.set_color(color);
}
if(!flag.empty()) {
side.set_flag(flag);
}
game_display_->reinit_flags_for_team(side);
return 0;
}
static int intf_modify_ai(lua_State *L, const char* action)
{
int side_num;
if(team* t = luaW_toteam(L, 1)) {
side_num = t->side();
} else {
side_num = luaL_checkinteger(L, 1);
}
std::string path = luaL_checkstring(L, 2);
config cfg {
"action", action,
"path", path
};
if(strcmp(action, "delete") == 0) {
ai::manager::get_singleton().modify_active_ai_for_side(side_num, cfg);
return 0;
}
config component = luaW_checkconfig(L, 3);
std::size_t len = std::string::npos, open_brak = path.find_last_of('[');
std::size_t dot = path.find_last_of('.');
if(open_brak != len) {
len = open_brak - dot - 1;
}
cfg.add_child(path.substr(dot + 1, len), component);
ai::manager::get_singleton().modify_active_ai_for_side(side_num, cfg);
return 0;
}
static int intf_switch_ai(lua_State *L)
{
int side_num;
if(team* t = luaW_toteam(L, 1)) {
side_num = t->side();
} else {
side_num = luaL_checkinteger(L, 1);
}
if(lua_isstring(L, 2)) {
std::string file = luaL_checkstring(L, 2);
if(!ai::manager::get_singleton().add_ai_for_side_from_file(side_num, file)) {
std::string err = formatter() << "Could not load AI for side " << side_num << " from file " << file;
lua_pushlstring(L, err.c_str(), err.length());
return lua_error(L);
}
} else {
ai::manager::get_singleton().add_ai_for_side_from_config(side_num, luaW_checkconfig(L, 2));
}
return 0;
}
static int intf_append_ai(lua_State *L)
{
int side_num;
if(team* t = luaW_toteam(L, 1)) {
side_num = t->side();
} else {
side_num = luaL_checkinteger(L, 1);
}
config cfg = luaW_checkconfig(L, 2);
if(!cfg.has_child("ai")) {
cfg = config {"ai", cfg};
}
bool added_dummy_stage = false;
if(!cfg.mandatory_child("ai").has_child("stage")) {
added_dummy_stage = true;
cfg.mandatory_child("ai").add_child("stage", config {"name", "empty"});
}
ai::configuration::expand_simplified_aspects(side_num, cfg);
if(added_dummy_stage) {
cfg.remove_children("stage", [](const config& stage_cfg) { return stage_cfg["name"] == "empty"; });
}
ai::manager::get_singleton().append_active_ai_for_side(side_num, cfg.mandatory_child("ai"));
return 0;
}
int game_lua_kernel::intf_get_side(lua_State* L)
{
unsigned i = luaL_checkinteger(L, 1);
if(i < 1 || i > teams().size()) return 0;
luaW_pushteam(L, board().get_team(i));
return 1;
}
/**
* Returns a proxy table array for all sides matching the given SSF.
* - Arg 1: SSF
* - Ret 1: proxy table array
*/
int game_lua_kernel::intf_get_sides(lua_State* L)
{
LOG_LUA << "intf_get_sides called: this = " << std::hex << this << std::dec << " myname = " << my_name();
std::vector<int> sides;
const vconfig ssf = luaW_checkvconfig(L, 1, true);
if(ssf.null()) {
for(const team& t : teams()) {
sides.push_back(t.side());
}
} else {
filter_context & fc = game_state_;
side_filter filter(ssf, &fc);
sides = filter.get_teams();
}
lua_settop(L, 0);
lua_createtable(L, sides.size(), 0);
unsigned index = 1;
for(int side : sides) {
luaW_pushteam(L, board().get_team(side));
lua_rawseti(L, -2, index);
++index;
}
return 1;
}
/**
* Adds a modification to a unit.
* - Arg 1: unit.
* - Arg 2: string.
* - Arg 3: WML table.
* - Arg 4: (optional) Whether to add to [modifications] - default true
*/
static int intf_add_modification(lua_State *L)
{
unit& u = luaW_checkunit(L, 1);
char const *m = luaL_checkstring(L, 2);
std::string sm = m;
if (sm == "advance") { // Maintain backwards compatibility
sm = "advancement";
deprecated_message("\"advance\" modification type", DEP_LEVEL::PREEMPTIVE, {1, 15, 0}, "Use \"advancement\" instead.");
}
if (sm != "advancement" && sm != "object" && sm != "trait") {
return luaL_argerror(L, 2, "unknown modification type");
}
bool write_to_mods = true;
if (!lua_isnone(L, 4)) {
write_to_mods = luaW_toboolean(L, 4);
}
if(sm.empty()) {
write_to_mods = false;
}
config cfg = luaW_checkconfig(L, 3);
u.add_modification(sm, cfg, !write_to_mods);
return 0;
}
/**
* Removes modifications from a unit
* - Arg 1: unit
* - Arg 2: table (filter as [filter_wml])
* - Arg 3: type of modification (default "object")
*/
static int intf_remove_modifications(lua_State *L)
{
unit& u = luaW_checkunit(L, 1);
config filter = luaW_checkconfig(L, 2);
std::vector<std::string> tags;
if(lua_isstring(L, 3)) {
tags.push_back(lua_check<std::string>(L, 3));
} else if (lua_istable(L, 3)){
tags = lua_check<std::vector<std::string>>(L, 3);
} else {
tags.push_back("object");
}
//TODO
if(filter.attribute_count() == 1 && filter.all_children_count() == 0 && filter.attribute_range().front().first == "duration") {
u.expire_modifications(filter["duration"]);
} else {
for(const std::string& tag : tags) {
for(config& obj : u.get_modifications().child_range(tag)) {
if(obj.matches(filter)) {
obj["duration"] = "now";
}
}
}
u.expire_modifications("now");
}
return 0;
}
/**
* Advances a unit if the unit has enough xp.
* - Arg 1: unit.
* - Arg 2: optional boolean whether to animate the advancement.
* - Arg 3: optional boolean whether to fire advancement events.
*/
static int intf_advance_unit(lua_State *L)
{
events::command_disabler command_disabler;
unit& u = luaW_checkunit(L, 1, true);
advance_unit_params par(u.get_location());
if(lua_isboolean(L, 2)) {
par.animate(luaW_toboolean(L, 2));
}
if(lua_isboolean(L, 3)) {
par.fire_events(luaW_toboolean(L, 3));
}
advance_unit_at(par);
return 0;
}
/**
* Adds a new known unit type to the help system.
* - Arg 1: string.
*/
static int intf_add_known_unit(lua_State *L)
{
char const *ty = luaL_checkstring(L, 1);
if(!unit_types.find(ty))
{
std::stringstream ss;
ss << "unknown unit type: '" << ty << "'";
return luaL_argerror(L, 1, ss.str().c_str());
}
prefs::get().encountered_units().insert(ty);
return 0;
}
/**
* Adds an overlay on a tile.
* - Arg 1: location.
* - Arg 2: WML table.
*/
int game_lua_kernel::intf_add_tile_overlay(lua_State *L)
{
map_location loc = luaW_checklocation(L, 1);
vconfig cfg = luaW_checkvconfig(L, 2);
const vconfig &ssf = cfg.child("filter_team");
std::string team_name;
if (!ssf.null()) {
const std::vector<int>& teams = side_filter(ssf, &game_state_).get_teams();
std::vector<std::string> team_names;
std::transform(teams.begin(), teams.end(), std::back_inserter(team_names),
[&](int team) { return game_state_.get_disp_context().get_team(team).team_name(); });
team_name = utils::join(team_names);
} else {
team_name = cfg["team_name"].str();
}
if (game_display_) {
game_display_->add_overlay(loc, overlay(
cfg["image"],
cfg["halo"],
team_name,
cfg["name"], // Name is treated as the ID
cfg["visible_in_fog"].to_bool(true),
cfg["submerge"].to_double(0),
cfg["z_order"].to_double(0)
));
}
return 0;
}
/**
* Removes an overlay from a tile.
* - Arg 1: location.
* - Arg 2: optional string.
*/
int game_lua_kernel::intf_remove_tile_overlay(lua_State *L)
{
map_location loc = luaW_checklocation(L, 1);
char const *m = lua_tostring(L, 2);
if (m) {
if (game_display_) {
game_display_->remove_single_overlay(loc, m);
}
} else {
if (game_display_) {
game_display_->remove_overlay(loc);
}
}
return 0;
}
int game_lua_kernel::intf_log_replay(lua_State* L)
{
replay& recorder = play_controller_.get_replay();
const int nargs = lua_gettop(L);
if(nargs < 2 || nargs > 3) {
return luaL_error(L, "Wrong number of arguments to ai.log_replay() - should be 2 or 3 arguments.");
}
const std::string key = nargs == 2 ? luaL_checkstring(L, 1) : luaL_checkstring(L, 2);
config cfg;
if(nargs == 2) {
recorder.add_log_data(key, luaL_checkstring(L, 2));
} else if(luaW_toconfig(L, 3, cfg)) {
recorder.add_log_data(luaL_checkstring(L, 1), key, cfg);
} else if(!lua_isstring(L, 3)) {
return luaL_argerror(L, 3, "accepts only string or config");
} else {
recorder.add_log_data(luaL_checkstring(L, 1), key, luaL_checkstring(L, 3));
}
return 0;
}
struct lua_event_filter : public game_events::event_filter
{
lua_event_filter(game_lua_kernel& lk, int idx, const config& args) : lk(lk), args_(args)
{
ref_ = lk.save_wml_event(idx);
}
bool operator()(const game_events::queued_event& event_info) const override
{
bool result;
return lk.run_wml_event(ref_, args_, event_info, &result) && result;
}
~lua_event_filter()
{
lk.clear_wml_event(ref_);
}
void serialize(config& cfg) const override {
cfg.add_child("filter_lua")["code"] = "<function>";
}
private:
game_lua_kernel& lk;
int ref_;
vconfig args_;
};
static std::string read_event_name(lua_State* L, int idx)
{
if(lua_isstring(L, idx)) {
return lua_tostring(L, idx);
} else {
return utils::join(lua_check<std::vector<std::string>>(L, idx));
}
}
/**
* Add undo actions for the current active event
* Arg 1: Either a table of ActionWML or a function to call
* Arg 2: (optional) If Arg 1 is a function, this is a WML table that will be passed to it
*/
int game_lua_kernel::intf_add_undo_actions(lua_State *L)
{
config cfg;
if(luaW_toconfig(L, 1, cfg)) {
game_state_.undo_stack_->add_custom<actions::undo_event>(cfg, get_event_info());
} else {
luaW_toconfig(L, 2, cfg);
game_state_.undo_stack_->add_custom<actions::undo_event>(save_wml_event(1), cfg, get_event_info());
}
return 0;
}
/** Add a new event handler
* Arg 1: Table of options.
* name: Event to handle, as a string or list of strings
* id: Event ID
* menu_item: True if this is a menu item (an ID is required); this means removing the menu item will automatically remove this event. Default false.
* first_time_only: Whether this event should fire again after the first time; default true.
* priority: Number that determines execution order. Events execute in order of decreasing priority, and secondarily in order of addition.
* filter: Event filters as a config with filter tags, a table of the form {filter_type = filter_contents}, or a function
* filter_args: Arbitrary data that will be passed to the filter, if it is a function. Ignored if the filter is specified as WML or a table.
* content: The content of the event. This is a WML table passed verbatim into the event when it fires. If no function is specified, it will be interpreted as ActionWML.
* action: The function to call when the event triggers. Defaults to wesnoth.wml_actions.command.
*
* Lua API: wesnoth.game_events.add
*/
int game_lua_kernel::intf_add_event(lua_State *L)
{
game_events::manager & man = *game_state_.events_manager_;
using namespace std::literals;
std::string name, id = luaW_table_get_def(L, 1, "id", ""s);
bool repeat = !luaW_table_get_def(L, 1, "first_time_only", true), is_menu_item = luaW_table_get_def(L, 1, "menu_item", false);
double priority = luaW_table_get_def(L, 1, "priority", 0.);
if(luaW_tableget(L, 1, "name")) {
name = read_event_name(L, -1);
} else if(is_menu_item) {
if(id.empty()) {
return luaL_argerror(L, 1, "non-empty id is required for a menu item");
}
name = "menu item " + id;
}
if(id.empty() && name.empty()) {
return luaL_argerror(L, 1, "either a name or id is required");
}
auto new_handler = man.add_event_handler_from_lua(name, id, repeat, priority, is_menu_item);
if(new_handler.valid()) {
bool has_lua_filter = false;
new_handler->set_arguments(luaW_table_get_def(L, 1, "content", config{"__empty_lua_event", true}));
if(luaW_tableget(L, 1, "filter")) {
int filterIdx = lua_gettop(L);
config filters;
if(!luaW_toconfig(L, filterIdx, filters)) {
if(lua_isfunction(L, filterIdx)) {
int fcnIdx = lua_absindex(L, -1);
new_handler->add_filter(std::make_unique<lua_event_filter>(*this, fcnIdx, luaW_table_get_def(L, 1, "filter_args", config())));
has_lua_filter = true;
} else {
#define READ_ONE_FILTER(key, tag) \
do { \
if(luaW_tableget(L, filterIdx, key)) { \
if(lua_isstring(L, -1)) { \
filters.add_child("insert_tag", config{ \
"name", tag, \
"variable", luaL_checkstring(L, -1) \
}); \
} else { \
filters.add_child(tag, luaW_checkconfig(L, -1)); \
} \
} \
} while(false);
READ_ONE_FILTER("condition", "filter_condition");
READ_ONE_FILTER("side", "filter_side");
READ_ONE_FILTER("unit", "filter");
READ_ONE_FILTER("attack", "filter_attack");
READ_ONE_FILTER("second_unit", "filter_second");
READ_ONE_FILTER("second_attack", "filter_second_attack");
#undef READ_ONE_FILTER
if(luaW_tableget(L, filterIdx, "formula")) {
filters["filter_formula"] = luaL_checkstring(L, -1);
}
}
}
new_handler->read_filters(filters);
}
if(luaW_tableget(L, 1, "action")) {
new_handler->set_event_ref(save_wml_event(-1), has_preloaded_);
} else {
if(has_lua_filter) {
// This just sets the appropriate flags so the engine knows it cannot be serialized.
// The register_wml_event call will override the actual event_ref so just pass LUA_NOREF here.
new_handler->set_event_ref(LUA_NOREF, has_preloaded_);
}
new_handler->register_wml_event(*this);
}
}
return 0;
}
/**
* Upvalue 1: The event function
* Upvalue 2: The undo function
* Arg 1: The event content
*/
int game_lua_kernel::cfun_undoable_event(lua_State* L)
{
lua_pushvalue(L, lua_upvalueindex(1));
lua_push(L, 1);
luaW_pcall(L, 1, 0);
game_state_.undo_stack_->add_custom<actions::undo_event>(lua_upvalueindex(2), config(), get_event_info());
return 0;
}
/** Add a new event handler
* Arg 1: Event to handle, as a string or list of strings; or menu item ID if this is a menu item
* Arg 2: The function to call when the event triggers
* Arg 3: (optional) Event priority
* Arg 4: (optional, non-menu-items only) The function to call when the event is undone
*
* Lua API:
* - wesnoth.game_events.add_repeating
* - wesnoth.game_events.add_menu
*/
template<bool is_menu_item>
int game_lua_kernel::intf_add_event_simple(lua_State *L)
{
game_events::manager & man = *game_state_.events_manager_;
bool repeat = true;
std::string name = read_event_name(L, 1), id;
double priority = luaL_optnumber(L, 3, 0.);
if(name.empty()) {
return luaL_argerror(L, 1, "must not be empty");
}
if(is_menu_item) {
id = name;
name = "menu item " + name;
} else if(lua_absindex(L, -1) > 2 && lua_isfunction(L, -1)) {
// If undo is provided as a separate function, link them together into a single function
// The function can be either the 3rd or 4th argument.
lua_pushcclosure(L, &dispatch<&game_lua_kernel::cfun_undoable_event>, 2);
}
auto new_handler = man.add_event_handler_from_lua(name, id, repeat, priority, is_menu_item);
if(new_handler.valid()) {
// An event with empty arguments is not added, so set some dummy arguments
new_handler->set_arguments(config{"__quick_lua_event", true});
new_handler->set_event_ref(save_wml_event(2), has_preloaded_);
}
return 0;
}
/** Add a new event handler
* Arg: A full event specification as a WML config
*
* WML API: [event]
*/
int game_lua_kernel::intf_add_event_wml(lua_State *L)
{
game_events::manager & man = *game_state_.events_manager_;
vconfig cfg(luaW_checkvconfig(L, 1));
bool delayed_variable_substitution = cfg["delayed_variable_substitution"].to_bool(true);
if(delayed_variable_substitution) {
man.add_event_handler_from_wml(cfg.get_config(), *this);
} else {
man.add_event_handler_from_wml(cfg.get_parsed_config(), *this);
}
return 0;
}
int game_lua_kernel::intf_remove_event(lua_State *L)
{
game_state_.events_manager_->remove_event_handler(luaL_checkstring(L, 1));
return 0;
}
int game_lua_kernel::intf_color_adjust(lua_State *L)
{
if (game_display_) {
game_display_->adjust_color_overlay(luaL_checkinteger(L, 1), luaL_checkinteger(L, 2), luaL_checkinteger(L, 3));
game_display_->invalidate_all();
}
return 0;
}
int game_lua_kernel::intf_get_color_adjust(lua_State *L)
{
if(game_display_) {
auto color = game_display_->get_color_overlay();
lua_pushinteger(L, color.r);
lua_pushinteger(L, color.g);
lua_pushinteger(L, color.b);
return 3;
}
return 0;
}
int game_lua_kernel::intf_screen_fade(lua_State *L)
{
if(game_display_) {
auto vec = lua_check<std::vector<uint8_t>>(L, 1);
if(vec.size() != 4) {
return luaW_type_error(L, 1, "array of 4 integers");
}
color_t fade{vec[0], vec[1], vec[2], vec[3]};
game_display_->fade_to(fade, std::chrono::milliseconds{luaL_checkinteger(L, 2)});
}
return 0;
}
/**
* Delays engine for a while.
* - Arg 1: integer.
* - Arg 2: boolean (optional).
*/
int game_lua_kernel::intf_delay(lua_State *L)
{
if(gamedata().phase() == game_data::PRELOAD || gamedata().phase() == game_data::PRESTART || gamedata().phase() == game_data::INITIAL) {
//don't call play_slice if the game ui is not active yet.
return 0;
}
events::command_disabler command_disabler;
using namespace std::chrono_literals;
std::chrono::milliseconds delay{luaL_checkinteger(L, 1)};
if(delay == 0ms) {
play_controller_.play_slice();
return 0;
}
if(luaW_toboolean(L, 2) && game_display_ && game_display_->turbo_speed() > 0) {
delay /= game_display_->turbo_speed();
}
const auto end_time = std::chrono::steady_clock::now() + delay;
do {
play_controller_.play_slice();
std::this_thread::sleep_for(10ms);
} while (std::chrono::steady_clock::now() < end_time);
return 0;
}
int game_lua_kernel::intf_add_label(lua_State *L)
{
// TODO: Support color = {r = 0, g = 0, b = 0}
if (game_display_) {
vconfig cfg(luaW_checkvconfig(L, 1));
game_display &screen = *game_display_;
terrain_label label(screen.labels(), cfg.get_config());
screen.labels().set_label(label.location(), label.text(), label.creator(), label.team_name(), label.color(),
label.visible_in_fog(), label.visible_in_shroud(), label.immutable(), label.category(), label.tooltip());
}
return 0;
}
int game_lua_kernel::intf_remove_label(lua_State *L)
{
if (game_display_) {
map_location loc = luaW_checklocation(L, 1);
std::string team_name;
// If there's only one parameter and it's a table, check if it contains team_name
if(lua_gettop(L) == 1 && lua_istable(L, 1)) {
using namespace std::literals;
team_name = luaW_table_get_def(L, 1, "team_name", ""sv);
} else {
team_name = luaL_optstring(L, 2, "");
}
game_display_->labels().set_label(loc, "", -1, team_name);
}
return 0;
}
int game_lua_kernel::intf_get_label(lua_State* L)
{
if(game_display_) {
game_display &screen = *game_display_;
auto loc = luaW_checklocation(L, 1);
const terrain_label* label = nullptr;
switch(lua_type(L, 2)) {
// Missing 2nd argument - get global label
case LUA_TNONE: case LUA_TNIL:
label = screen.labels().get_label(loc, "");
break;
// Side number - get label belonging to that side's team
case LUA_TNUMBER:
if(size_t n = luaL_checkinteger(L, 2); n > 0 && n <= teams().size()) {
label = screen.labels().get_label(loc, teams().at(n - 1).team_name());
}
break;
// String - get label belonging to the team with that name
case LUA_TSTRING:
label = screen.labels().get_label(loc, luaL_checkstring(L, 2));
break;
// Side userdata - get label belonging to that side's team
case LUA_TUSERDATA:
label = screen.labels().get_label(loc, luaW_checkteam(L, 2).team_name());
break;
}
if(label) {
config cfg;
label->write(cfg);
luaW_pushconfig(L, cfg);
return 1;
}
}
return 0;
}
int game_lua_kernel::intf_redraw(lua_State *L)
{
if (game_display_) {
game_display & screen = *game_display_;
vconfig cfg(luaW_checkvconfig(L, 1));
bool clear_shroud(luaW_toboolean(L, 2));
// We do this twice so any applicable redraws happen both before and after
// any events caused by redrawing shroud are fired
bool result = screen.maybe_rebuild();
if (!result) {
screen.invalidate_all();
}
if (clear_shroud) {
side_filter filter(cfg, &game_state_);
for (const int side : filter.get_teams()){
actions::clear_shroud(side);
}
screen.recalculate_minimap();
}
result = screen.maybe_rebuild();
if (!result) {
screen.invalidate_all();
}
}
return 0;
}
/**
* Lua frontend to the modify_ai functionality
* - Arg 1: config.
*/
static int intf_modify_ai_old(lua_State *L)
{
config cfg;
luaW_toconfig(L, 1, cfg);
int side = cfg["side"].to_int();
ai::manager::get_singleton().modify_active_ai_for_side(side, cfg);
return 0;
}
static int cfun_exec_candidate_action(lua_State *L)
{
bool exec = luaW_toboolean(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "ca_ptr");
ai::candidate_action *ca = static_cast<ai::candidate_action*>(lua_touserdata(L, -1));
lua_pop(L, 2);
if (exec) {
ca->execute();
return 0;
}
lua_pushnumber(L, ca->evaluate());
return 1;
}
static int cfun_exec_stage(lua_State *L)
{
lua_getfield(L, -1, "stg_ptr");
ai::stage *stg = static_cast<ai::stage*>(lua_touserdata(L, -1));
lua_pop(L, 2);
stg->play_stage();
return 0;
}
static void push_component(lua_State *L, ai::component* c, const std::string &ct = "")
{
lua_createtable(L, 0, 0); // Table for a component
lua_pushstring(L, "name");
lua_pushstring(L, c->get_name().c_str());
lua_rawset(L, -3);
lua_pushstring(L, "engine");
lua_pushstring(L, c->get_engine().c_str());
lua_rawset(L, -3);
lua_pushstring(L, "id");
lua_pushstring(L, c->get_id().c_str());
lua_rawset(L, -3);
if (ct == "candidate_action") {
lua_pushstring(L, "ca_ptr");
lua_pushlightuserdata(L, c);
lua_rawset(L, -3);
lua_pushstring(L, "exec");
lua_pushcclosure(L, &cfun_exec_candidate_action, 0);
lua_rawset(L, -3);
}
if (ct == "stage") {
lua_pushstring(L, "stg_ptr");
lua_pushlightuserdata(L, c);
lua_rawset(L, -3);
lua_pushstring(L, "exec");
lua_pushcclosure(L, &cfun_exec_stage, 0);
lua_rawset(L, -3);
}
std::vector<std::string> c_types = c->get_children_types();
for (std::vector<std::string>::const_iterator t = c_types.begin(); t != c_types.end(); ++t)
{
std::vector<ai::component*> children = c->get_children(*t);
std::string type = *t;
if (type == "aspect" || type == "goal" || type == "engine")
{
continue;
}
lua_pushstring(L, type.c_str());
lua_createtable(L, 0, 0); // this table will be on top of the stack during recursive calls
for (std::vector<ai::component*>::const_iterator i = children.begin(); i != children.end(); ++i)
{
lua_pushstring(L, (*i)->get_name().c_str());
push_component(L, *i, type);
lua_rawset(L, -3);
//if (type == "candidate_action")
//{
// ai::candidate_action *ca = dynamic_cast<ai::candidate_action*>(*i);
// ca->execute();
//}
}
lua_rawset(L, -3); // setting the child table
}
}
/**
* Debug access to the ai tables
* - Arg 1: int
* - Ret 1: ai table
*/
static int intf_debug_ai(lua_State *L)
{
if (!game_config::debug) { // This function works in debug mode only
return 0;
}
int side;
if(team* t = luaW_toteam(L, 1)) {
side = t->side();
} else {
side = luaL_checkinteger(L, 1);
}
lua_pop(L, 1);
ai::component* c = ai::manager::get_singleton().get_active_ai_holder_for_side_dbg(side).get_component(nullptr, "");
// Bad, but works
std::vector<ai::component*> engines = c->get_children("engine");
ai::engine_lua* lua_engine = nullptr;
for (std::vector<ai::component*>::const_iterator i = engines.begin(); i != engines.end(); ++i)
{
if ((*i)->get_name() == "lua")
{
lua_engine = dynamic_cast<ai::engine_lua *>(*i);
}
}
// Better way, but doesn't work
//ai::component* e = ai::manager::get_singleton().get_active_ai_holder_for_side_dbg(side).get_component(c, "engine[lua]");
//ai::engine_lua* lua_engine = dynamic_cast<ai::engine_lua *>(e);
if (lua_engine == nullptr)
{
//no lua engine is defined for this side.
//so set up a dummy engine
ai::ai_composite * ai_ptr = dynamic_cast<ai::ai_composite *>(c);
assert(ai_ptr);
ai::ai_context& ai_context = ai_ptr->get_ai_context();
config cfg = ai::configuration::get_default_ai_parameters();
lua_engine = new ai::engine_lua(ai_context, cfg);
LOG_LUA << "Created new dummy lua-engine for debug_ai().";
//and add the dummy engine as a component
//to the manager, so we could use it later
cfg.add_child("engine", lua_engine->to_config());
ai::component_manager::add_component(c, "engine[]", cfg);
}
lua_engine->push_ai_table(); // stack: [-1: ai_context]
lua_pushstring(L, "components");
push_component(L, c); // stack: [-1: component tree; -2: ai context]
lua_rawset(L, -3);
return 1;
}
/** Allow undo sets the flag saying whether the event has mutated the game to false. */
int game_lua_kernel::intf_allow_end_turn(lua_State * L)
{
bool allow;
t_string reason;
// The extra iststring is required to prevent totstring from converting a bool value
if(luaW_iststring(L, 1) && luaW_totstring(L, 1, reason)) {
allow = false;
} else {
allow = luaW_toboolean(L, 1);
luaW_totstring(L, 2, reason);
}
gamedata().set_allow_end_turn(allow, reason);
return 0;
}
/** Allow undo sets the flag saying whether the event has mutated the game to false. */
int game_lua_kernel::intf_allow_undo(lua_State * L)
{
if(lua_isboolean(L, 1)) {
play_controller_.pump().set_undo_disabled(!luaW_toboolean(L, 1));
}
else {
play_controller_.pump().set_undo_disabled(false);
}
return 0;
}
int game_lua_kernel::intf_cancel_action(lua_State*)
{
play_controller_.pump().set_action_canceled();
return 0;
}
/** Adding new time_areas dynamically with Standard Location Filters.
* Arg 1: Area ID
* Arg 2: Area locations (either a filter or a list of locations)
* Arg 3: (optional) Area schedule - WML table with [time] tags and optional current_time=
*/
int game_lua_kernel::intf_add_time_area(lua_State * L)
{
log_scope("time_area");
std::string id;
std::set<map_location> locs;
config times;
if(lua_gettop(L) == 1) {
vconfig cfg = luaW_checkvconfig(L, 1);
deprecated_message("Single-argument wesnoth.map.place_area is deprecated. Instead, pass ID, filter, and schedule as three separate arguments.", DEP_LEVEL::INDEFINITE, {1, 17, 0});
id = cfg["id"].str();
const terrain_filter filter(cfg, &game_state_, false);
filter.get_locations(locs, true);
times = cfg.get_parsed_config();
} else {
id = luaL_checkstring(L, 1);
if(!lua_isnoneornil(L, 3))
times = luaW_checkconfig(L, 3);
vconfig cfg{config()};
if(luaW_tovconfig(L, 2, cfg)) {
// Second argument is a location filter
const terrain_filter filter(cfg, &game_state_, false);
filter.get_locations(locs, true);
} else {
// Second argument is an array of locations
luaW_check_locationset(L, 2);
}
}
tod_man().add_time_area(id, locs, times);
LOG_LUA << "Lua inserted time_area '" << id << "'";
return 0;
}
/** Removing new time_areas dynamically with Standard Location Filters. */
int game_lua_kernel::intf_remove_time_area(lua_State * L)
{
log_scope("remove_time_area");
const char * id = luaL_checkstring(L, 1);
tod_man().remove_time_area(id);
LOG_LUA << "Lua removed time_area '" << id << "'";
return 0;
}
int game_lua_kernel::intf_get_time_area(lua_State* L)
{
map_location loc;
if(luaW_tolocation(L, 1, loc)) {
int area_index = tod_man().get_area_on_hex(loc).first;
if(area_index < 0) {
lua_pushnil(L);
return 1;
}
luaW_push_schedule(L, area_index);
return 1;
} else {
std::string area_id = luaL_checkstring(L, 1);
const auto& area_ids = tod_man().get_area_ids();
if(auto iter = std::find(area_ids.begin(), area_ids.end(), area_id); iter == area_ids.end()) {
lua_pushnil(L);
return 1;
} else {
luaW_push_schedule(L, std::distance(area_ids.begin(), iter));
return 1;
}
}
}
/** Replacing the current time of day schedule. */
int game_lua_kernel::intf_replace_schedule(lua_State * L)
{
map_location loc;
if(luaL_testudata(L, 1, "schedule")) {
// Replace the global schedule with a time area's schedule
// Replacing the global schedule with the global schedule
// is also supported but obviously a no-op
int area = luaW_check_schedule(L, 1);
if(area >= 0) tod_man().replace_schedule(tod_man().times(area));
} else {
vconfig cfg = luaW_checkvconfig(L, 1);
if(cfg.get_children("time").empty()) {
ERR_LUA << "attempted to to replace ToD schedule with empty schedule";
} else {
tod_man().replace_schedule(cfg.get_parsed_config());
if (game_display_) {
game_display_->new_turn();
}
LOG_LUA << "replaced ToD schedule";
}
}
return 0;
}
int game_lua_kernel::intf_scroll(lua_State * L)
{
if (game_display_) {
point scroll_to(luaL_checkinteger(L, 1), luaL_checkinteger(L, 2));
game_display_->scroll(scroll_to, true);
lua_remove(L, 1);
lua_remove(L, 1);
lua_push(L, 25);
intf_delay(L);
}
return 0;
}
namespace {
struct lua_report_generator : reports::generator
{
lua_State *mState;
std::string name;
lua_report_generator(lua_State *L, const std::string &n)
: mState(L), name(n) {}
virtual config generate(const reports::context & rc);
};
config lua_report_generator::generate(const reports::context & /*rc*/)
{
lua_State *L = mState;
config cfg;
if (!luaW_getglobal(L, "wesnoth", "interface", "game_display", name))
return cfg;
if (!luaW_pcall(L, 0, 1)) return cfg;
luaW_toconfig(L, -1, cfg);
lua_pop(L, 1);
return cfg;
}
}//unnamed namespace for lua_report_generator
/**
* Executes its upvalue as a theme item generator.
*/
int game_lua_kernel::impl_theme_item(lua_State *L, const std::string& m)
{
reports::context temp_context = reports::context(board(), *game_display_, tod_man(), play_controller_.get_whiteboard(), play_controller_.get_mouse_handler_base());
luaW_pushconfig(L, reports_.generate_report(m.c_str(), temp_context , true));
return 1;
}
/**
* Creates a field of the theme_items table and returns it (__index metamethod).
*/
int game_lua_kernel::impl_theme_items_get(lua_State *L)
{
char const *m = luaL_checkstring(L, 2);
lua_cpp::push_closure(L, std::bind(&game_lua_kernel::impl_theme_item, this, std::placeholders::_1, std::string(m)), 0);
lua_pushvalue(L, 2);
lua_pushvalue(L, -2);
lua_rawset(L, 1);
reports_.register_generator(m, new lua_report_generator(L, m));
return 1;
}
/**
* Sets a field of the theme_items table (__newindex metamethod).
*/
int game_lua_kernel::impl_theme_items_set(lua_State *L)
{
char const *m = luaL_checkstring(L, 2);
lua_pushvalue(L, 2);
lua_pushvalue(L, 3);
lua_rawset(L, 1);
reports_.register_generator(m, new lua_report_generator(L, m));
return 0;
}
/**
* Get all available theme_items (__dir metamethod).
*/
int game_lua_kernel::impl_theme_items_dir(lua_State *L)
{
lua_push(L, reports_.report_list());
return 1;
}
/**
* Gets all the WML variables currently set.
* - Ret 1: WML table
*/
int game_lua_kernel::intf_get_all_vars(lua_State *L) {
luaW_pushconfig(L, gamedata().get_variables());
return 1;
}
/**
* Teeleports a unit to a location.
* Arg 1: unit
* Arg 2: target location
* Arg 3: bool (ignore_passability)
* Arg 4: bool (clear_shroud)
* Arg 5: bool (animate)
*/
int game_lua_kernel::intf_teleport(lua_State *L)
{
events::command_disabler command_disabler;
unit_ptr u = luaW_checkunit_ptr(L, 1, true);
map_location dst = luaW_checklocation(L, 2);
bool check_passability = !luaW_toboolean(L, 3);
bool clear_shroud = luaW_toboolean(L, 4);
bool animate = luaW_toboolean(L, 5);
if (dst == u->get_location() || !map().on_board(dst)) {
return 0;
}
const map_location vacant_dst = find_vacant_tile(dst, pathfind::VACANT_ANY, check_passability ? u.get() : nullptr);
if (!map().on_board(vacant_dst)) {
return 0;
}
// Clear the destination hex before the move (so the animation can be seen).
actions::shroud_clearer clearer;
if ( clear_shroud ) {
clearer.clear_dest(vacant_dst, *u);
}
map_location src_loc = u->get_location();
std::vector<map_location> teleport_path;
teleport_path.push_back(src_loc);
teleport_path.push_back(vacant_dst);
unit_display::move_unit(teleport_path, u, animate);
units().move(src_loc, vacant_dst);
unit::clear_status_caches();
u = units().find(vacant_dst).get_shared_ptr();
u->anim_comp().set_standing();
if ( clear_shroud ) {
// Now that the unit is visibly in position, clear the shroud.
clearer.clear_unit(vacant_dst, *u);
}
if (map().is_village(vacant_dst)) {
actions::get_village(vacant_dst, u->side());
}
game_display_->invalidate_unit_after_move(src_loc, vacant_dst);
// Sighted events.
clearer.fire_events();
return 0;
}
/**
* Logs a message
* Arg 1: (optional) Logger; "wml" for WML errors or deprecations
* Arg 2: Message
* Arg 3: Whether to print to chat (always true if arg 1 is "wml")
*/
int game_lua_kernel::intf_log(lua_State *L)
{
const std::string& logger = lua_isstring(L, 2) ? luaL_checkstring(L, 1) : "";
const std::string& msg = lua_isstring(L, 2) ? luaL_checkstring(L, 2) : luaL_checkstring(L, 1);
if(logger == "wml" || logger == "WML") {
lg::log_to_chat() << msg << '\n';
ERR_WML << msg;
} else {
bool in_chat = luaW_toboolean(L, -1);
game_state_.events_manager_->pump().put_wml_message(logger,msg,in_chat);
}
return 0;
}
int game_lua_kernel::intf_get_fog_or_shroud(lua_State *L, bool fog)
{
team& t = luaW_checkteam(L, 1, board());
map_location loc = luaW_checklocation(L, 2);
lua_pushboolean(L, fog ? t.fogged(loc) : t.shrouded(loc));
return 1;
}
/**
* Implements the lifting and resetting of fog via WML.
* Keeping affect_normal_fog as false causes only the fog override to be affected.
* Otherwise, fog lifting will be implemented similar to normal sight (cannot be
* individually reset and ends at the end of the turn), and fog resetting will, in
* addition to removing overrides, extend the specified teams' normal fog to all
* hexes.
*
* Arg 1: (optional) Side number, or list of side numbers
* Arg 2: List of locations; each is a two-element array or a table with x and y keys
* Arg 3: (optional) boolean
*/
int game_lua_kernel::intf_toggle_fog(lua_State *L, const bool clear)
{
bool affect_normal_fog = false;
if(lua_isboolean(L, -1)) {
affect_normal_fog = luaW_toboolean(L, -1);
}
std::set<int> sides;
if(team* t = luaW_toteam(L, 1)) {
sides.insert(t->side());
} else if(lua_isnumber(L, 1)) {
sides.insert(lua_tointeger(L, 1));
} else if(lua_istable(L, 1) && lua_istable(L, 2)) {
const auto& v = lua_check<std::vector<int>>(L, 1);
sides.insert(v.begin(), v.end());
} else {
for(const team& t : teams()) {
sides.insert(t.side()+1);
}
}
const auto& locs = luaW_check_locationset(L, lua_istable(L, 2) ? 2 : 1);
for(const int &side_num : sides) {
if(side_num < 1 || static_cast<std::size_t>(side_num) > teams().size()) {
continue;
}
team &t = board().get_team(side_num);
if(!clear) {
// Extend fog.
t.remove_fog_override(locs);
if(affect_normal_fog) {
t.refog();
}
} else if(!affect_normal_fog) {
// Force the locations clear of fog.
t.add_fog_override(locs);
} else {
// Simply clear fog from the locations.
for(const map_location &hex : locs) {
t.clear_fog(hex);
}
}
}
// Flag a screen update.
game_display_->recalculate_minimap();
game_display_->invalidate_all();
return 0;
}
// Invokes a synced command
static int intf_invoke_synced_command(lua_State* L)
{
const std::string name = luaL_checkstring(L, 1);
auto it = synced_command::registry().find(name);
config cmd;
if(it == synced_command::registry().end()) {
// Custom command
if(!luaW_getglobal(L, "wesnoth", "custom_synced_commands", name)) {
return luaL_argerror(L, 1, "Unknown synced command");
}
config& cmd_tag = cmd.child_or_add("custom_command");
cmd_tag["name"] = name;
if(!lua_isnoneornil(L, 2)) {
cmd_tag.add_child("data", luaW_checkconfig(L, 2));
}
} else {
// Built-in command
cmd.add_child(name, luaW_checkconfig(L, 2));
}
// Now just forward to the WML action.
luaW_getglobal(L, "wesnoth", "wml_actions", "do_command");
luaW_pushconfig(L, cmd);
luaW_pcall(L, 1, 0);
return 0;
}
struct callbacks_tag {
game_lua_kernel& ref;
callbacks_tag(game_lua_kernel& k) : ref(k) {}
};
#define CALLBACK_GETTER(name, type) LATTR_GETTER(name, lua_index_raw, callbacks_tag, ) { lua_pushcfunction(L, &impl_null_callback<type>); return lua_index_raw(L); }
luaW_Registry callbacksReg{"game_events"};
template<typename Ret>
static int impl_null_callback(lua_State* L) {
if constexpr(std::is_same_v<Ret, void>) return 0;
else lua_push(L, Ret());
return 1;
};
template<> struct lua_object_traits<callbacks_tag> {
inline static auto metatable = "game_events";
inline static game_lua_kernel& get(lua_State* L, int) {
return lua_kernel_base::get_lua_kernel<game_lua_kernel>(L);
}
};
namespace {
CALLBACK_GETTER("on_event", void);
CALLBACK_GETTER("on_load", void);
CALLBACK_GETTER("on_save", config);
CALLBACK_GETTER("on_mouse_action", void);
CALLBACK_GETTER("on_mouse_button", bool);
CALLBACK_GETTER("on_mouse_move", void);
}
static int impl_game_events_dir(lua_State* L) {
return callbacksReg.dir(L);
}
static int impl_game_events_get(lua_State* L) {
return callbacksReg.get(L);
}
template<typename Ret = void>
static bool impl_get_callback(lua_State* L, const std::string& name) {
int top = lua_gettop(L);
if(!luaW_getglobal(L, "wesnoth", "game_events")) {
return false;
}
lua_getfield(L, -1, name.c_str()); // calls impl_game_events_get
lua_pushcfunction(L, &impl_null_callback<Ret>);
if(lua_rawequal(L, -1, -2)) {
lua_settop(L, top);
return false;
}
lua_pop(L, 1);
lua_remove(L, -2);
return true;
}
// END CALLBACK IMPLEMENTATION
game_board & game_lua_kernel::board() {
return game_state_.board_;
}
unit_map & game_lua_kernel::units() {
return game_state_.board_.units();
}
std::vector<team> & game_lua_kernel::teams() {
return game_state_.board_.teams();
}
const gamemap & game_lua_kernel::map() const {
return game_state_.board_.map();
}
game_data & game_lua_kernel::gamedata() {
return game_state_.gamedata_;
}
tod_manager & game_lua_kernel::tod_man() {
return game_state_.tod_manager_;
}
const game_events::queued_event & game_lua_kernel::get_event_info() {
return *queued_events_.top();
}
game_lua_kernel::game_lua_kernel(game_state & gs, play_controller & pc, reports & reports_object)
: lua_kernel_base()
, game_display_(nullptr)
, game_state_(gs)
, play_controller_(pc)
, reports_(reports_object)
, level_lua_()
, EVENT_TABLE(LUA_NOREF)
, queued_events_()
, map_locked_(0)
{
static game_events::queued_event default_queued_event("_from_lua", "", map_location(), map_location(), config());
queued_events_.push(&default_queued_event);
lua_State *L = mState;
cmd_log_ << "Registering game-specific wesnoth lib functions...\n";
// Put some callback functions in the scripting environment.
static luaL_Reg const callbacks[] {
{ "add_known_unit", &intf_add_known_unit },
{ "get_era", &intf_get_era },
{ "get_resource", &intf_get_resource },
{ "modify_ai", &intf_modify_ai_old },
{ "cancel_action", &dispatch<&game_lua_kernel::intf_cancel_action > },
{ "log_replay", &dispatch<&game_lua_kernel::intf_log_replay > },
{ "log", &dispatch<&game_lua_kernel::intf_log > },
{ "redraw", &dispatch<&game_lua_kernel::intf_redraw > },
{ "simulate_combat", &dispatch<&game_lua_kernel::intf_simulate_combat > },
{ nullptr, nullptr }
};lua_getglobal(L, "wesnoth");
if (!lua_istable(L,-1)) {
lua_newtable(L);
}
luaL_setfuncs(L, callbacks, 0);
lua_setglobal(L, "wesnoth");
lua_getglobal(L, "gui");
lua_pushcfunction(L, &dispatch<&game_lua_kernel::intf_gamestate_inspector>);
lua_setfield(L, -2, "show_inspector");
lua_pushcfunction(L, &lua_gui2::intf_show_recruit_dialog);
lua_setfield(L, -2, "show_recruit_dialog");
lua_pushcfunction(L, &lua_gui2::intf_show_recall_dialog);
lua_setfield(L, -2, "show_recall_dialog");
lua_pop(L, 1);
if(play_controller_.get_classification().is_test()) {
// Create the unit_test module
lua_newtable(L);
static luaL_Reg const test_callbacks[] {
{ "fire_wml_menu_item", &dispatch<&game_lua_kernel::intf_fire_wml_menu_item> },
{ nullptr, nullptr }
};
luaL_setfuncs(L, test_callbacks, 0);
lua_setglobal(L, "unit_test");
}
// Create the getside metatable.
cmd_log_ << lua_team::register_metatable(L);
// Create the gettype metatable.
cmd_log_ << lua_unit_type::register_metatable(L);
//Create the getrace metatable
cmd_log_ << lua_race::register_metatable(L);
//Create the unit metatables
cmd_log_ << lua_units::register_metatables(L);
cmd_log_ << lua_units::register_attacks_metatables(L);
// Create the vconfig metatable.
cmd_log_ << lua_common::register_vconfig_metatable(L);
// Create the unit_types table
cmd_log_ << lua_unit_type::register_table(L);
// Create the terrainmap metatables
cmd_log_ << lua_terrainmap::register_metatables(L);
// Create the terrain_types table
cmd_log_ << "Adding terrain_types table...\n";
lua_getglobal(L, "wesnoth");
lua_newuserdatauv(L, 0, 0);
lua_createtable(L, 0, 2);
lua_pushcfunction(L, &dispatch<&game_lua_kernel::impl_get_terrain_info>);
lua_setfield(L, -2, "__index");
lua_pushcfunction(L, &dispatch<&game_lua_kernel::impl_get_terrain_list>);
lua_setfield(L, -2, "__dir");
lua_pushstring(L, "terrain types");
lua_setfield(L, -2, "__metatable");
lua_setmetatable(L, -2);
lua_setfield(L, -2, "terrain_types");
lua_pop(L, 1);
// Create the ai elements table.
cmd_log_ << "Adding ai elements table...\n";
ai::lua_ai_context::init(L);
// Create the current variable with its metatable.
cmd_log_ << "Adding wesnoth current table...\n";
lua_getglobal(L, "wesnoth");
lua_newuserdatauv(L, 0, 0);
lua_createtable(L, 0, 2);
lua_pushcfunction(L, &dispatch<&game_lua_kernel::impl_current_get>);
lua_setfield(L, -2, "__index");
lua_pushcfunction(L, &dispatch<&game_lua_kernel::impl_current_dir>);
lua_setfield(L, -2, "__dir");
lua_pushboolean(L, true);
lua_setfield(L, -2, "__dir_tablelike");
lua_pushstring(L, "current config");
lua_setfield(L, -2, "__metatable");
lua_setmetatable(L, -2);
lua_setfield(L, -2, "current");
lua_pop(L, 1);
// Add functions to the WML module
lua_getglobal(L, "wml");
static luaL_Reg const wml_callbacks[] {
{"tovconfig", &lua_common::intf_tovconfig},
{"eval_conditional", &intf_eval_conditional},
// These aren't actually part of the API - they're used internally by the variable metatable.
{ "get_variable", &dispatch<&game_lua_kernel::intf_get_variable>},
{ "set_variable", &dispatch<&game_lua_kernel::intf_set_variable>},
{ "get_all_vars", &dispatch<&game_lua_kernel::intf_get_all_vars>},
{ nullptr, nullptr }
};
luaL_setfuncs(L, wml_callbacks, 0);
lua_pop(L, 1);
// Add functions to the map module
luaW_getglobal(L, "wesnoth", "map");
static luaL_Reg const map_callbacks[] {
// Map methods
{"terrain_mask", &intf_terrain_mask},
{"on_board", &intf_on_board},
{"on_border", &intf_on_border},
{"iter", &intf_terrainmap_iter},
// Village operations
{"get_owner", &dispatch<&game_lua_kernel::intf_get_village_owner>},
{"set_owner", &dispatch<&game_lua_kernel::intf_set_village_owner>},
// Label operations
{"add_label", &dispatch<&game_lua_kernel::intf_add_label>},
{"remove_label", &dispatch<&game_lua_kernel::intf_remove_label>},
{"get_label", &dispatch<&game_lua_kernel::intf_get_label>},
// Time area operations
{"place_area", &dispatch<&game_lua_kernel::intf_add_time_area>},
{"remove_area", &dispatch<&game_lua_kernel::intf_remove_time_area>},
{"get_area", &dispatch<&game_lua_kernel::intf_get_time_area>},
// Filters
{"find", &dispatch<&game_lua_kernel::intf_get_locations>},
{"matches", &dispatch<&game_lua_kernel::intf_match_location>},
{"replace_if_failed", intf_replace_if_failed},
{ nullptr, nullptr }
};
luaL_setfuncs(L, map_callbacks, 0);
lua_pop(L, 1);
// Create the units module
cmd_log_ << "Adding units module...\n";
static luaL_Reg const unit_callbacks[] {
{"advance", &intf_advance_unit},
{"clone", &intf_copy_unit},
{"erase", &dispatch<&game_lua_kernel::intf_erase_unit>},
{"extract", &dispatch<&game_lua_kernel::intf_extract_unit>},
{"matches", &dispatch<&game_lua_kernel::intf_match_unit>},
{"select", &dispatch<&game_lua_kernel::intf_select_unit>},
{"to_map", &dispatch<&game_lua_kernel::intf_put_unit>},
{"to_recall", &dispatch<&game_lua_kernel::intf_put_recall_unit>},
{"transform", &intf_transform_unit},
{"teleport", &dispatch<&game_lua_kernel::intf_teleport>},
{"ability", &dispatch<&game_lua_kernel::intf_unit_ability>},
{"defense_on", &intf_unit_defense},
{"jamming_on", &intf_unit_jamming_cost},
{"movement_on", &intf_unit_movement_cost},
{"resistance_against", intf_unit_resistance},
{"vision_on", &intf_unit_vision_cost},
{"add_modification", &intf_add_modification},
{"remove_modifications", &intf_remove_modifications},
// Static functions
{"create", &intf_create_unit},
{"find_on_map", &dispatch<&game_lua_kernel::intf_get_units>},
{"find_on_recall", &dispatch<&game_lua_kernel::intf_get_recall_units>},
{"get", &dispatch<&game_lua_kernel::intf_get_unit>},
{"get_hovered", &dispatch<&game_lua_kernel::intf_get_displayed_unit>},
{"create_animator", &dispatch<&game_lua_kernel::intf_create_animator>},
{"create_weapon", intf_create_attack},
{ nullptr, nullptr }
};
lua_getglobal(L, "wesnoth");
lua_newtable(L);
luaL_setfuncs(L, unit_callbacks, 0);
lua_setfield(L, -2, "units");
lua_pop(L, 1);
// Create sides module
cmd_log_ << "Adding sides module...\n";
static luaL_Reg const side_callbacks[] {
{ "is_enemy", &dispatch<&game_lua_kernel::intf_is_enemy> },
{ "matches", &dispatch<&game_lua_kernel::intf_match_side> },
{ "set_id", &dispatch<&game_lua_kernel::intf_set_side_id> },
{ "append_ai", &intf_append_ai },
{ "debug_ai", &intf_debug_ai },
{ "switch_ai", &intf_switch_ai },
// Static functions
{ "find", &dispatch<&game_lua_kernel::intf_get_sides> },
{ "get", &dispatch<&game_lua_kernel::intf_get_side> },
{ "create", &dispatch<&game_lua_kernel::intf_create_side> },
// Shroud operations
{"place_shroud", &dispatch2<&game_lua_kernel::intf_toggle_shroud, true>},
{"remove_shroud", &dispatch2<&game_lua_kernel::intf_toggle_shroud, false>},
{"override_shroud", &dispatch<&game_lua_kernel::intf_override_shroud>},
{"is_shrouded", &dispatch2<&game_lua_kernel::intf_get_fog_or_shroud, false>},
// Fog operations
{"place_fog", &dispatch2<&game_lua_kernel::intf_toggle_fog, false>},
{"remove_fog", &dispatch2<&game_lua_kernel::intf_toggle_fog, true>},
{"is_fogged", &dispatch2<&game_lua_kernel::intf_get_fog_or_shroud, true>},
{ nullptr, nullptr }
};
std::vector<lua_cpp::Reg> const cpp_side_callbacks {
{"add_ai_component", std::bind(intf_modify_ai, std::placeholders::_1, "add")},
{"delete_ai_component", std::bind(intf_modify_ai, std::placeholders::_1, "delete")},
{"change_ai_component", std::bind(intf_modify_ai, std::placeholders::_1, "change")},
{nullptr, nullptr}
};
lua_getglobal(L, "wesnoth");
lua_newtable(L);
luaL_setfuncs(L, side_callbacks, 0);
lua_cpp::set_functions(L, cpp_side_callbacks);
lua_setfield(L, -2, "sides");
lua_pop(L, 1);
// Create the interface module
cmd_log_ << "Adding interface module...\n";
static luaL_Reg const intf_callbacks[] {
{"add_hex_overlay", &dispatch<&game_lua_kernel::intf_add_tile_overlay>},
{"remove_hex_overlay", &dispatch<&game_lua_kernel::intf_remove_tile_overlay>},
{"get_color_adjust", &dispatch<&game_lua_kernel::intf_get_color_adjust>},
{"color_adjust", &dispatch<&game_lua_kernel::intf_color_adjust>},
{"screen_fade", &dispatch<&game_lua_kernel::intf_screen_fade>},
{"delay", &dispatch<&game_lua_kernel::intf_delay>},
{"deselect_hex", &dispatch<&game_lua_kernel::intf_deselect_hex>},
{"highlight_hex", &dispatch<&game_lua_kernel::intf_highlight_hex>},
{"float_label", &dispatch<&game_lua_kernel::intf_float_label>},
{"get_displayed_unit", &dispatch<&game_lua_kernel::intf_get_displayed_unit>},
{"get_hovered_hex", &dispatch<&game_lua_kernel::intf_get_mouseover_tile>},
{"get_selected_hex", &dispatch<&game_lua_kernel::intf_get_selected_tile>},
{"lock", &dispatch<&game_lua_kernel::intf_lock_view>},
{"is_locked", &dispatch<&game_lua_kernel::intf_view_locked>},
{"scroll", &dispatch<&game_lua_kernel::intf_scroll>},
{"scroll_to_hex", &dispatch<&game_lua_kernel::intf_scroll_to_tile>},
{"skip_messages", &dispatch<&game_lua_kernel::intf_skip_messages>},
{"is_skipping_messages", &dispatch<&game_lua_kernel::intf_is_skipping_messages>},
{"zoom", &dispatch<&game_lua_kernel::intf_zoom>},
{"clear_menu_item", &dispatch<&game_lua_kernel::intf_clear_menu_item>},
{"set_menu_item", &dispatch<&game_lua_kernel::intf_set_menu_item>},
{"allow_end_turn", &dispatch<&game_lua_kernel::intf_allow_end_turn>},
{"clear_chat_messages", &dispatch<&game_lua_kernel::intf_clear_messages>},
{"end_turn", &dispatch<&game_lua_kernel::intf_end_turn>},
{"get_viewing_side", &intf_get_viewing_side},
{"add_chat_message", &dispatch<&game_lua_kernel::intf_message>},
{"add_overlay_text", &dispatch2<&game_lua_kernel::intf_set_floating_label, true>},
{"handle_user_interact", &intf_handle_user_interact},
{ nullptr, nullptr }
};
lua_getglobal(L, "wesnoth");
lua_newtable(L);
luaL_setfuncs(L, intf_callbacks, 0);
lua_setfield(L, -2, "interface");
lua_pop(L, 1);
// Create the achievements module
cmd_log_ << "Adding achievements module...\n";
static luaL_Reg const achievement_callbacks[] {
{ "set", &dispatch<&game_lua_kernel::intf_set_achievement> },
{ "has", &dispatch<&game_lua_kernel::intf_has_achievement> },
{ "get", &dispatch<&game_lua_kernel::intf_get_achievement> },
{ "progress", &dispatch<&game_lua_kernel::intf_progress_achievement> },
{ "has_sub_achievement", &dispatch<&game_lua_kernel::intf_has_sub_achievement> },
{ "set_sub_achievement", &dispatch<&game_lua_kernel::intf_set_sub_achievement> },
{ nullptr, nullptr }
};
lua_getglobal(L, "wesnoth");
lua_newtable(L);
luaL_setfuncs(L, achievement_callbacks, 0);
lua_setfield(L, -2, "achievements");
lua_pop(L, 1);
// Create the audio module
cmd_log_ << "Adding audio module...\n";
static luaL_Reg const audio_callbacks[] {
{ "play", &dispatch<&game_lua_kernel::intf_play_sound > },
{ nullptr, nullptr }
};
lua_getglobal(L, "wesnoth");
lua_newtable(L);
luaL_setfuncs(L, audio_callbacks, 0);
lua_setfield(L, -2, "audio");
lua_pop(L, 1);
// Create the paths module
cmd_log_ << "Adding paths module...\n";
static luaL_Reg const path_callbacks[] {
{ "find_cost_map", &dispatch<&game_lua_kernel::intf_find_cost_map > },
{ "find_path", &dispatch<&game_lua_kernel::intf_find_path > },
{ "find_reach", &dispatch<&game_lua_kernel::intf_find_reach > },
{ "find_vacant_hex", &dispatch<&game_lua_kernel::intf_find_vacant_tile > },
{ "find_vision_range", &dispatch<&game_lua_kernel::intf_find_vision_range > },
{ nullptr, nullptr }
};
lua_getglobal(L, "wesnoth");
lua_newtable(L);
luaL_setfuncs(L, path_callbacks, 0);
lua_setfield(L, -2, "paths");
lua_pop(L, 1);
// Create the sync module
cmd_log_ << "Adding sync module...\n";
static luaL_Reg const sync_callbacks[] {
{ "invoke_command", &intf_invoke_synced_command },
{ "run_unsynced", &intf_do_unsynced },
{ "evaluate_single", &intf_synchronize_choice },
{ "evaluate_multiple", &intf_synchronize_choices },
{ nullptr, nullptr }
};
lua_getglobal(L, "wesnoth");
lua_newtable(L);
luaL_setfuncs(L, sync_callbacks, 0);
lua_setfield(L, -2, "sync");
lua_pop(L, 1);
// Create the schedule module
cmd_log_ << "Adding schedule module...\n";
static luaL_Reg const schedule_callbacks[] {
{ "get_time_of_day", &dispatch<&game_lua_kernel::intf_get_time_of_day<false>>},
{ "get_illumination", &dispatch<&game_lua_kernel::intf_get_time_of_day<true>>},
{ "replace", &dispatch<&game_lua_kernel::intf_replace_schedule>},
{ nullptr, nullptr }
};
lua_getglobal(L, "wesnoth");
lua_newtable(L);
luaL_setfuncs(L, schedule_callbacks, 0);
lua_createtable(L, 0, 2);
lua_setmetatable(L, -2);
lua_setfield(L, -2, "schedule");
lua_pop(L, 1);
// Create the playlist table with its metatable
cmd_log_ << lua_audio::register_table(L);
// Create the wml_actions table.
cmd_log_ << "Adding wml_actions table...\n";
lua_getglobal(L, "wesnoth");
lua_newtable(L);
lua_setfield(L, -2, "wml_actions");
lua_pop(L, 1);
// Create the wml_conditionals table.
cmd_log_ << "Adding wml_conditionals table...\n";
lua_getglobal(L, "wesnoth");
lua_newtable(L);
lua_setfield(L, -2, "wml_conditionals");
lua_pop(L, 1);
set_wml_condition("have_unit", &game_events::builtin_conditions::have_unit);
set_wml_condition("have_location", &game_events::builtin_conditions::have_location);
set_wml_condition("variable", &game_events::builtin_conditions::variable_matches);
// Create the effects table.
cmd_log_ << "Adding effects table...\n";
lua_getglobal(L, "wesnoth");
lua_newtable(L);
lua_setfield(L, -2, "effects");
lua_pop(L, 1);
// Create the custom_synced_commands table.
cmd_log_ << "Adding custom_synced_commands table...\n";
lua_getglobal(L, "wesnoth");
lua_newtable(L);
lua_setfield(L, -2, "custom_synced_commands");
lua_pop(L, 1);
// Create the game_events table.
cmd_log_ << "Adding game_events module...\n";
static luaL_Reg const event_callbacks[] {
{ "add", &dispatch<&game_lua_kernel::intf_add_event> },
{ "add_repeating", &dispatch<&game_lua_kernel::intf_add_event_simple<false>> },
{ "add_menu", &dispatch<&game_lua_kernel::intf_add_event_simple<true>> },
{ "add_wml", &dispatch<&game_lua_kernel::intf_add_event_wml> },
{ "remove", &dispatch<&game_lua_kernel::intf_remove_event> },
{ "fire", &dispatch2<&game_lua_kernel::intf_fire_event, false> },
{ "fire_by_id", &dispatch2<&game_lua_kernel::intf_fire_event, true> },
{ "add_undo_actions", &dispatch<&game_lua_kernel::intf_add_undo_actions> },
{ "set_undoable", &dispatch<&game_lua_kernel::intf_allow_undo > },
{ nullptr, nullptr }
};
lua_getglobal(L, "wesnoth");
lua_newtable(L);
luaL_setfuncs(L, event_callbacks, 0);
lua_createtable(L, 0, 2);
lua_pushcfunction(L, &impl_game_events_dir);
lua_setfield(L, -2, "__dir");
lua_pushcfunction(L, &impl_game_events_get);
lua_setfield(L, -2, "__index");
lua_pushstring(L, "game_events");
lua_setfield(L, -2, "__metatable");
lua_setmetatable(L, -2);
lua_setfield(L, -2, "game_events");
lua_pop(L, 1);
// Create the theme_items table.
cmd_log_ << "Adding game_display table...\n";
luaW_getglobal(L, "wesnoth", "interface");
lua_newtable(L);
lua_createtable(L, 0, 2);
lua_pushcfunction(L, &dispatch<&game_lua_kernel::impl_theme_items_get>);
lua_setfield(L, -2, "__index");
lua_pushcfunction(L, &dispatch<&game_lua_kernel::impl_theme_items_set>);
lua_setfield(L, -2, "__newindex");
lua_pushcfunction(L, &dispatch<&game_lua_kernel::impl_theme_items_dir>);
lua_setfield(L, -2, "__dir");
lua_setmetatable(L, -2);
lua_setfield(L, -2, "game_display");
lua_pop(L, 1);
// Create the scenario table.
cmd_log_ << "Adding scenario table...\n";
luaW_getglobal(L, "wesnoth");
lua_newtable(L);
lua_createtable(L, 0, 2);
lua_pushcfunction(L, &dispatch<&game_lua_kernel::impl_scenario_get>);
lua_setfield(L, -2, "__index");
lua_pushcfunction(L, &dispatch<&game_lua_kernel::impl_scenario_set>);
lua_setfield(L, -2, "__newindex");
lua_pushcfunction(L, &dispatch<&game_lua_kernel::impl_scenario_dir>);
lua_setfield(L, -2, "__dir");
lua_setmetatable(L, -2);
lua_setfield(L, -2, "scenario");
lua_pop(L, 1);
lua_settop(L, 0);
for(const auto& handler : game_events::wml_action::registry())
{
set_wml_action(handler.first, handler.second);
}
luaW_getglobal(L, "wesnoth", "effects");
for(const std::string& effect : unit::builtin_effects) {
lua_pushstring(L, effect.c_str());
push_builtin_effect();
lua_rawset(L, -3);
}
lua_settop(L, 0);
// Set up the registry table for event handlers
lua_newtable(L);
EVENT_TABLE = luaL_ref(L, LUA_REGISTRYINDEX);
}
void game_lua_kernel::initialize(const config& level)
{
lua_State *L = mState;
assert(level_lua_.empty());
level_lua_.append_children(level, "lua");
//Create the races table.
cmd_log_ << "Adding races table...\n";
lua_settop(L, 0);
lua_getglobal(L, "wesnoth");
luaW_pushracetable(L);
lua_setfield(L, -2, "races");
lua_pop(L, 1);
// Execute the preload scripts.
cmd_log_ << "Running preload scripts...\n";
game_config::load_config(game_lua_kernel::preload_config);
for (const config &cfg : game_lua_kernel::preload_scripts) {
run_lua_tag(cfg);
}
for (const config &cfg : level_lua_.child_range("lua")) {
run_lua_tag(cfg);
}
}
void game_lua_kernel::set_game_display(game_display * gd) {
game_display_ = gd;
}
/**
* These are the child tags of [scenario] (and the like) that are handled
* elsewhere (in the C++ code).
* Any child tags not in this list will be passed to Lua's on_load event.
*/
static bool is_handled_file_tag(std::string_view s)
{
// Make sure this is sorted, since we binary_search!
using namespace std::literals::string_view_literals;
static constexpr std::array handled_file_tags {
"color_palette"sv,
"color_range"sv,
"display"sv,
"end_level_data"sv,
"era"sv,
"event"sv,
"generator"sv,
"label"sv,
"lua"sv,
"map"sv,
"menu_item"sv,
"modification"sv,
"modify_unit_type"sv,
"music"sv,
"options"sv,
"side"sv,
"sound_source"sv,
"story"sv,
"terrain_graphics"sv,
"time"sv,
"time_area"sv,
"tunnel"sv,
"undo_stack"sv,
"variables"sv
};
return std::binary_search(handled_file_tags.begin(), handled_file_tags.end(), s);
}
/**
* Executes the game_events.on_load function and passes to it all the
* scenario tags not yet handled.
*/
void game_lua_kernel::load_game(const config& level)
{
lua_State *L = mState;
if(!impl_get_callback(L, "on_load"))
return;
lua_newtable(L);
int k = 1;
for(const auto [child_key, child_cfg] : level.all_children_view())
{
if (is_handled_file_tag(child_key)) continue;
lua_createtable(L, 2, 0);
lua_pushstring(L, child_key.c_str());
lua_rawseti(L, -2, 1);
luaW_pushconfig(L, child_cfg);
lua_rawseti(L, -2, 2);
lua_rawseti(L, -2, k++);
}
luaW_pcall(L, 1, 0, true);
}
/**
* Executes the game_events.on_save function and adds to @a cfg the
* returned tags. Also flushes the [lua] tags.
*/
void game_lua_kernel::save_game(config &cfg)
{
lua_State *L = mState;
if(!impl_get_callback<config>(L, "on_save"))
return;
if (!luaW_pcall(L, 0, 1, false))
return;
config v;
luaW_toconfig(L, -1, v);
lua_pop(L, 1);
// Make a copy of the source tag names. Since splice is a destructive operation,
// we can't guarantee that the view will remain valid during iteration.
const auto temp = v.child_name_view();
const std::vector<std::string> src_tags(temp.begin(), temp.end());
for(const auto& key : src_tags) {
if(is_handled_file_tag(key)) {
/*
* It seems the only tags appearing in the config v variable here
* are the core-lua-handled (currently [item] and [objectives])
* and the extra UMC ones.
*/
const std::string m = "Tag is already used: [" + key + "]";
log_error(m.c_str());
continue;
} else {
cfg.splice_children(v, key);
}
}
}
/**
* Executes the game_events.on_event function.
* Returns false if there was no lua handler for this event
*/
bool game_lua_kernel::run_event(const game_events::queued_event& ev)
{
lua_State *L = mState;
if(!impl_get_callback(L, "on_event"))
return false;
queued_event_context dummy(&ev, queued_events_);
lua_pushstring(L, ev.name.c_str());
luaW_pcall(L, 1, 0, false);
return true;
}
void game_lua_kernel::custom_command(const std::string& name, const config& cfg)
{
lua_State *L = mState;
if (!luaW_getglobal(L, "wesnoth", "custom_synced_commands", name)) {
return;
}
luaW_pushconfig(L, cfg);
luaW_pcall(L, 1, 0, false);
}
/**
* Applies its upvalue as an effect
* Arg 1: The unit to apply to
* Arg 3: The [effect] tag contents
* Arg 3: If false, only build description
* Return: The description of the effect
*/
int game_lua_kernel::cfun_builtin_effect(lua_State *L)
{
std::string which_effect = lua_tostring(L, lua_upvalueindex(1));
bool need_apply = luaW_toboolean(L, lua_upvalueindex(2));
// Argument 1 is the implicit "self" argument, which isn't needed here
lua_unit u(luaW_checkunit(L, 2));
config cfg = luaW_checkconfig(L, 3);
// The times= key is supposed to be ignored by the effect function.
// However, just in case someone doesn't realize this, we will set it to 1 here.
cfg["times"] = 1;
if(need_apply) {
u->apply_builtin_effect(which_effect, cfg);
return 0;
} else {
std::string description = u->describe_builtin_effect(which_effect, cfg);
lua_pushstring(L, description.c_str());
return 1;
}
}
/**
* Registers a function for use as an effect handler.
*/
void game_lua_kernel::push_builtin_effect()
{
lua_State *L = mState;
// The effect name is at the top of the stack
int str_i = lua_gettop(L);
lua_newtable(L); // The functor table
lua_newtable(L); // The functor metatable
lua_pushstring(L, "__call");
lua_pushvalue(L, str_i);
lua_pushboolean(L, true);
lua_pushcclosure(L, &dispatch<&game_lua_kernel::cfun_builtin_effect>, 2);
lua_rawset(L, -3); // Set the call metafunction
lua_pushstring(L, "__descr");
lua_pushvalue(L, str_i);
lua_pushboolean(L, false);
lua_pushcclosure(L, &dispatch<&game_lua_kernel::cfun_builtin_effect>, 2);
lua_rawset(L, -3); // Set the descr "metafunction"
lua_setmetatable(L, -2); // Apply the metatable to the functor table
}
/**
* Executes its upvalue as a wml action.
*/
int game_lua_kernel::cfun_wml_action(lua_State *L)
{
game_events::wml_action::handler h = reinterpret_cast<game_events::wml_action::handler>
(lua_touserdata(L, lua_upvalueindex(1)));
vconfig vcfg = luaW_checkvconfig(L, 1);
h(get_event_info(), vcfg);
return 0;
}
/**
* Registers a function for use as an action handler.
*/
void game_lua_kernel::set_wml_action(const std::string& cmd, game_events::wml_action::handler h)
{
lua_State *L = mState;
lua_getglobal(L, "wesnoth");
lua_pushstring(L, "wml_actions");
lua_rawget(L, -2);
lua_pushstring(L, cmd.c_str());
lua_pushlightuserdata(L, reinterpret_cast<void *>(h));
lua_pushcclosure(L, &dispatch<&game_lua_kernel::cfun_wml_action>, 1);
lua_rawset(L, -3);
lua_pop(L, 2);
}
using wml_conditional_handler = bool(*)(const vconfig&);
/**
* Executes its upvalue as a wml condition and returns the result.
*/
static int cfun_wml_condition(lua_State *L)
{
wml_conditional_handler h = reinterpret_cast<wml_conditional_handler>
(lua_touserdata(L, lua_upvalueindex(1)));
vconfig vcfg = luaW_checkvconfig(L, 1);
lua_pushboolean(L, h(vcfg));
return 1;
}
/**
* Registers a function for use as a conditional handler.
*/
void game_lua_kernel::set_wml_condition(const std::string& cmd, wml_conditional_handler h)
{
lua_State *L = mState;
lua_getglobal(L, "wesnoth");
lua_pushstring(L, "wml_conditionals");
lua_rawget(L, -2);
lua_pushstring(L, cmd.c_str());
lua_pushlightuserdata(L, reinterpret_cast<void *>(h));
lua_pushcclosure(L, &cfun_wml_condition, 1);
lua_rawset(L, -3);
lua_pop(L, 2);
}
/**
* Runs a command from an event handler.
* @return true if there is a handler for the command.
* @note @a cfg should be either volatile or long-lived since the Lua
* code may grab it for an arbitrary long time.
*/
bool game_lua_kernel::run_wml_action(const std::string& cmd, const vconfig& cfg,
const game_events::queued_event& ev)
{
lua_State *L = mState;
if (!luaW_getglobal(L, "wesnoth", "wml_actions", cmd))
return false;
queued_event_context dummy(&ev, queued_events_);
luaW_pushvconfig(L, cfg);
luaW_pcall(L, 1, 0, true);
return true;
}
/**
* Evaluates a WML conidition.
*
* @returns Whether the condition passed.
* @note @a cfg should be either volatile or long-lived since the Lua
* code may grab it for an arbitrarily long time.
*/
bool game_lua_kernel::run_wml_conditional(const std::string& cmd, const vconfig& cfg)
{
lua_State* L = mState;
// If an invalid coniditional tag is used, consider it a pass.
if(!luaW_getglobal(L, "wesnoth", "wml_conditionals", cmd)) {
lg::log_to_chat() << "unknown conditional wml: [" << cmd << "]\n";
ERR_WML << "unknown conditional wml: [" << cmd << "]";
return true;
}
luaW_pushvconfig(L, cfg);
// Any runtime error is considered a fail.
if(!luaW_pcall(L, 1, 1, true)) {
return false;
}
bool b = luaW_toboolean(L, -1);
lua_pop(L, 1);
return b;
}
static int intf_run_event_wml(lua_State* L)
{
int argIdx = lua_gettop(L);
if(!luaW_getglobal(L, "wesnoth", "wml_actions", "command")) {
return luaL_error(L, "wesnoth.wml_actions.command is missing");
}
lua_pushvalue(L, argIdx);
lua_call(L, 1, 0);
return 0;
}
int game_lua_kernel::save_wml_event()
{
lua_State* L = mState;
lua_geti(L, LUA_REGISTRYINDEX, EVENT_TABLE);
int evtIdx = lua_gettop(L);
ON_SCOPE_EXIT(L) {
lua_pop(L, 1);
};
lua_pushcfunction(L, intf_run_event_wml);
return luaL_ref(L, evtIdx);
}
int game_lua_kernel::save_wml_event(const std::string& name, const std::string& id, const std::string& code)
{
lua_State* L = mState;
lua_geti(L, LUA_REGISTRYINDEX, EVENT_TABLE);
int evtIdx = lua_gettop(L);
ON_SCOPE_EXIT(L) {
lua_pop(L, 1);
};
std::ostringstream lua_name;
lua_name << "event ";
if(name.empty()) {
lua_name << "<anon>";
} else {
lua_name << name;
}
if(!id.empty()) {
lua_name << "[id=" << id << "]";
}
if(!load_string(code.c_str(), lua_name.str())) {
ERR_LUA << "Failed to register WML event: " << lua_name.str();
return LUA_NOREF;
}
return luaL_ref(L, evtIdx);
}
int game_lua_kernel::save_wml_event(int idx)
{
lua_State* L = mState;
idx = lua_absindex(L, idx);
lua_geti(L, LUA_REGISTRYINDEX, EVENT_TABLE);
int evtIdx = lua_gettop(L);
ON_SCOPE_EXIT(L) {
lua_pop(L, 1);
};
lua_pushvalue(L, idx);
return luaL_ref(L, evtIdx);
}
void game_lua_kernel::clear_wml_event(int ref)
{
lua_State* L = mState;
lua_geti(L, LUA_REGISTRYINDEX, EVENT_TABLE);
luaL_unref(L, -1, ref);
lua_pop(L, 1);
}
bool game_lua_kernel::run_wml_event(int ref, const vconfig& args, const game_events::queued_event& ev, bool* out)
{
lua_State* L = mState;
lua_geti(L, LUA_REGISTRYINDEX, EVENT_TABLE);
ON_SCOPE_EXIT(L) {
lua_pop(L, 1);
};
lua_geti(L, -1, ref);
if(lua_isnil(L, -1)) return false;
luaW_pushvconfig(L, args);
queued_event_context dummy(&ev, queued_events_);
if(luaW_pcall(L, 1, out ? 1 : 0, true)) {
if(out) {
*out = luaW_toboolean(L, -1);
lua_pop(L, 1);
}
return true;
}
return false;
}
/**
* Runs a script from a location filter.
* The script is an already compiled function given by its name.
*/
bool game_lua_kernel::run_filter(char const *name, const map_location& l)
{
lua_pushinteger(mState, l.wml_x());
lua_pushinteger(mState, l.wml_y());
return run_filter(name, 2);
}
/**
* Runs a script from a location filter.
* The script is an already compiled function given by its name.
*/
bool game_lua_kernel::run_filter(char const *name, const team& t)
{
//TODO: instead of passing the lua team object we coudl also jsut pass its
// number. then we wouldn't need this const cast.
luaW_pushteam(mState, const_cast<team&>(t));
return run_filter(name, 1);
}
/**
* Runs a script from a unit filter.
* The script is an already compiled function given by its name.
*/
bool game_lua_kernel::run_filter(char const *name, const unit& u)
{
lua_State *L = mState;
lua_unit* lu = luaW_pushlocalunit(L, const_cast<unit&>(u));
// stack: unit
// put the unit to the stack twice to prevent gc.
lua_pushvalue(L, -1);
// stack: unit, unit
bool res = run_filter(name, 1);
// stack: unit
lu->clear_ref();
lua_pop(L, 1);
return res;
}
/**
* Runs a script from a filter.
* The script is an already compiled function given by its name.
*/
bool game_lua_kernel::run_filter(char const *name, int nArgs)
{
auto ml = map_locker(this);
lua_State *L = mState;
// Get the user filter by name.
const std::vector<std::string>& path = utils::split(name, '.', utils::STRIP_SPACES);
if (!luaW_getglobal(L, path))
{
std::string message = std::string() + "function " + name + " not found";
log_error(message.c_str(), "Lua SUF Error");
//we pushed nothing and can safeley return.
return false;
}
lua_insert(L, -nArgs - 1);
if (!luaW_pcall(L, nArgs, 1)) return false;
bool b = luaW_toboolean(L, -1);
lua_pop(L, 1);
return b;
}
std::string game_lua_kernel::apply_effect(const std::string& name, unit& u, const config& cfg, bool need_apply)
{
lua_State *L = mState;
int top = lua_gettop(L);
std::string descr;
// Stack: nothing
lua_unit* lu = luaW_pushlocalunit(L, u);
// Stack: unit
// (Note: The unit needs to be on the stack twice to prevent untimely GC.)
luaW_pushconfig(L, cfg);
// Stack: unit, cfg
if(luaW_getglobal(L, "wesnoth", "effects", name)) {
auto ml = map_locker(this);
// Stack: unit, cfg, effect
if(lua_istable(L, -1)) {
// Effect is implemented by a table with __call and __descr
if(need_apply) {
lua_pushvalue(L, -1);
// Stack: unit, cfg, effect, effect
lua_pushvalue(L, top + 1);
// Stack: unit, cfg, effect, effect, unit
lua_pushvalue(L, top + 2);
// Stack: unit, cfg, effect, effect, unit, cfg
luaW_pcall(L, 2, 0);
// Stack: unit, cfg, effect
}
if(luaL_getmetafield(L, -1, "__descr")) {
// Stack: unit, cfg, effect, __descr
if(lua_isstring(L, -1)) {
// __descr was a static string
descr = lua_tostring(L, -1);
} else {
lua_pushvalue(L, -2);
// Stack: unit, cfg, effect, __descr, effect
lua_pushvalue(L, top + 1);
// Stack: unit, cfg, effect, __descr, effect, unit
lua_pushvalue(L, top + 2);
// Stack: unit, cfg, effect, __descr, effect, unit, cfg
luaW_pcall(L, 3, 1);
if(lua_isstring(L, -1) && !lua_isnumber(L, -1)) {
descr = lua_tostring(L, -1);
} else {
ERR_LUA << "Effect __descr metafunction should have returned a string, but instead returned ";
if(lua_isnone(L, -1)) {
ERR_LUA << "nothing";
} else {
ERR_LUA << lua_typename(L, lua_type(L, -1));
}
}
}
}
} else if(need_apply) {
// Effect is assumed to be a simple function; no description is provided
lua_pushvalue(L, top + 1);
// Stack: unit, cfg, effect, unit
lua_pushvalue(L, top + 2);
// Stack: unit, cfg, effect, unit, cfg
luaW_pcall(L, 2, 0);
// Stack: unit, cfg
}
}
lua_settop(L, top);
lu->clear_ref();
return descr;
}
ai::lua_ai_context* game_lua_kernel::create_lua_ai_context(char const *code, ai::engine_lua *engine)
{
return ai::lua_ai_context::create(mState,code,engine);
}
ai::lua_ai_action_handler* game_lua_kernel::create_lua_ai_action_handler(char const *code, ai::lua_ai_context &context)
{
return ai::lua_ai_action_handler::create(mState,code,context);
}
void game_lua_kernel::mouse_over_hex_callback(const map_location& loc)
{
lua_State *L = mState;
if(!impl_get_callback(L, "on_mouse_move")) {
return;
}
lua_push(L, loc.wml_x());
lua_push(L, loc.wml_y());
luaW_pcall(L, 2, 0, false);
return;
}
bool game_lua_kernel::mouse_button_callback(const map_location& loc, const std::string &button, const std::string &event)
{
lua_State *L = mState;
if(!impl_get_callback<bool>(L, "on_mouse_button")) {
return false;
}
lua_push(L, loc.wml_x());
lua_push(L, loc.wml_y());
lua_push(L, button);
lua_push(L, event);
if (!luaW_pcall(L, 4, 1)) return false;
bool result = luaW_toboolean(L, -1);
lua_pop(L, 1);
return result;
}
void game_lua_kernel::select_hex_callback(const map_location& loc)
{
lua_State *L = mState;
if(!impl_get_callback(L, "on_mouse_action")) {
return;
}
lua_push(L, loc.wml_x());
lua_push(L, loc.wml_y());
luaW_pcall(L, 2, 0, false);
return;
}