#include "Core.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 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 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(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 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 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 engines; static std::map coord_engines; static EngineInfo *find_engine(df::building *bld, bool create = false) { auto ebld = strict_virtual_cast(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(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(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 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()); 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); 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); 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 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) { 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 path; struct Hit { UnitPath *path; df::coord pos; int dist; float time, lmargin, rmargin; }; static std::map 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::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_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 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(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(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 *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 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 units; std::vector 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(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(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 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 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_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 &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; }