diff options
Diffstat (limited to 'plugins/siege-engine.cpp')
| -rw-r--r-- | plugins/siege-engine.cpp | 1871 |
1 files changed, 1871 insertions, 0 deletions
diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp new file mode 100644 index 00000000..7c880351 --- /dev/null +++ b/plugins/siege-engine.cpp @@ -0,0 +1,1871 @@ +#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 <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"); + +/* + * 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(1, 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); +} + +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 void random_direction(float &x, float &y, float &z) +{ + float a, b, d; + for (;;) { + a = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f; + b = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f; + d = a*a + b*b; + if (d < 1.0f) + break; + } + + float sq = sqrtf(1-d); + x = 2.0f*a*sq; + y = 2.0f*b*sq; + z = 1.0f - 2.0f*d; +} + +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 = random_int(strain_type::COMPRESSIVE); + int power = minv + random_int(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) + { + 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; + + bool is_catapult; + int proj_speed, hit_delay; + std::pair<int, int> fire_range; + + 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) + 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->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->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(); + + 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)); + } + + pworld->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); + } + + pworld->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) + { + pworld->DeletePersistentData(*it); + continue;; + } + + engine->stockpiles.insert(it->ival(1)); + } + + pworld->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); + } + + pworld->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)) + { + pworld->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 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; +} + +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 pworld = Core::getInstance().getWorld(); + auto key = stl_sprintf("siege-engine/ammo/%d", engine->id); + auto entry = pworld->GetPersistentData(key, NULL); + if (!entry.isValid()) + return 0; + + engine->ammo_vector_id = job_item_vector_id::ANY_FREE; + 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 pworld = Core::getInstance().getWorld(); + auto key = stl_sprintf("siege-engine/stockpiles/%d/%d", engine->id, pile_id); + pworld->DeletePersistentData(pworld->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); + + if (!enable_plugin()) + return false; + + auto pworld = Core::getInstance().getWorld(); + auto key = stl_sprintf("siege-engine/stockpiles/%d/%d", bld->id, pile->id); + auto entry = pworld->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); + + if (!enable_plugin()) + return NULL; + + // Save skill limits + auto pworld = Core::getInstance().getWorld(); + auto key = stl_sprintf("siege-engine/profiles/%d", bld->id); + auto entry = pworld->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); + pworld->GetPersistentData(&vec, key, true); + + for (auto it = vec.begin(); it != vec.end(); ++it) + { + if (linear_index(workers, it->ival(1)) < 0) + pworld->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 = pworld->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) + { + 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; + + 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 + Maps::getTileOccupancy(path.goal)->bits.arrow_color = 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 + Maps::getTileOccupancy(path.goal)->bits.arrow_color = COLOR_LIGHTRED; + + // Dabbling always hit in 7x7 area + if (skill < skill_rating::Novice) + { + fail_target.x += random_int(7)-3; + fail_target.y += random_int(7)-3; + aimAtPoint(engine, ProjectilePath(path.origin, fail_target)); + return; + } + + // Exact hit chance + float hit_chance = 1.04f - powf(0.8f, skill); + + if (float(rand())/RAND_MAX < hit_chance) + { + aimAtPoint(engine, path); + return; + } + + // Otherwise perturb + if (skill <= skill_rating::Proficient) + { + // 5x5 + fail_target.x += random_int(5)-2; + fail_target.y += random_int(5)-2; + } + else + { + // 3x3 + int idx = random_int(8); + fail_target.x += offsets[idx][0]; + fail_target.y += offsets[idx][1]; + } + + 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( + random_int(tsize.x), random_int(tsize.y), random_int(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); + } + + 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(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 +}; + +static bool is_enabled = false; + +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 pworld = Core::getInstance().getWorld(); + auto entry = pworld->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_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case SC_MAP_LOADED: + if (!gamemode || *gamemode == game_mode::DWARF) + { + 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; +} + +DFhackCExport command_result plugin_onupdate ( color_ostream &out ) +{ + clear_caches(out); + return CR_OK; +} |
