dfhack/plugins/devel/siege-engine.cpp

691 lines
17 KiB
C++

#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <Error.h>
#include <PluginManager.h>
#include <modules/Gui.h>
#include <modules/Screen.h>
#include <modules/Maps.h>
#include <modules/World.h>
#include <modules/Units.h>
#include <LuaTools.h>
#include <TileTypes.h>
#include <vector>
#include <cstdio>
#include <stack>
#include <string>
#include <cmath>
#include <string.h>
#include <VTableInterpose.h>
#include "df/graphic.h"
#include "df/building_siegeenginest.h"
#include "df/builtin_mats.h"
#include "df/world.h"
#include "df/buildings_other_id.h"
#include "df/job.h"
#include "df/building_drawbuffer.h"
#include "df/ui.h"
#include "df/viewscreen_dwarfmodest.h"
#include "df/ui_build_selector.h"
#include "df/flow_info.h"
#include "df/report.h"
#include "df/proj_itemst.h"
#include "df/unit.h"
#include "df/unit_soul.h"
#include "df/unit_skill.h"
#include "df/physical_attribute_type.h"
#include "df/creature_raw.h"
#include "df/caste_raw.h"
#include "df/caste_raw_flags.h"
#include "df/assumed_identity.h"
#include "df/game_mode.h"
#include "df/unit_misc_trait.h"
#include "MiscUtils.h"
using std::vector;
using std::string;
using std::stack;
using namespace DFHack;
using namespace df::enums;
using df::global::gamemode;
using df::global::gps;
using df::global::world;
using df::global::ui;
using df::global::ui_build_selector;
using Screen::Pen;
DFHACK_PLUGIN("siege-engine");
/*
* Misc. utils
*/
typedef std::pair<df::coord, df::coord> coord_range;
static void set_range(coord_range *target, df::coord p1, df::coord p2)
{
if (!p1.isValid() || !p2.isValid())
{
*target = coord_range();
}
else
{
target->first.x = std::min(p1.x, p2.x);
target->first.y = std::min(p1.y, p2.y);
target->first.z = std::min(p1.z, p2.z);
target->second.x = std::max(p1.x, p2.x);
target->second.y = std::max(p1.y, p2.y);
target->second.z = std::max(p1.z, p2.z);
}
}
static bool is_range_valid(const coord_range &target)
{
return target.first.isValid() && target.second.isValid();
}
static bool is_in_range(const coord_range &target, df::coord pos)
{
return target.first.isValid() && target.second.isValid() &&
target.first.x <= pos.x && pos.x <= target.second.x &&
target.first.y <= pos.y && pos.y <= target.second.y &&
target.first.z <= pos.z && pos.z <= target.second.z;
}
static std::pair<int, int> get_engine_range(df::building_siegeenginest *bld)
{
if (bld->type == siegeengine_type::Ballista)
return std::make_pair(0, 200);
else
return std::make_pair(30, 100);
}
static void orient_engine(df::building_siegeenginest *bld, df::coord target)
{
int dx = target.x - bld->centerx;
int dy = target.y - bld->centery;
if (abs(dx) > abs(dy))
bld->facing = (dx > 0) ?
df::building_siegeenginest::Right :
df::building_siegeenginest::Left;
else
bld->facing = (dy > 0) ?
df::building_siegeenginest::Down :
df::building_siegeenginest::Up;
}
static int random_int(int val)
{
return int(int64_t(rand())*val/RAND_MAX);
}
/*
* Configuration management
*/
static bool enable_plugin();
struct EngineInfo {
int id;
coord_range target;
df::coord center;
bool hasTarget() { return is_range_valid(target); }
bool onTarget(df::coord pos) { return is_in_range(target, pos); }
df::coord getTargetSize() { return target.second - target.first; }
};
static std::map<df::building*, EngineInfo> engines;
static std::map<df::coord, df::building*> coord_engines;
static EngineInfo *find_engine(df::building *bld, bool create = false)
{
if (!bld)
return NULL;
auto it = engines.find(bld);
if (it != engines.end())
return &it->second;
if (!create)
return NULL;
auto *obj = &engines[bld];
obj->id = bld->id;
obj->center = df::coord(bld->centerx, bld->centery, bld->z);
coord_engines[obj->center] = bld;
return obj;
}
static EngineInfo *find_engine(df::coord pos)
{
return find_engine(coord_engines[pos]);
}
static void clear_engines()
{
engines.clear();
coord_engines.clear();
}
static void load_engines()
{
clear_engines();
auto pworld = Core::getInstance().getWorld();
std::vector<PersistentDataItem> vec;
pworld->GetPersistentData(&vec, "siege-engine/target/", true);
for (auto it = vec.begin(); it != vec.end(); ++it)
{
auto engine = find_engine(df::building::find(it->ival(0)), true);
if (!engine) continue;
engine->target.first = df::coord(it->ival(1), it->ival(2), it->ival(3));
engine->target.second = df::coord(it->ival(4), it->ival(5), it->ival(6));
}
}
static int getTargetArea(lua_State *L)
{
auto bld = Lua::CheckDFObject<df::building>(L, 1);
if (!bld) luaL_argerror(L, 1, "null building");
auto engine = find_engine(bld);
if (engine && engine->hasTarget())
{
Lua::Push(L, engine->target.first);
Lua::Push(L, engine->target.second);
}
else
{
lua_pushnil(L);
lua_pushnil(L);
}
return 2;
}
static void clearTargetArea(df::building_siegeenginest *bld)
{
CHECK_NULL_POINTER(bld);
if (auto engine = find_engine(bld))
engine->target = coord_range();
auto pworld = Core::getInstance().getWorld();
auto key = stl_sprintf("siege-engine/target/%d", bld->id);
pworld->DeletePersistentData(pworld->GetPersistentData(key));
}
static bool setTargetArea(df::building_siegeenginest *bld, df::coord target_min, df::coord target_max)
{
CHECK_NULL_POINTER(bld);
CHECK_INVALID_ARGUMENT(target_min.isValid() && target_max.isValid());
if (!enable_plugin())
return false;
auto pworld = Core::getInstance().getWorld();
auto key = stl_sprintf("siege-engine/target/%d", bld->id);
auto entry = pworld->GetPersistentData(key, NULL);
if (!entry.isValid())
return false;
auto engine = find_engine(bld, true);
set_range(&engine->target, target_min, target_max);
entry.ival(0) = bld->id;
entry.ival(1) = engine->target.first.x;
entry.ival(2) = engine->target.first.y;
entry.ival(3) = engine->target.first.z;
entry.ival(4) = engine->target.second.x;
entry.ival(5) = engine->target.second.y;
entry.ival(6) = engine->target.second.z;
df::coord sum = target_min + target_max;
orient_engine(bld, df::coord(sum.x/2, sum.y/2, sum.z/2));
return true;
}
/*
* Trajectory
*/
struct ProjectilePath {
df::coord origin, goal, target, fudge_delta;
int divisor, fudge_factor;
df::coord speed, direction;
ProjectilePath(df::coord origin, df::coord goal) :
origin(origin), goal(goal), target(goal), fudge_factor(1)
{
fudge_delta = df::coord(0,0,0);
calc_line();
}
void fudge(int factor, df::coord delta)
{
fudge_factor = factor;
fudge_delta = delta;
auto diff = goal - origin;
diff.x *= fudge_factor;
diff.y *= fudge_factor;
diff.z *= fudge_factor;
target = origin + diff + fudge_delta;
calc_line();
}
void calc_line()
{
speed = target - origin;
divisor = std::max(abs(speed.x), std::max(abs(speed.y), abs(speed.z)));
if (divisor <= 0) divisor = 1;
direction = df::coord(speed.x>=0?1:-1,speed.y>=0?1:-1,speed.z>=0?1:-1);
}
df::coord operator[] (int i) const
{
int div2 = divisor * 2;
int bias = divisor-1;
return origin + df::coord(
(2*speed.x*i + direction.x*bias)/div2,
(2*speed.y*i + direction.y*bias)/div2,
(2*speed.z*i + direction.z*bias)/div2
);
}
};
static bool isPassableTile(df::coord pos)
{
auto ptile = Maps::getTileType(pos);
return !ptile || FlowPassable(*ptile);
}
struct PathMetrics {
enum CollisionType {
Impassable,
Floor,
Ceiling,
MapEdge
} hit_type;
int collision_step;
int goal_step, goal_z_step;
std::vector<df::coord> coords;
bool hits() { return collision_step > goal_step; }
PathMetrics(const ProjectilePath &path, bool list_coords = false)
{
coords.clear();
collision_step = goal_step = goal_z_step = 1000000;
int step = 0;
df::coord prev_pos = path.origin;
if (list_coords)
coords.push_back(prev_pos);
for (;;) {
df::coord cur_pos = path[++step];
if (cur_pos == prev_pos)
break;
if (list_coords)
coords.push_back(cur_pos);
if (cur_pos.z == path.goal.z)
{
goal_z_step = std::min(step, goal_z_step);
if (cur_pos == path.goal)
goal_step = step;
}
if (!Maps::isValidTilePos(cur_pos))
{
hit_type = PathMetrics::MapEdge;
break;
}
if (!isPassableTile(cur_pos))
{
hit_type = Impassable;
break;
}
if (cur_pos.z != prev_pos.z)
{
int top_z = std::max(prev_pos.z, cur_pos.z);
auto ptile = Maps::getTileType(cur_pos.x, cur_pos.y, top_z);
if (ptile && !LowPassable(*ptile))
{
hit_type = (cur_pos.z > prev_pos.z ? Ceiling : Floor);
break;
}
}
prev_pos = cur_pos;
}
collision_step = step;
}
};
struct AimContext {
df::building_siegeenginest *bld;
df::coord origin;
coord_range building_rect;
EngineInfo *engine;
std::pair<int, int> fire_range;
AimContext(df::building_siegeenginest *bld, EngineInfo *engine)
: bld(bld), engine(engine)
{
origin = df::coord(bld->centerx, bld->centery, bld->z);
building_rect = coord_range(
df::coord(bld->x1, bld->y1, bld->z),
df::coord(bld->x2, bld->y2, bld->z)
);
fire_range = get_engine_range(bld);
}
bool isInRange(const PathMetrics &raytrace)
{
return raytrace.goal_step >= fire_range.first &&
raytrace.goal_step <= fire_range.second;
}
bool adjustToPassable(df::coord *pos)
{
if (isPassableTile(*pos))
return true;
for (df::coord fudge = *pos;
fudge.z < engine->target.second.z; fudge.z++)
{
if (!isPassableTile(fudge))
continue;
*pos = fudge;
return true;
}
for (df::coord fudge = *pos;
fudge.z > engine->target.first.z; fudge.z--)
{
if (!isPassableTile(fudge))
continue;
*pos = fudge;
return true;
}
return false;
}
};
static std::string getTileStatus(df::building_siegeenginest *bld, df::coord tile_pos)
{
AimContext context(bld, NULL);
ProjectilePath path(context.origin, tile_pos);
PathMetrics raytrace(path);
if (raytrace.hits())
{
if (context.isInRange(raytrace))
return "ok";
else
return "out_of_range";
}
else
return "blocked";
}
static void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::coord2d ltop, df::coord2d size)
{
CHECK_NULL_POINTER(bld);
AimContext context(bld, find_engine(bld));
for (int x = 0; x < size.x; x++)
{
for (int y = 0; y < size.y; y++)
{
df::coord tile_pos = view + df::coord(x,y,0);
if (is_in_range(context.building_rect, tile_pos))
continue;
Pen cur_tile = Screen::readTile(ltop.x+x, ltop.y+y);
if (!cur_tile.valid())
continue;
ProjectilePath path(context.origin, tile_pos);
PathMetrics raytrace(path);
int color;
if (raytrace.hits())
{
if (context.isInRange(raytrace))
color = COLOR_GREEN;
else
color = COLOR_CYAN;
}
else
color = COLOR_RED;
if (cur_tile.fg && cur_tile.ch != ' ')
{
cur_tile.fg = color;
cur_tile.bg = 0;
}
else
{
cur_tile.fg = 0;
cur_tile.bg = color;
}
cur_tile.bold = (context.engine && context.engine->onTarget(tile_pos));
if (cur_tile.tile)
cur_tile.tile_mode = Pen::CharColor;
Screen::paintTile(cur_tile, ltop.x+x, ltop.y+y);
}
}
}
/*
* Unit tracking
*/
/*
* Projectile hook
*/
struct projectile_hook : df::proj_itemst {
typedef df::proj_itemst interpose_base;
void aimAtPoint(AimContext &context, ProjectilePath &path, bool bad_shot = false)
{
target_pos = path.target;
PathMetrics raytrace(path);
// Materialize map blocks, or the projectile will crash into them
for (int i = 0; i < raytrace.collision_step; i++)
Maps::ensureTileBlock(path[i]);
if (flags.bits.piercing)
{
if (bad_shot)
fall_threshold = std::min(raytrace.goal_z_step, raytrace.collision_step);
}
else
{
if (bad_shot)
fall_threshold = context.fire_range.second;
else
fall_threshold = raytrace.goal_step;
}
fall_threshold = std::max(fall_threshold, context.fire_range.first);
fall_threshold = std::min(fall_threshold, context.fire_range.second);
}
void aimAtArea(AimContext &context)
{
df::coord target, last_passable;
df::coord tbase = context.engine->target.first;
df::coord tsize = context.engine->getTargetSize();
bool success = false;
for (int i = 0; i < 50; i++)
{
target = tbase + df::coord(
random_int(tsize.x), random_int(tsize.y), random_int(tsize.z)
);
if (context.adjustToPassable(&target))
last_passable = target;
else
continue;
ProjectilePath path(context.origin, target);
PathMetrics raytrace(path);
if (raytrace.hits() && context.isInRange(raytrace))
{
aimAtPoint(context, path);
return;
}
}
if (!last_passable.isValid())
last_passable = target;
ProjectilePath path(context.origin, last_passable);
aimAtPoint(context, path, true);
}
void doCheckMovement()
{
if (distance_flown != 0 || fall_counter != fall_delay)
return;
auto engine = find_engine(origin_pos);
if (!engine || !engine->hasTarget())
return;
auto bld0 = df::building::find(engine->id);
auto bld = strict_virtual_cast<df::building_siegeenginest>(bld0);
if (!bld)
return;
AimContext context(bld, engine);
aimAtArea(context);
}
DEFINE_VMETHOD_INTERPOSE(bool, checkMovement, ())
{
if (flags.bits.high_flying || flags.bits.piercing)
doCheckMovement();
return INTERPOSE_NEXT(checkMovement)();
}
};
IMPLEMENT_VMETHOD_INTERPOSE(projectile_hook, checkMovement);
/*
* Initialization
*/
DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(clearTargetArea),
DFHACK_LUA_FUNCTION(setTargetArea),
DFHACK_LUA_FUNCTION(getTileStatus),
DFHACK_LUA_FUNCTION(paintAimScreen),
DFHACK_LUA_END
};
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(getTargetArea),
DFHACK_LUA_END
};
static bool is_enabled = false;
static void enable_hooks(bool enable)
{
is_enabled = enable;
INTERPOSE_HOOK(projectile_hook, checkMovement).apply(enable);
if (enable)
load_engines();
else
clear_engines();
}
static bool enable_plugin()
{
if (is_enabled)
return true;
auto pworld = Core::getInstance().getWorld();
auto entry = pworld->GetPersistentData("siege-engine/enabled", NULL);
if (!entry.isValid())
return false;
enable_hooks(true);
return true;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
{
auto pworld = Core::getInstance().getWorld();
bool enable = pworld->GetPersistentData("siege-engine/enabled").isValid();
if (enable)
{
out.print("Enabling the siege engine plugin.\n");
enable_hooks(true);
}
else
enable_hooks(false);
}
break;
case SC_MAP_UNLOADED:
enable_hooks(false);
break;
default:
break;
}
return CR_OK;
}
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
if (Core::getInstance().isMapLoaded())
plugin_onstatechange(out, SC_MAP_LOADED);
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
enable_hooks(false);
return CR_OK;
}