1989 lines
54 KiB
C++
1989 lines
54 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/MapCache.h>
|
|
#include <modules/World.h>
|
|
#include <modules/Units.h>
|
|
#include <modules/Job.h>
|
|
#include <modules/Materials.h>
|
|
#include <modules/Random.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 "df/job.h"
|
|
#include "df/job_item.h"
|
|
#include "df/item_actual.h"
|
|
#include "df/items_other_id.h"
|
|
#include "df/building_stockpilest.h"
|
|
#include "df/stockpile_links.h"
|
|
#include "df/workshop_profile.h"
|
|
#include "df/strain_type.h"
|
|
#include "df/material.h"
|
|
#include "df/flow_type.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");
|
|
|
|
/*
|
|
Aiming is simulated by using a normal distribution to perturb X and Y.
|
|
|
|
The chance a normal distribution is within n*sigma of median is
|
|
erf(n/sqrt(2)). For direct hit, it must be within 0.5 tiles of
|
|
center, so it is erf(0.5/sigma/sqrt(2)) = erf(1/sigma/sqrt(8)).
|
|
Since it must hit in both X and Y, it must be squared, so final
|
|
is erf(1/sigma/sqrt(8))^2 = erf(skill*coeff/(distance*sqrt(8)))^2.
|
|
|
|
The chance to hit a RxR area is erf(skill*coeff*R/(distance*sqrt(8)))^2.
|
|
|
|
Catapults can fire between 30 and 100, and the projectile is supposed
|
|
to travel in an arc, and is thus harder to aim; yet they require a direct
|
|
hit unless using the feature for firing bins.
|
|
|
|
The coefficient of 30 gives the following probabilities:
|
|
|
|
| | Direct Hit | 3x3 Area Hit |
|
|
| | 30 50 100 | 30 50 100 |
|
|
| Novice | 15% 5% <5% | 75% 40% 10% |
|
|
| Adequate | 45% 20% 5% | 100% 85% 40% |
|
|
| Competent | 75% 40% 10% | 100% 100% 70% |
|
|
| Proficient | 100% 75% 30% | 100% 100% 95% |
|
|
| Professional | 100% 100% 80% | 100% 100% 100% |
|
|
| Legendary | 100% 100% 95% | 100% 100% 100% |
|
|
|
|
Original data:
|
|
|
|
* http://www.wolframalpha.com/input/?i=erf%2830*x%2Fsqrt%288%29%2F30%29^2%2C+erf%2830*x%2Fsqrt%288%29%2F50%29^2%2C+erf%2830*x%2Fsqrt%288%29%2F100%29^2%2C+x+from+0+to+15
|
|
* http://www.wolframalpha.com/input/?i=erf%2830*x*3%2Fsqrt%288%29%2F30%29^2%2C+erf%2830*x*3%2Fsqrt%288%29%2F50%29^2%2C+erf%2830*x*3%2Fsqrt%288%29%2F100%29^2%2C+x+from+0+to+6
|
|
|
|
Ballista can fire up to 200 tiles away, and can't use the bin method
|
|
to compensate for inaccuracy. On the other hand, it shoots straight
|
|
and should be easier to aim. It also damages everything in its path,
|
|
so the hit probability may be estimated using an 1D projection.
|
|
|
|
The coefficient of 48 gives the following probabilities:
|
|
|
|
| | Direct Hit | 1D Hit |
|
|
| | 30 50 100 200 | 30 50 100 200 |
|
|
| Novice | 25% 10% 5% <5% | 55% 35% 20% 10% |
|
|
| Adequate | 80% 45% 15% 5% | 90% 65% 40% 20% |
|
|
| Competent | 95% 70% 30% 8% | 100% 85% 50% 30% |
|
|
| Proficient | 100% 95% 65% 20% | 100% 100% 75% 45% |
|
|
| Accomplished | 100% 100% 95% 60% | 100% 100% 100% 75% |
|
|
| Legendary | 100% 100% 100% 85% | 100% 100% 100% 90% |
|
|
|
|
Original data:
|
|
|
|
* http://www.wolframalpha.com/input/?i=erf%2848*x%2Fsqrt%288%29%2F30%29^2%2C+erf%2848*x%2Fsqrt%288%29%2F50%29^2%2C+erf%2848*x%2Fsqrt%288%29%2F100%29^2%2C+erf%2848*x%2Fsqrt%288%29%2F200%29^2%2C+x+from+0+to+15
|
|
* http://www.wolframalpha.com/input/?i=erf%2848*x%2Fsqrt%288%29%2F30%29%2C+erf%2848*x%2Fsqrt%288%29%2F50%29%2C+erf%2848*x%2Fsqrt%288%29%2F100%29%2C+erf%2848*x%2Fsqrt%288%29%2F200%29%2C+x+from+0+to+15
|
|
|
|
Quality can increase range of both engines by 25% max, so it
|
|
also boosts aiming by 1.06x every step, up to 33.8% gain.
|
|
*/
|
|
|
|
/*
|
|
* Misc. utils
|
|
*/
|
|
|
|
static bool debug_mode = false;
|
|
|
|
static void setDebug(bool on)
|
|
{
|
|
debug_mode = on;
|
|
}
|
|
|
|
static void set_arrow_color(df::coord pos, int color)
|
|
{
|
|
auto tile = Maps::getTileOccupancy(pos);
|
|
|
|
if (tile)
|
|
tile->bits.arrow_color = color;
|
|
}
|
|
|
|
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, float quality)
|
|
{
|
|
if (bld->type == siegeengine_type::Ballista)
|
|
return std::make_pair(1, 200 + int(10 * quality));
|
|
else
|
|
return std::make_pair(30 - int(quality), 100 + int(5 * quality));
|
|
}
|
|
|
|
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 bool is_build_complete(df::building *bld)
|
|
{
|
|
return bld->getBuildStage() >= bld->getMaxBuildStage();
|
|
}
|
|
|
|
static float average_quality(df::building_actual *bld)
|
|
{
|
|
float quality = 0;
|
|
int count = 0;
|
|
|
|
for (size_t i = 0; i < bld->contained_items.size(); i++)
|
|
{
|
|
if (bld->contained_items[i]->use_mode != 2)
|
|
continue;
|
|
count++;
|
|
quality += bld->contained_items[i]->item->getQuality();
|
|
}
|
|
|
|
return count > 0 ? quality/count : 0;
|
|
}
|
|
|
|
static int point_distance(df::coord speed)
|
|
{
|
|
return std::max(abs(speed.x), std::max(abs(speed.y), abs(speed.z)));
|
|
}
|
|
|
|
inline void normalize(float &x, float &y, float &z)
|
|
{
|
|
float dist = sqrtf(x*x + y*y + z*z);
|
|
if (dist == 0.0f) return;
|
|
x /= dist; y /= dist; z /= dist;
|
|
}
|
|
|
|
static Random::MersenneRNG rng;
|
|
|
|
static void random_direction(float &x, float &y, float &z)
|
|
{
|
|
float vec[3];
|
|
rng.unitvector(vec, 3);
|
|
x = vec[0]; y = vec[1]; z = vec[2];
|
|
}
|
|
|
|
static double random_error()
|
|
{
|
|
// Irwin-Hall approximation to normal distribution with n = 3; varies in (-3,3)
|
|
return (rng.drandom0() + rng.drandom0() + rng.drandom0()) * 2.0 - 3.0;
|
|
}
|
|
|
|
// round() is only available in C++11
|
|
static int int_round (double val)
|
|
{
|
|
double frac = val - floor(val);
|
|
if (frac < 0.5)
|
|
return (int)floor(val);
|
|
else
|
|
return (int)ceil(val);
|
|
}
|
|
|
|
static const int WEAR_TICKS = 806400;
|
|
|
|
static bool apply_impact_damage(df::item *item, int minv, int maxv)
|
|
{
|
|
MaterialInfo info(item);
|
|
if (!info.isValid())
|
|
{
|
|
item->setWear(3);
|
|
return false;
|
|
}
|
|
|
|
auto &strength = info.material->strength;
|
|
|
|
// Use random strain type excluding COMPRESSIVE (conveniently last)
|
|
int type = rng.random(strain_type::COMPRESSIVE);
|
|
int power = minv + rng.random(maxv-minv+1);
|
|
|
|
// High elasticity materials just bend
|
|
if (strength.strain_at_yield[type] >= 5000)
|
|
return true;
|
|
|
|
// Instant fracture?
|
|
int fracture = strength.fracture[type];
|
|
if (fracture <= power || info.material->flags.is_set(material_flags::IS_GLASS))
|
|
{
|
|
item->setWear(3);
|
|
return false;
|
|
}
|
|
|
|
// Impact within elastic strain range?
|
|
int yield = strength.yield[type];
|
|
if (yield > power)
|
|
return true;
|
|
|
|
// Can wear?
|
|
auto actual = virtual_cast<df::item_actual>(item);
|
|
if (!actual)
|
|
return false;
|
|
|
|
// Transform plastic deformation to wear
|
|
int max_wear = WEAR_TICKS * 4;
|
|
int cur_wear = WEAR_TICKS * actual->wear + actual->wear_timer;
|
|
cur_wear += int64_t(power - yield)*max_wear/(fracture - yield);
|
|
|
|
if (cur_wear >= max_wear)
|
|
{
|
|
actual->wear = 3;
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
actual->wear = cur_wear / WEAR_TICKS;
|
|
actual->wear_timer = cur_wear % WEAR_TICKS;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Configuration object
|
|
*/
|
|
|
|
static bool enable_plugin();
|
|
|
|
struct EngineInfo {
|
|
int id;
|
|
df::building_siegeenginest *bld;
|
|
|
|
df::coord center;
|
|
coord_range building_rect;
|
|
|
|
float quality;
|
|
bool is_catapult;
|
|
int proj_speed, hit_delay;
|
|
std::pair<int, int> fire_range;
|
|
|
|
double sigma_coeff;
|
|
|
|
coord_range target;
|
|
|
|
df::job_item_vector_id ammo_vector_id;
|
|
df::item_type ammo_item_type;
|
|
|
|
int operator_id, operator_frame;
|
|
|
|
std::set<int> stockpiles;
|
|
df::stockpile_links links;
|
|
df::workshop_profile profile;
|
|
|
|
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; }
|
|
|
|
bool isInRange(int dist) {
|
|
return dist >= fire_range.first && dist <= fire_range.second;
|
|
}
|
|
};
|
|
|
|
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)
|
|
{
|
|
auto ebld = strict_virtual_cast<df::building_siegeenginest>(bld);
|
|
if (!ebld)
|
|
return NULL;
|
|
|
|
auto &obj = engines[bld];
|
|
|
|
if (obj)
|
|
{
|
|
obj->bld = ebld;
|
|
return obj;
|
|
}
|
|
|
|
if (!create || !is_build_complete(bld))
|
|
return NULL;
|
|
|
|
obj = new EngineInfo();
|
|
|
|
obj->id = bld->id;
|
|
obj->bld = ebld;
|
|
obj->center = df::coord(bld->centerx, bld->centery, bld->z);
|
|
obj->building_rect = coord_range(
|
|
df::coord(bld->x1, bld->y1, bld->z),
|
|
df::coord(bld->x2, bld->y2, bld->z)
|
|
);
|
|
obj->quality = average_quality(ebld);
|
|
obj->is_catapult = (ebld->type == siegeengine_type::Catapult);
|
|
obj->proj_speed = 2;
|
|
obj->hit_delay = obj->is_catapult ? 2 : -1;
|
|
obj->fire_range = get_engine_range(ebld, obj->quality);
|
|
|
|
// Base coefficients per engine type, plus 6% exponential bonus per quality level
|
|
obj->sigma_coeff = (obj->is_catapult ? 30.0 : 48.0) * pow(1.06f, obj->quality);
|
|
|
|
obj->ammo_vector_id = job_item_vector_id::BOULDER;
|
|
obj->ammo_item_type = item_type::BOULDER;
|
|
|
|
obj->operator_id = obj->operator_frame = -1;
|
|
|
|
coord_engines[obj->center] = bld;
|
|
return obj;
|
|
}
|
|
|
|
static EngineInfo *find_engine(lua_State *L, int idx, bool create = false, bool silent = false)
|
|
{
|
|
auto bld = Lua::CheckDFObject<df::building_siegeenginest>(L, idx);
|
|
|
|
auto engine = find_engine(bld, create);
|
|
if (!engine && !silent)
|
|
luaL_error(L, "no such engine");
|
|
|
|
return engine;
|
|
}
|
|
|
|
static EngineInfo *find_engine(df::coord pos)
|
|
{
|
|
auto engine = find_engine(coord_engines[pos]);
|
|
|
|
if (engine)
|
|
{
|
|
auto bld0 = df::building::find(engine->id);
|
|
auto bld = strict_virtual_cast<df::building_siegeenginest>(bld0);
|
|
if (!bld)
|
|
return NULL;
|
|
|
|
engine->bld = bld;
|
|
}
|
|
|
|
return engine;
|
|
}
|
|
|
|
/*
|
|
* Configuration management
|
|
*/
|
|
|
|
static void clear_engines()
|
|
{
|
|
for (auto it = engines.begin(); it != engines.end(); ++it)
|
|
delete it->second;
|
|
engines.clear();
|
|
coord_engines.clear();
|
|
}
|
|
|
|
static void load_engines()
|
|
{
|
|
clear_engines();
|
|
|
|
std::vector<PersistentDataItem> vec;
|
|
|
|
World::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));
|
|
}
|
|
|
|
World::GetPersistentData(&vec, "siege-engine/ammo/", 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->ammo_vector_id = (df::job_item_vector_id)it->ival(1);
|
|
engine->ammo_item_type = (df::item_type)it->ival(2);
|
|
}
|
|
|
|
World::GetPersistentData(&vec, "siege-engine/stockpiles/", true);
|
|
for (auto it = vec.begin(); it != vec.end(); ++it)
|
|
{
|
|
auto engine = find_engine(df::building::find(it->ival(0)), true);
|
|
if (!engine)
|
|
continue;
|
|
auto pile = df::building::find(it->ival(1));
|
|
if (!pile || pile->getType() != building_type::Stockpile)
|
|
{
|
|
World::DeletePersistentData(*it);
|
|
continue;;
|
|
}
|
|
|
|
engine->stockpiles.insert(it->ival(1));
|
|
}
|
|
|
|
World::GetPersistentData(&vec, "siege-engine/profiles/", 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->profile.min_level = it->ival(1);
|
|
engine->profile.max_level = it->ival(2);
|
|
}
|
|
|
|
World::GetPersistentData(&vec, "siege-engine/profile-workers/", true);
|
|
for (auto it = vec.begin(); it != vec.end(); ++it)
|
|
{
|
|
auto engine = find_engine(df::building::find(it->ival(0)), true);
|
|
if (!engine)
|
|
continue;
|
|
auto unit = df::unit::find(it->ival(1));
|
|
if (!unit || !Units::isCitizen(unit))
|
|
{
|
|
World::DeletePersistentData(*it);
|
|
continue;
|
|
}
|
|
engine->profile.permitted_workers.push_back(it->ival(1));
|
|
}
|
|
}
|
|
|
|
static int getTargetArea(lua_State *L)
|
|
{
|
|
auto engine = find_engine(L, 1, false, true);
|
|
|
|
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 key = stl_sprintf("siege-engine/target/%d", bld->id);
|
|
World::DeletePersistentData(World::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());
|
|
CHECK_INVALID_ARGUMENT(is_build_complete(bld));
|
|
|
|
if (!enable_plugin())
|
|
return false;
|
|
|
|
auto key = stl_sprintf("siege-engine/target/%d", bld->id);
|
|
auto entry = World::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;
|
|
}
|
|
|
|
static int getAmmoItem(lua_State *L)
|
|
{
|
|
auto engine = find_engine(L, 1, false, true);
|
|
if (!engine)
|
|
Lua::Push(L, item_type::BOULDER);
|
|
else
|
|
Lua::Push(L, engine->ammo_item_type);
|
|
return 1;
|
|
}
|
|
|
|
static int setAmmoItem(lua_State *L)
|
|
{
|
|
if (!enable_plugin())
|
|
return 0;
|
|
|
|
auto engine = find_engine(L, 1, true);
|
|
auto item_type = (df::item_type)luaL_optint(L, 2, item_type::BOULDER);
|
|
if (!is_valid_enum_item(item_type))
|
|
luaL_argerror(L, 2, "invalid item type");
|
|
|
|
auto key = stl_sprintf("siege-engine/ammo/%d", engine->id);
|
|
auto entry = World::GetPersistentData(key, NULL);
|
|
if (!entry.isValid())
|
|
return 0;
|
|
|
|
engine->ammo_vector_id = job_item_vector_id::IN_PLAY;
|
|
engine->ammo_item_type = item_type;
|
|
|
|
FOR_ENUM_ITEMS(job_item_vector_id, id)
|
|
{
|
|
auto other = ENUM_ATTR(job_item_vector_id, other, id);
|
|
auto type = ENUM_ATTR(items_other_id, item, other);
|
|
if (type == item_type)
|
|
{
|
|
engine->ammo_vector_id = id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
entry.ival(0) = engine->id;
|
|
entry.ival(1) = engine->ammo_vector_id;
|
|
entry.ival(2) = engine->ammo_item_type;
|
|
|
|
lua_pushboolean(L, true);
|
|
return 1;
|
|
}
|
|
|
|
static void forgetStockpileLink(EngineInfo *engine, int pile_id)
|
|
{
|
|
engine->stockpiles.erase(pile_id);
|
|
|
|
auto key = stl_sprintf("siege-engine/stockpiles/%d/%d", engine->id, pile_id);
|
|
World::DeletePersistentData(World::GetPersistentData(key));
|
|
}
|
|
|
|
static void update_stockpile_links(EngineInfo *engine)
|
|
{
|
|
engine->links.take_from_pile.clear();
|
|
|
|
for (auto it = engine->stockpiles.begin(); it != engine->stockpiles.end(); )
|
|
{
|
|
int id = *it; ++it;
|
|
auto pile = df::building::find(id);
|
|
|
|
if (!pile || pile->getType() != building_type::Stockpile)
|
|
forgetStockpileLink(engine, id);
|
|
else
|
|
// The vector is sorted, but we are iterating through a sorted set
|
|
engine->links.take_from_pile.push_back(pile);
|
|
}
|
|
}
|
|
|
|
static int getStockpileLinks(lua_State *L)
|
|
{
|
|
auto engine = find_engine(L, 1, false, true);
|
|
if (!engine || engine->stockpiles.empty())
|
|
return 0;
|
|
|
|
update_stockpile_links(engine);
|
|
|
|
auto &links = engine->links.take_from_pile;
|
|
lua_createtable(L, links.size(), 0);
|
|
|
|
for (size_t i = 0; i < links.size(); i++)
|
|
{
|
|
Lua::Push(L, links[i]);
|
|
lua_rawseti(L, -2, i+1);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static bool isLinkedToPile(df::building_siegeenginest *bld, df::building_stockpilest *pile)
|
|
{
|
|
CHECK_NULL_POINTER(bld);
|
|
CHECK_NULL_POINTER(pile);
|
|
|
|
auto engine = find_engine(bld);
|
|
|
|
return engine && engine->stockpiles.count(pile->id);
|
|
}
|
|
|
|
static bool addStockpileLink(df::building_siegeenginest *bld, df::building_stockpilest *pile)
|
|
{
|
|
CHECK_NULL_POINTER(bld);
|
|
CHECK_NULL_POINTER(pile);
|
|
CHECK_INVALID_ARGUMENT(is_build_complete(bld));
|
|
|
|
if (!enable_plugin())
|
|
return false;
|
|
|
|
auto key = stl_sprintf("siege-engine/stockpiles/%d/%d", bld->id, pile->id);
|
|
auto entry = World::GetPersistentData(key, NULL);
|
|
if (!entry.isValid())
|
|
return false;
|
|
|
|
auto engine = find_engine(bld, true);
|
|
|
|
entry.ival(0) = bld->id;
|
|
entry.ival(1) = pile->id;
|
|
|
|
engine->stockpiles.insert(pile->id);
|
|
return true;
|
|
}
|
|
|
|
static bool removeStockpileLink(df::building_siegeenginest *bld, df::building_stockpilest *pile)
|
|
{
|
|
CHECK_NULL_POINTER(bld);
|
|
CHECK_NULL_POINTER(pile);
|
|
|
|
if (auto engine = find_engine(bld))
|
|
{
|
|
forgetStockpileLink(engine, pile->id);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static df::workshop_profile *saveWorkshopProfile(df::building_siegeenginest *bld)
|
|
{
|
|
CHECK_NULL_POINTER(bld);
|
|
CHECK_INVALID_ARGUMENT(is_build_complete(bld));
|
|
|
|
if (!enable_plugin())
|
|
return NULL;
|
|
|
|
// Save skill limits
|
|
auto key = stl_sprintf("siege-engine/profiles/%d", bld->id);
|
|
auto entry = World::GetPersistentData(key, NULL);
|
|
if (!entry.isValid())
|
|
return NULL;
|
|
|
|
auto engine = find_engine(bld, true);
|
|
|
|
entry.ival(0) = engine->id;
|
|
entry.ival(1) = engine->profile.min_level;
|
|
entry.ival(2) = engine->profile.max_level;
|
|
|
|
// Save worker list
|
|
std::vector<PersistentDataItem> vec;
|
|
auto &workers = engine->profile.permitted_workers;
|
|
|
|
key = stl_sprintf("siege-engine/profile-workers/%d", bld->id);
|
|
World::GetPersistentData(&vec, key, true);
|
|
|
|
for (auto it = vec.begin(); it != vec.end(); ++it)
|
|
{
|
|
if (linear_index(workers, it->ival(1)) < 0)
|
|
World::DeletePersistentData(*it);
|
|
}
|
|
|
|
for (size_t i = 0; i < workers.size(); i++)
|
|
{
|
|
key = stl_sprintf("siege-engine/profile-workers/%d/%d", bld->id, workers[i]);
|
|
entry = World::GetPersistentData(key, NULL);
|
|
if (!entry.isValid())
|
|
continue;
|
|
entry.ival(0) = engine->id;
|
|
entry.ival(1) = workers[i];
|
|
}
|
|
|
|
return &engine->profile;
|
|
}
|
|
|
|
static int getOperatorSkill(df::building_siegeenginest *bld, bool force = false)
|
|
{
|
|
CHECK_NULL_POINTER(bld);
|
|
|
|
auto engine = find_engine(bld);
|
|
if (!engine)
|
|
return 0;
|
|
|
|
if (engine->operator_id != -1 &&
|
|
(world->frame_counter - engine->operator_frame) <= 5)
|
|
{
|
|
auto op_unit = df::unit::find(engine->operator_id);
|
|
if (op_unit)
|
|
return Units::getEffectiveSkill(op_unit, job_skill::SIEGEOPERATE);
|
|
}
|
|
|
|
if (force)
|
|
{
|
|
color_ostream_proxy out(Core::getInstance().getConsole());
|
|
out.print("Forced siege operator search\n");
|
|
|
|
auto &active = world->units.active;
|
|
for (size_t i = 0; i < active.size(); i++)
|
|
if (active[i]->pos == engine->center && Units::isCitizen(active[i]))
|
|
return Units::getEffectiveSkill(active[i], job_skill::SIEGEOPERATE);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Trajectory raytracing
|
|
*/
|
|
|
|
struct ProjectilePath {
|
|
static const int DEFAULT_FUDGE = 31;
|
|
|
|
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), fudge_factor(1)
|
|
{
|
|
fudge_delta = df::coord(0,0,0);
|
|
calc_line();
|
|
}
|
|
|
|
ProjectilePath(df::coord origin, df::coord goal, df::coord delta, int factor) :
|
|
origin(origin), goal(goal), fudge_delta(delta), fudge_factor(factor)
|
|
{
|
|
calc_line();
|
|
}
|
|
|
|
ProjectilePath(df::coord origin, df::coord goal, float zdelta, int factor = DEFAULT_FUDGE) :
|
|
origin(origin), goal(goal), fudge_factor(factor)
|
|
{
|
|
fudge_delta = df::coord(0,0,int(factor * zdelta));
|
|
calc_line();
|
|
}
|
|
|
|
void calc_line()
|
|
{
|
|
speed = goal - origin;
|
|
speed.x *= fudge_factor;
|
|
speed.y *= fudge_factor;
|
|
speed.z *= fudge_factor;
|
|
speed = speed + fudge_delta;
|
|
target = origin + speed;
|
|
divisor = point_distance(speed);
|
|
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 ProjectilePath decode_path(lua_State *L, int idx, df::coord origin)
|
|
{
|
|
idx = lua_absindex(L, idx);
|
|
|
|
Lua::StackUnwinder frame(L);
|
|
df::coord goal;
|
|
|
|
lua_getfield(L, idx, "target");
|
|
Lua::CheckDFAssign(L, &goal, frame[1]);
|
|
|
|
lua_getfield(L, idx, "delta");
|
|
|
|
if (!lua_isnil(L, frame[2]))
|
|
{
|
|
lua_getfield(L, idx, "factor");
|
|
int factor = luaL_optnumber(L, frame[3], ProjectilePath::DEFAULT_FUDGE);
|
|
|
|
if (lua_isnumber(L, frame[2]))
|
|
return ProjectilePath(origin, goal, lua_tonumber(L, frame[2]), factor);
|
|
|
|
df::coord delta;
|
|
Lua::CheckDFAssign(L, &delta, frame[2]);
|
|
|
|
return ProjectilePath(origin, goal, delta, factor);
|
|
}
|
|
|
|
return ProjectilePath(origin, goal);
|
|
}
|
|
|
|
static int projPosAtStep(lua_State *L)
|
|
{
|
|
auto engine = find_engine(L, 1);
|
|
auto path = decode_path(L, 2, engine->center);
|
|
int step = luaL_checkint(L, 3);
|
|
Lua::Push(L, path[step]);
|
|
return 1;
|
|
}
|
|
|
|
static bool isPassableTile(df::coord pos)
|
|
{
|
|
auto ptile = Maps::getTileType(pos);
|
|
|
|
return !ptile || FlowPassable(*ptile);
|
|
}
|
|
|
|
static bool isTargetableTile(df::coord pos)
|
|
{
|
|
auto ptile = Maps::getTileType(pos);
|
|
|
|
return ptile && FlowPassable(*ptile) && !isOpenTerrain(*ptile);
|
|
}
|
|
|
|
static bool isTreeTile(df::coord pos)
|
|
{
|
|
auto ptile = Maps::getTileType(pos);
|
|
|
|
return ptile && tileShape(*ptile) == tiletype_shape::TREE;
|
|
}
|
|
|
|
static bool adjustToTarget(EngineInfo *engine, df::coord *pos)
|
|
{
|
|
if (isTargetableTile(*pos))
|
|
return true;
|
|
|
|
for (df::coord fudge = *pos;
|
|
fudge.z <= engine->target.second.z; fudge.z++)
|
|
{
|
|
if (!isTargetableTile(fudge))
|
|
continue;
|
|
*pos = fudge;
|
|
return true;
|
|
}
|
|
|
|
for (df::coord fudge = *pos;
|
|
fudge.z >= engine->target.first.z; fudge.z--)
|
|
{
|
|
if (!isTargetableTile(fudge))
|
|
continue;
|
|
*pos = fudge;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static int adjustToTarget(lua_State *L)
|
|
{
|
|
auto engine = find_engine(L, 1, true);
|
|
df::coord pos;
|
|
Lua::CheckDFAssign(L, &pos, 2);
|
|
bool ok = adjustToTarget(engine, &pos);
|
|
Lua::Push(L, pos);
|
|
Lua::Push(L, ok);
|
|
return 2;
|
|
}
|
|
|
|
static const char* const hit_type_names[] = {
|
|
"wall", "floor", "ceiling", "map_edge", "tree"
|
|
};
|
|
|
|
struct PathMetrics {
|
|
enum CollisionType {
|
|
Impassable,
|
|
Floor,
|
|
Ceiling,
|
|
MapEdge,
|
|
Tree
|
|
} hit_type;
|
|
|
|
int collision_step, collision_z_step;
|
|
int goal_step, goal_z_step, goal_distance;
|
|
|
|
bool hits() const { return collision_step > goal_step; }
|
|
|
|
PathMetrics(const ProjectilePath &path)
|
|
{
|
|
compute(path);
|
|
}
|
|
|
|
void compute(const ProjectilePath &path)
|
|
{
|
|
hit_type = Impassable;
|
|
collision_step = goal_step = goal_z_step = 1000000;
|
|
collision_z_step = 0;
|
|
|
|
goal_distance = point_distance(path.origin - path.goal);
|
|
|
|
int step = 0;
|
|
df::coord prev_pos = path.origin;
|
|
|
|
for (;;) {
|
|
df::coord cur_pos = path[++step];
|
|
if (cur_pos == prev_pos)
|
|
break;
|
|
|
|
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))
|
|
{
|
|
if (isTreeTile(cur_pos))
|
|
{
|
|
// The projectile code has a bug where it will
|
|
// hit a tree on the same tick as a Z level change.
|
|
if (cur_pos.z != prev_pos.z)
|
|
{
|
|
hit_type = Tree;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
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;
|
|
}
|
|
|
|
collision_z_step = step;
|
|
}
|
|
|
|
prev_pos = cur_pos;
|
|
}
|
|
|
|
collision_step = step;
|
|
}
|
|
};
|
|
|
|
enum TargetTileStatus {
|
|
TARGET_OK, TARGET_RANGE, TARGET_BLOCKED, TARGET_SEMIBLOCKED
|
|
};
|
|
static const char* const target_tile_type_names[] = {
|
|
"ok", "out_of_range", "blocked", "semi_blocked"
|
|
};
|
|
|
|
static TargetTileStatus calcTileStatus(EngineInfo *engine, const PathMetrics &raytrace)
|
|
{
|
|
if (raytrace.hits())
|
|
{
|
|
if (engine->isInRange(raytrace.goal_step))
|
|
return TARGET_OK;
|
|
else
|
|
return TARGET_RANGE;
|
|
}
|
|
else
|
|
return TARGET_BLOCKED;
|
|
}
|
|
|
|
static int projPathMetrics(lua_State *L)
|
|
{
|
|
auto engine = find_engine(L, 1);
|
|
auto path = decode_path(L, 2, engine->center);
|
|
|
|
PathMetrics info(path);
|
|
|
|
lua_createtable(L, 0, 7);
|
|
Lua::SetField(L, hit_type_names[info.hit_type], -1, "hit_type");
|
|
Lua::SetField(L, info.collision_step, -1, "collision_step");
|
|
Lua::SetField(L, info.collision_z_step, -1, "collision_z_step");
|
|
Lua::SetField(L, info.goal_distance, -1, "goal_distance");
|
|
if (info.goal_step < info.collision_step)
|
|
Lua::SetField(L, info.goal_step, -1, "goal_step");
|
|
if (info.goal_z_step < info.collision_step)
|
|
Lua::SetField(L, info.goal_z_step, -1, "goal_z_step");
|
|
Lua::SetField(L, target_tile_type_names[calcTileStatus(engine, info)], -1, "status");
|
|
return 1;
|
|
}
|
|
|
|
static TargetTileStatus calcTileStatus(EngineInfo *engine, df::coord target, float zdelta)
|
|
{
|
|
ProjectilePath path(engine->center, target, zdelta);
|
|
PathMetrics raytrace(path);
|
|
return calcTileStatus(engine, raytrace);
|
|
}
|
|
|
|
static TargetTileStatus calcTileStatus(EngineInfo *engine, df::coord target)
|
|
{
|
|
auto status = calcTileStatus(engine, target, 0.0f);
|
|
|
|
if (status == TARGET_BLOCKED)
|
|
{
|
|
if (calcTileStatus(engine, target, 0.5f) < TARGET_BLOCKED)
|
|
return TARGET_SEMIBLOCKED;
|
|
|
|
if (calcTileStatus(engine, target, -0.5f) < TARGET_BLOCKED)
|
|
return TARGET_SEMIBLOCKED;
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
static std::string getTileStatus(df::building_siegeenginest *bld, df::coord tile_pos)
|
|
{
|
|
auto engine = find_engine(bld, true);
|
|
if (!engine)
|
|
return "invalid";
|
|
|
|
return target_tile_type_names[calcTileStatus(engine, tile_pos)];
|
|
}
|
|
|
|
static void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::coord2d ltop, df::coord2d size)
|
|
{
|
|
auto engine = find_engine(bld, true);
|
|
CHECK_NULL_POINTER(engine);
|
|
|
|
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(engine->building_rect, tile_pos))
|
|
continue;
|
|
|
|
Pen cur_tile = Screen::readTile(ltop.x+x, ltop.y+y);
|
|
if (!cur_tile.valid())
|
|
continue;
|
|
|
|
int color = COLOR_YELLOW;
|
|
|
|
switch (calcTileStatus(engine, tile_pos))
|
|
{
|
|
case TARGET_OK:
|
|
color = COLOR_GREEN;
|
|
break;
|
|
case TARGET_RANGE:
|
|
color = COLOR_CYAN;
|
|
break;
|
|
case TARGET_BLOCKED:
|
|
color = COLOR_RED;
|
|
break;
|
|
case TARGET_SEMIBLOCKED:
|
|
color = COLOR_BROWN;
|
|
break;
|
|
}
|
|
|
|
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 = 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
|
|
*/
|
|
|
|
static const float MAX_TIME = 1000000.0f;
|
|
|
|
struct UnitPath {
|
|
df::unit *unit;
|
|
std::map<float, df::coord> path;
|
|
|
|
struct Hit {
|
|
UnitPath *path;
|
|
df::coord pos;
|
|
int dist;
|
|
float time, lmargin, rmargin;
|
|
};
|
|
|
|
static std::map<df::unit*, UnitPath*> cache;
|
|
|
|
static UnitPath *get(df::unit *unit)
|
|
{
|
|
auto &cv = cache[unit];
|
|
if (!cv) cv = new UnitPath(unit);
|
|
return cv;
|
|
};
|
|
|
|
UnitPath(df::unit *unit) : unit(unit)
|
|
{
|
|
if (unit->flags1.bits.rider)
|
|
{
|
|
auto mount = df::unit::find(unit->relations.rider_mount_id);
|
|
|
|
if (mount)
|
|
{
|
|
path = get(mount)->path;
|
|
return;
|
|
}
|
|
}
|
|
|
|
df::coord pos = unit->pos;
|
|
df::coord dest = unit->path.dest;
|
|
auto &upath = unit->path.path;
|
|
|
|
if (dest.isValid() && !upath.x.empty())
|
|
{
|
|
float time = unit->counters.job_counter+0.5f;
|
|
float speed = Units::computeMovementSpeed(unit)/100.0f;
|
|
|
|
if (unit->counters.unconscious > 0)
|
|
time += unit->counters.unconscious;
|
|
|
|
for (size_t i = 0; i < upath.size(); i++)
|
|
{
|
|
df::coord new_pos = upath[i];
|
|
if (new_pos == pos)
|
|
continue;
|
|
|
|
float delay = speed;
|
|
if (new_pos.x != pos.x && new_pos.y != pos.y)
|
|
delay *= 362.0/256.0;
|
|
|
|
path[time] = pos;
|
|
pos = new_pos;
|
|
time += delay + 1;
|
|
}
|
|
}
|
|
|
|
path[MAX_TIME] = pos;
|
|
}
|
|
|
|
void get_margin(std::map<float,df::coord>::iterator &it, float time, float *lmargin, float *rmargin)
|
|
{
|
|
auto it2 = it;
|
|
*lmargin = (it == path.begin()) ? MAX_TIME : time - (--it2)->first;
|
|
*rmargin = (it->first == MAX_TIME) ? MAX_TIME : it->first - time;
|
|
}
|
|
|
|
df::coord posAtTime(float time, float *lmargin = NULL, float *rmargin = NULL)
|
|
{
|
|
CHECK_INVALID_ARGUMENT(time < MAX_TIME);
|
|
|
|
auto it = path.upper_bound(time);
|
|
if (lmargin)
|
|
get_margin(it, time, lmargin, rmargin);
|
|
return it->second;
|
|
}
|
|
|
|
bool findHits(EngineInfo *engine, std::vector<Hit> *hit_points, float bias)
|
|
{
|
|
df::coord origin = engine->center;
|
|
|
|
Hit info;
|
|
info.path = this;
|
|
|
|
for (auto it = path.begin(); it != path.end(); ++it)
|
|
{
|
|
info.pos = it->second;
|
|
info.dist = point_distance(origin - info.pos);
|
|
info.time = float(info.dist)*(engine->proj_speed+1) + engine->hit_delay + bias;
|
|
get_margin(it, info.time, &info.lmargin, &info.rmargin);
|
|
|
|
if (info.lmargin > 0 && info.rmargin > 0)
|
|
{
|
|
if (engine->onTarget(info.pos) && engine->isInRange(info.dist))
|
|
hit_points->push_back(info);
|
|
}
|
|
}
|
|
|
|
return !hit_points->empty();
|
|
}
|
|
};
|
|
|
|
std::map<df::unit*, UnitPath*> UnitPath::cache;
|
|
|
|
static void push_margin(lua_State *L, float margin)
|
|
{
|
|
if (margin == MAX_TIME)
|
|
lua_pushnil(L);
|
|
else
|
|
lua_pushnumber(L, margin);
|
|
}
|
|
|
|
static int traceUnitPath(lua_State *L)
|
|
{
|
|
auto unit = Lua::CheckDFObject<df::unit>(L, 1);
|
|
|
|
CHECK_NULL_POINTER(unit);
|
|
|
|
size_t idx = 1;
|
|
auto info = UnitPath::get(unit);
|
|
lua_createtable(L, info->path.size(), 0);
|
|
|
|
float last_time = 0.0f;
|
|
for (auto it = info->path.begin(); it != info->path.end(); ++it)
|
|
{
|
|
Lua::Push(L, it->second);
|
|
if (idx > 1)
|
|
{
|
|
lua_pushnumber(L, last_time);
|
|
lua_setfield(L, -2, "from");
|
|
}
|
|
if (idx < info->path.size())
|
|
{
|
|
lua_pushnumber(L, it->first);
|
|
lua_setfield(L, -2, "to");
|
|
}
|
|
lua_rawseti(L, -2, idx++);
|
|
last_time = it->first;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int unitPosAtTime(lua_State *L)
|
|
{
|
|
auto unit = Lua::CheckDFObject<df::unit>(L, 1);
|
|
float time = luaL_checknumber(L, 2);
|
|
|
|
CHECK_NULL_POINTER(unit);
|
|
|
|
float lmargin, rmargin;
|
|
auto info = UnitPath::get(unit);
|
|
|
|
Lua::Push(L, info->posAtTime(time, &lmargin, &rmargin));
|
|
push_margin(L, lmargin);
|
|
push_margin(L, rmargin);
|
|
return 3;
|
|
}
|
|
|
|
static bool canTargetUnit(df::unit *unit)
|
|
{
|
|
CHECK_NULL_POINTER(unit);
|
|
|
|
if (unit->flags1.bits.dead ||
|
|
unit->flags3.bits.ghostly ||
|
|
unit->flags1.bits.caged ||
|
|
unit->flags1.bits.hidden_in_ambush)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void proposeUnitHits(EngineInfo *engine, std::vector<UnitPath::Hit> *hits, float bias)
|
|
{
|
|
auto &active = world->units.active;
|
|
|
|
for (size_t i = 0; i < active.size(); i++)
|
|
{
|
|
auto unit = active[i];
|
|
|
|
if (!canTargetUnit(unit))
|
|
continue;
|
|
|
|
UnitPath::get(unit)->findHits(engine, hits, bias);
|
|
}
|
|
}
|
|
|
|
static int proposeUnitHits(lua_State *L)
|
|
{
|
|
auto engine = find_engine(L, 1);
|
|
float bias = luaL_optnumber(L, 2, 0);
|
|
|
|
if (!engine->hasTarget())
|
|
luaL_error(L, "target not set");
|
|
|
|
std::vector<UnitPath::Hit> hits;
|
|
proposeUnitHits(engine, &hits, bias);
|
|
|
|
lua_createtable(L, hits.size(), 0);
|
|
|
|
for (size_t i = 0; i < hits.size(); i++)
|
|
{
|
|
auto &hit = hits[i];
|
|
lua_createtable(L, 0, 6);
|
|
Lua::SetField(L, hit.path->unit, -1, "unit");
|
|
Lua::SetField(L, hit.pos, -1, "pos");
|
|
Lua::SetField(L, hit.dist, -1, "dist");
|
|
Lua::SetField(L, hit.time, -1, "time");
|
|
push_margin(L, hit.lmargin); lua_setfield(L, -2, "lmargin");
|
|
push_margin(L, hit.rmargin); lua_setfield(L, -2, "rmargin");
|
|
lua_rawseti(L, -2, i+1);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int computeNearbyWeight(lua_State *L)
|
|
{
|
|
auto engine = find_engine(L, 1);
|
|
luaL_checktype(L, 2, LUA_TTABLE);
|
|
luaL_checktype(L, 3, LUA_TTABLE);
|
|
const char *fname = luaL_optstring(L, 4, "nearby_weight");
|
|
|
|
std::vector<UnitPath*> units;
|
|
std::vector<float> weights;
|
|
|
|
lua_pushnil(L);
|
|
|
|
while (lua_next(L, 3))
|
|
{
|
|
df::unit *unit;
|
|
if (lua_isnumber(L, -2))
|
|
unit = df::unit::find(lua_tointeger(L, -2));
|
|
else
|
|
unit = Lua::CheckDFObject<df::unit>(L, -2);
|
|
if (!unit)
|
|
continue;
|
|
units.push_back(UnitPath::get(unit));
|
|
weights.push_back(lua_tonumber(L, -1));
|
|
lua_pop(L, 1);
|
|
}
|
|
|
|
lua_pushnil(L);
|
|
|
|
while (lua_next(L, 2))
|
|
{
|
|
Lua::StackUnwinder frame(L, 1);
|
|
|
|
lua_getfield(L, frame[1], "unit");
|
|
df::unit *unit = Lua::CheckDFObject<df::unit>(L, -1);
|
|
|
|
lua_getfield(L, frame[1], "time");
|
|
float time = luaL_checknumber(L, lua_gettop(L));
|
|
|
|
df::coord pos;
|
|
|
|
lua_getfield(L, frame[1], "pos");
|
|
if (lua_isnil(L, -1))
|
|
{
|
|
if (!unit) luaL_error(L, "either unit or pos is required");
|
|
pos = UnitPath::get(unit)->posAtTime(time);
|
|
}
|
|
else
|
|
Lua::CheckDFAssign(L, &pos, -1);
|
|
|
|
float sum = 0.0f;
|
|
|
|
for (size_t i = 0; i < units.size(); i++)
|
|
{
|
|
if (units[i]->unit == unit)
|
|
continue;
|
|
|
|
auto diff = units[i]->posAtTime(time) - pos;
|
|
float dist = 1 + sqrtf(diff.x*diff.x + diff.y*diff.y + diff.z*diff.z);
|
|
sum += weights[i]/(dist*dist);
|
|
}
|
|
|
|
lua_pushnumber(L, sum);
|
|
lua_setfield(L, frame[1], fname);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Projectile hook
|
|
*/
|
|
|
|
static const int offsets[8][2] = {
|
|
{ -1, -1 }, { 0, -1 }, { 1, -1 },
|
|
{ -1, 0 }, { 1, 0 },
|
|
{ -1, 1 }, { 0, 1 }, { 1, 1 }
|
|
};
|
|
|
|
struct projectile_hook : df::proj_itemst {
|
|
typedef df::proj_itemst interpose_base;
|
|
|
|
void aimAtPoint(EngineInfo *engine, const ProjectilePath &path)
|
|
{
|
|
target_pos = path.target;
|
|
|
|
// Debug
|
|
if (debug_mode)
|
|
set_arrow_color(path.goal, COLOR_LIGHTMAGENTA);
|
|
|
|
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]);
|
|
|
|
// Find valid hit point for catapult stones
|
|
if (flags.bits.high_flying)
|
|
{
|
|
if (raytrace.hits())
|
|
fall_threshold = raytrace.goal_step;
|
|
else
|
|
fall_threshold = (raytrace.collision_z_step+raytrace.collision_step-1)/2;
|
|
|
|
while (fall_threshold < raytrace.collision_step-1)
|
|
{
|
|
if (isTargetableTile(path[fall_threshold]))
|
|
break;
|
|
|
|
fall_threshold++;
|
|
}
|
|
}
|
|
|
|
fall_threshold = std::max(fall_threshold, engine->fire_range.first);
|
|
fall_threshold = std::min(fall_threshold, engine->fire_range.second);
|
|
}
|
|
|
|
void aimAtPoint(EngineInfo *engine, int skill, const ProjectilePath &path)
|
|
{
|
|
df::coord fail_target = path.goal;
|
|
|
|
orient_engine(engine->bld, path.goal);
|
|
|
|
// Debug
|
|
if (debug_mode)
|
|
set_arrow_color(path.goal, COLOR_LIGHTRED);
|
|
|
|
// Dabbling always hit in 11x11 area
|
|
if (skill < skill_rating::Novice)
|
|
{
|
|
fail_target.x += rng.random(11)-5;
|
|
fail_target.y += rng.random(11)-5;
|
|
aimAtPoint(engine, ProjectilePath(path.origin, fail_target));
|
|
return;
|
|
}
|
|
|
|
// Otherwise use a normal distribution to simulate errors
|
|
double sigma = point_distance(path.origin - path.goal) / (engine->sigma_coeff * skill);
|
|
|
|
int dx = int_round(random_error() * sigma);
|
|
int dy = int_round(random_error() * sigma);
|
|
|
|
if (dx == 0 && dy == 0)
|
|
{
|
|
aimAtPoint(engine, path);
|
|
return;
|
|
}
|
|
|
|
fail_target.x += dx;
|
|
fail_target.y += dy;
|
|
|
|
ProjectilePath fail(path.origin, fail_target, path.fudge_delta, path.fudge_factor);
|
|
aimAtPoint(engine, fail);
|
|
}
|
|
|
|
void aimAtArea(EngineInfo *engine, int skill)
|
|
{
|
|
df::coord target, last_passable;
|
|
df::coord tbase = engine->target.first;
|
|
df::coord tsize = engine->getTargetSize();
|
|
bool success = false;
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
target = tbase + df::coord(
|
|
rng.random(tsize.x), rng.random(tsize.y), rng.random(tsize.z)
|
|
);
|
|
|
|
if (adjustToTarget(engine, &target))
|
|
last_passable = target;
|
|
else
|
|
continue;
|
|
|
|
ProjectilePath path(engine->center, target, engine->is_catapult ? 0.5f : 0.0f);
|
|
PathMetrics raytrace(path);
|
|
|
|
if (raytrace.hits() && engine->isInRange(raytrace.goal_step))
|
|
{
|
|
aimAtPoint(engine, skill, path);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!last_passable.isValid())
|
|
last_passable = target;
|
|
|
|
aimAtPoint(engine, skill, ProjectilePath(engine->center, last_passable));
|
|
}
|
|
|
|
static int safeAimProjectile(lua_State *L)
|
|
{
|
|
color_ostream &out = *Lua::GetOutput(L);
|
|
auto proj = (projectile_hook*)lua_touserdata(L, 1);
|
|
auto engine = (EngineInfo*)lua_touserdata(L, 2);
|
|
int skill = lua_tointeger(L, 3);
|
|
|
|
if (!Lua::PushModulePublic(out, L, "plugins.siege-engine", "doAimProjectile"))
|
|
luaL_error(L, "Projectile aiming AI not available");
|
|
|
|
Lua::PushDFObject(L, engine->bld);
|
|
Lua::Push(L, proj->item);
|
|
Lua::Push(L, engine->target.first);
|
|
Lua::Push(L, engine->target.second);
|
|
Lua::Push(L, skill);
|
|
|
|
lua_call(L, 5, 1);
|
|
|
|
if (lua_isnil(L, -1))
|
|
proj->aimAtArea(engine, skill);
|
|
else
|
|
proj->aimAtPoint(engine, skill, decode_path(L, -1, engine->center));
|
|
|
|
return 0;
|
|
}
|
|
|
|
void doCheckMovement()
|
|
{
|
|
if (flags.bits.parabolic || distance_flown != 0 ||
|
|
fall_counter != fall_delay || item == NULL)
|
|
return;
|
|
|
|
auto engine = find_engine(origin_pos);
|
|
if (!engine || !engine->hasTarget())
|
|
return;
|
|
|
|
auto L = Lua::Core::State;
|
|
CoreSuspendClaimer suspend;
|
|
color_ostream_proxy out(Core::getInstance().getConsole());
|
|
|
|
int skill = getOperatorSkill(engine->bld, true);
|
|
|
|
// Dabbling can't aim
|
|
if (skill < skill_rating::Novice)
|
|
aimAtArea(engine, skill);
|
|
else
|
|
{
|
|
lua_pushcfunction(L, safeAimProjectile);
|
|
lua_pushlightuserdata(L, this);
|
|
lua_pushlightuserdata(L, engine);
|
|
lua_pushinteger(L, skill);
|
|
|
|
if (!Lua::Core::SafeCall(out, 3, 0))
|
|
aimAtArea(engine, skill);
|
|
}
|
|
|
|
bool forbid_ammo = DF_GLOBAL_VALUE(standing_orders_forbid_used_ammo, false);
|
|
if (forbid_ammo)
|
|
item->flags.bits.forbid = true;
|
|
|
|
switch (item->getType())
|
|
{
|
|
case item_type::CAGE:
|
|
flags.bits.bouncing = false;
|
|
break;
|
|
case item_type::BIN:
|
|
case item_type::BARREL:
|
|
flags.bits.bouncing = false;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void doLaunchContents()
|
|
{
|
|
// Translate cartoon flight speed to parabolic
|
|
float speed = 100000.0f / (fall_delay + 1);
|
|
int min_zspeed = (fall_delay+1)*4900;
|
|
|
|
float bonus = 1.0f + 0.1f*(origin_pos.z -cur_pos.z);
|
|
bonus *= 1.0f + (distance_flown - 60) / 200.0f;
|
|
speed *= bonus;
|
|
|
|
// Flight direction vector
|
|
df::coord dist = target_pos - origin_pos;
|
|
float vx = dist.x, vy = dist.y, vz = fabs((float)dist.z);
|
|
normalize(vx, vy, vz);
|
|
|
|
int start_z = 0;
|
|
|
|
// Start at tile top, if hit a wall
|
|
ProjectilePath path(origin_pos, target_pos);
|
|
auto next_pos = path[distance_flown+1];
|
|
if (next_pos.z == cur_pos.z && !isPassableTile(next_pos))
|
|
start_z = 49000;
|
|
|
|
bool forbid_ammo = DF_GLOBAL_VALUE(standing_orders_forbid_used_ammo, false);
|
|
|
|
MapExtras::MapCache mc;
|
|
std::vector<df::item*> contents;
|
|
Items::getContainedItems(item, &contents);
|
|
|
|
for (size_t i = 0; i < contents.size(); i++)
|
|
{
|
|
auto child = contents[i];
|
|
|
|
if (forbid_ammo)
|
|
child->flags.bits.forbid = true;
|
|
|
|
// Liquids are vaporized so that they cover nearby units
|
|
if (child->isLiquid())
|
|
{
|
|
auto flow = Maps::spawnFlow(
|
|
cur_pos,
|
|
flow_type::MaterialVapor,
|
|
child->getMaterial(), child->getMaterialIndex(),
|
|
100
|
|
);
|
|
|
|
// should it leave a puddle too?..
|
|
if (flow && Items::remove(mc, child))
|
|
continue;
|
|
}
|
|
|
|
auto proj = Items::makeProjectile(mc, child);
|
|
if (!proj) continue;
|
|
|
|
bool keep = apply_impact_damage(child, 50000, int(250000*bonus));
|
|
|
|
proj->flags.bits.no_impact_destroy = keep;
|
|
//proj->flags.bits.bouncing = true;
|
|
proj->flags.bits.piercing = true;
|
|
proj->flags.bits.parabolic = true;
|
|
proj->flags.bits.unk9 = true;
|
|
proj->flags.bits.no_collide = true;
|
|
|
|
proj->pos_z = start_z;
|
|
|
|
float sx, sy, sz;
|
|
random_direction(sx, sy, sz);
|
|
sx += vx*0.7; sy += vy*0.7; sz += vz*0.7;
|
|
if (sz < 0) sz = -sz;
|
|
normalize(sx, sy, sz);
|
|
|
|
proj->speed_x = int(speed * sx);
|
|
proj->speed_y = int(speed * sy);
|
|
proj->speed_z = std::max(min_zspeed, int(speed * sz));
|
|
}
|
|
}
|
|
|
|
DEFINE_VMETHOD_INTERPOSE(bool, checkMovement, ())
|
|
{
|
|
if (flags.bits.high_flying || flags.bits.piercing)
|
|
doCheckMovement();
|
|
|
|
return INTERPOSE_NEXT(checkMovement)();
|
|
}
|
|
|
|
DEFINE_VMETHOD_INTERPOSE(bool, checkImpact, (bool no_damage_floor))
|
|
{
|
|
if (!flags.bits.has_hit_ground && !flags.bits.parabolic &&
|
|
flags.bits.high_flying && !flags.bits.bouncing &&
|
|
!flags.bits.no_impact_destroy && target_pos != origin_pos &&
|
|
item && item->flags.bits.container)
|
|
{
|
|
doLaunchContents();
|
|
}
|
|
|
|
return INTERPOSE_NEXT(checkImpact)(no_damage_floor);
|
|
}
|
|
};
|
|
|
|
IMPLEMENT_VMETHOD_INTERPOSE(projectile_hook, checkMovement);
|
|
IMPLEMENT_VMETHOD_INTERPOSE(projectile_hook, checkImpact);
|
|
|
|
/*
|
|
* Building hook
|
|
*/
|
|
|
|
struct building_hook : df::building_siegeenginest {
|
|
typedef df::building_siegeenginest interpose_base;
|
|
|
|
DEFINE_VMETHOD_INTERPOSE(df::workshop_profile*, getWorkshopProfile, ())
|
|
{
|
|
if (auto engine = find_engine(this))
|
|
return &engine->profile;
|
|
|
|
return INTERPOSE_NEXT(getWorkshopProfile)();
|
|
}
|
|
|
|
DEFINE_VMETHOD_INTERPOSE(df::stockpile_links*, getStockpileLinks, ())
|
|
{
|
|
if (auto engine = find_engine(this))
|
|
{
|
|
update_stockpile_links(engine);
|
|
return &engine->links;
|
|
}
|
|
|
|
return INTERPOSE_NEXT(getStockpileLinks)();
|
|
}
|
|
|
|
DEFINE_VMETHOD_INTERPOSE(void, updateAction, ())
|
|
{
|
|
INTERPOSE_NEXT(updateAction)();
|
|
|
|
if (jobs.empty())
|
|
return;
|
|
|
|
if (auto engine = find_engine(this))
|
|
{
|
|
auto job = jobs[0];
|
|
bool save_op = false;
|
|
|
|
switch (job->job_type)
|
|
{
|
|
case job_type::LoadCatapult:
|
|
if (!job->job_items.empty())
|
|
{
|
|
auto item = job->job_items[0];
|
|
item->item_type = engine->ammo_item_type;
|
|
item->vector_id = engine->ammo_vector_id;
|
|
|
|
switch (item->item_type)
|
|
{
|
|
case item_type::NONE:
|
|
case item_type::BOULDER:
|
|
case item_type::BLOCKS:
|
|
item->mat_type = 0;
|
|
break;
|
|
|
|
case item_type::BIN:
|
|
case item_type::BARREL:
|
|
item->mat_type = -1;
|
|
// A hack to make it take objects assigned to stockpiles.
|
|
// Since reaction_id is not set, the actual value is not used.
|
|
item->contains.resize(1);
|
|
break;
|
|
|
|
default:
|
|
item->mat_type = -1;
|
|
break;
|
|
}
|
|
}
|
|
// fallthrough
|
|
|
|
case job_type::LoadBallista:
|
|
case job_type::FireCatapult:
|
|
case job_type::FireBallista:
|
|
if (auto worker = Job::getWorker(job))
|
|
{
|
|
engine->operator_id = worker->id;
|
|
engine->operator_frame = world->frame_counter;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
IMPLEMENT_VMETHOD_INTERPOSE(building_hook, getWorkshopProfile);
|
|
IMPLEMENT_VMETHOD_INTERPOSE(building_hook, getStockpileLinks);
|
|
IMPLEMENT_VMETHOD_INTERPOSE(building_hook, updateAction);
|
|
|
|
/*
|
|
* Initialization
|
|
*/
|
|
|
|
DFHACK_PLUGIN_LUA_FUNCTIONS {
|
|
DFHACK_LUA_FUNCTION(setDebug),
|
|
DFHACK_LUA_FUNCTION(clearTargetArea),
|
|
DFHACK_LUA_FUNCTION(setTargetArea),
|
|
DFHACK_LUA_FUNCTION(isLinkedToPile),
|
|
DFHACK_LUA_FUNCTION(addStockpileLink),
|
|
DFHACK_LUA_FUNCTION(removeStockpileLink),
|
|
DFHACK_LUA_FUNCTION(saveWorkshopProfile),
|
|
DFHACK_LUA_FUNCTION(getTileStatus),
|
|
DFHACK_LUA_FUNCTION(paintAimScreen),
|
|
DFHACK_LUA_FUNCTION(canTargetUnit),
|
|
DFHACK_LUA_FUNCTION(isPassableTile),
|
|
DFHACK_LUA_FUNCTION(isTreeTile),
|
|
DFHACK_LUA_FUNCTION(isTargetableTile),
|
|
DFHACK_LUA_END
|
|
};
|
|
|
|
DFHACK_PLUGIN_LUA_COMMANDS {
|
|
DFHACK_LUA_COMMAND(getTargetArea),
|
|
DFHACK_LUA_COMMAND(getAmmoItem),
|
|
DFHACK_LUA_COMMAND(setAmmoItem),
|
|
DFHACK_LUA_COMMAND(getStockpileLinks),
|
|
DFHACK_LUA_COMMAND(projPosAtStep),
|
|
DFHACK_LUA_COMMAND(projPathMetrics),
|
|
DFHACK_LUA_COMMAND(adjustToTarget),
|
|
DFHACK_LUA_COMMAND(traceUnitPath),
|
|
DFHACK_LUA_COMMAND(unitPosAtTime),
|
|
DFHACK_LUA_COMMAND(proposeUnitHits),
|
|
DFHACK_LUA_COMMAND(computeNearbyWeight),
|
|
DFHACK_LUA_END
|
|
};
|
|
|
|
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
|
|
|
|
static void enable_hooks(bool enable)
|
|
{
|
|
is_enabled = enable;
|
|
|
|
INTERPOSE_HOOK(projectile_hook, checkMovement).apply(enable);
|
|
INTERPOSE_HOOK(projectile_hook, checkImpact).apply(enable);
|
|
|
|
INTERPOSE_HOOK(building_hook, getWorkshopProfile).apply(enable);
|
|
INTERPOSE_HOOK(building_hook, getStockpileLinks).apply(enable);
|
|
INTERPOSE_HOOK(building_hook, updateAction).apply(enable);
|
|
|
|
if (enable)
|
|
load_engines();
|
|
else
|
|
clear_engines();
|
|
}
|
|
|
|
static bool enable_plugin()
|
|
{
|
|
if (is_enabled)
|
|
return true;
|
|
|
|
auto entry = World::GetPersistentData("siege-engine/enabled", NULL);
|
|
if (!entry.isValid())
|
|
return false;
|
|
|
|
enable_hooks(true);
|
|
return true;
|
|
}
|
|
|
|
static void clear_caches(color_ostream &out)
|
|
{
|
|
if (!UnitPath::cache.empty())
|
|
{
|
|
for (auto it = UnitPath::cache.begin(); it != UnitPath::cache.end(); ++it)
|
|
delete it->second;
|
|
|
|
UnitPath::cache.clear();
|
|
}
|
|
}
|
|
|
|
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
|
|
{
|
|
if (gamemode && *gamemode != game_mode::DWARF)
|
|
return CR_FAILURE;
|
|
|
|
if (enable != is_enabled)
|
|
{
|
|
if (enable)
|
|
enable_plugin();
|
|
else
|
|
{
|
|
World::DeletePersistentData(World::GetPersistentData("siege-engine/enabled"));
|
|
enable_hooks(false);
|
|
}
|
|
}
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
|
|
{
|
|
switch (event) {
|
|
case SC_MAP_LOADED:
|
|
if (!gamemode || *gamemode == game_mode::DWARF)
|
|
{
|
|
bool enable = World::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)
|
|
{
|
|
rng.init();
|
|
|
|
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;
|
|
}
|
|
|
|
DFhackCExport command_result plugin_onupdate ( color_ostream &out )
|
|
{
|
|
clear_caches(out);
|
|
return CR_OK;
|
|
}
|