summaryrefslogtreecommitdiff
path: root/plugins/devel
diff options
context:
space:
mode:
authorKelly Martin2012-09-16 16:36:51 -0500
committerKelly Martin2012-09-16 16:36:51 -0500
commit38f920dd658ba32dbe5126654b21f776386f07d5 (patch)
treef48c91a895917b0715ce5f7fb5e25f98258d2c08 /plugins/devel
parent96fec768c73aee3fca8937017ec8ef59cf0d7adb (diff)
parentc927623050708da1276e109899549b6f2577180f (diff)
downloaddfhack-38f920dd658ba32dbe5126654b21f776386f07d5.tar.gz
dfhack-38f920dd658ba32dbe5126654b21f776386f07d5.tar.bz2
dfhack-38f920dd658ba32dbe5126654b21f776386f07d5.tar.xz
Merge remote-tracking branch 'q/master'
Diffstat (limited to 'plugins/devel')
-rw-r--r--plugins/devel/CMakeLists.txt2
-rw-r--r--plugins/devel/building_steam_engine.txt92
-rw-r--r--plugins/devel/item_trapcomp_steam_engine.txt12
-rw-r--r--plugins/devel/reaction_steam_engine.txt14
-rw-r--r--plugins/devel/siege-engine.cpp1649
-rw-r--r--plugins/devel/steam-engine.cpp929
6 files changed, 1650 insertions, 1048 deletions
diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt
index f126ae53..39e8f7b6 100644
--- a/plugins/devel/CMakeLists.txt
+++ b/plugins/devel/CMakeLists.txt
@@ -18,7 +18,7 @@ DFHACK_PLUGIN(stripcaged stripcaged.cpp)
DFHACK_PLUGIN(rprobe rprobe.cpp)
DFHACK_PLUGIN(nestboxes nestboxes.cpp)
DFHACK_PLUGIN(vshook vshook.cpp)
-DFHACK_PLUGIN(steam-engine steam-engine.cpp)
+DFHACK_PLUGIN(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
IF(UNIX)
DFHACK_PLUGIN(ref-index ref-index.cpp)
ENDIF()
diff --git a/plugins/devel/building_steam_engine.txt b/plugins/devel/building_steam_engine.txt
deleted file mode 100644
index 48657b0c..00000000
--- a/plugins/devel/building_steam_engine.txt
+++ /dev/null
@@ -1,92 +0,0 @@
-building_steam_engine
-
-[OBJECT:BUILDING]
-
-[BUILDING_WORKSHOP:STEAM_ENGINE]
- [NAME:Steam Engine]
- [NAME_COLOR:4:0:1]
- [DIM:3:3]
- [WORK_LOCATION:2:3]
- [BUILD_LABOR:MECHANIC]
- [BUILD_KEY:CUSTOM_ALT_S]
- [BLOCK:1:1:1:1]
- [BLOCK:2:1:1:1]
- [BLOCK:3:1:0:1]
- [TILE:0:1:240:' ':254]
- [TILE:0:2:' ':' ':128]
- [TILE:0:3:246:' ':' ']
- [COLOR:0:1:6:0:0:0:0:0:7:0:0]
- [COLOR:0:2:0:0:0:0:0:0:7:0:0]
- [COLOR:0:3:MAT:0:0:0:0:0:0]
- [TILE:1:1:246:128:' ']
- [TILE:1:2:' ':' ':254]
- [TILE:1:3:254:'/':240]
- [COLOR:1:1:MAT:7:0:0:0:0:0]
- [COLOR:1:2:0:0:0:0:0:0:7:0:0]
- [COLOR:1:3:7:0:0:6:0:0:6:0:0]
- [TILE:2:1:21:' ':128]
- [TILE:2:2:128:' ':246]
- [TILE:2:3:177:19:177]
- [COLOR:2:1:6:0:0:0:0:0:7:0:0]
- [COLOR:2:2:7:0:0:0:0:0:MAT]
- [COLOR:2:3:7:0:0:6:0:0:7:0:0]
- Tile 15 marks places where machines can connect.
- Tile 19 marks the hearth (color changed to reflect power).
- [TILE:3:1:15:246:15]
- [TILE:3:2:'\':19:'/']
- [TILE:3:3:7:' ':7]
- Color 1:?:1 water indicator, 4:?:1 magma indicator:
- [COLOR:3:1:7:0:0:MAT:7:0:0]
- [COLOR:3:2:6:0:0:0:0:1:6:0:0]
- [COLOR:3:3:1:7:1:0:0:0:4:7:1]
- [BUILD_ITEM:1:BARREL:NONE:INORGANIC:NONE][EMPTY][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:PIPE_SECTION:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON:INORGANIC:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:CHAIN:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:TRAPPARTS:NONE:NONE:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:BLOCKS:NONE:NONE:NONE][BUILDMAT][FIRE_BUILD_SAFE]
-
-[BUILDING_WORKSHOP:MAGMA_STEAM_ENGINE]
- [NAME:Magma Steam Engine]
- [NAME_COLOR:4:0:1]
- [DIM:3:3]
- [WORK_LOCATION:2:3]
- [BUILD_LABOR:MECHANIC]
- [BUILD_KEY:CUSTOM_ALT_E]
- [NEEDS_MAGMA]
- [BLOCK:1:1:1:1]
- [BLOCK:2:1:1:1]
- [BLOCK:3:1:0:1]
- [TILE:0:1:240:' ':254]
- [TILE:0:2:' ':' ':128]
- [TILE:0:3:246:' ':' ']
- [COLOR:0:1:6:0:0:0:0:0:7:0:0]
- [COLOR:0:2:0:0:0:0:0:0:7:0:0]
- [COLOR:0:3:MAT:0:0:0:0:0:0]
- [TILE:1:1:246:128:' ']
- [TILE:1:2:' ':' ':254]
- [TILE:1:3:254:'/':240]
- [COLOR:1:1:MAT:7:0:0:0:0:0]
- [COLOR:1:2:0:0:0:0:0:0:7:0:0]
- [COLOR:1:3:7:0:0:6:0:0:6:0:0]
- [TILE:2:1:21:' ':128]
- [TILE:2:2:128:' ':246]
- [TILE:2:3:177:19:177]
- [COLOR:2:1:6:0:0:0:0:0:7:0:0]
- [COLOR:2:2:7:0:0:0:0:0:MAT]
- [COLOR:2:3:7:0:0:6:0:0:7:0:0]
- Tile 15 marks places where machines can connect.
- Tile 19 marks the hearth (color changed to reflect power).
- [TILE:3:1:15:246:15]
- [TILE:3:2:'\':19:'/']
- [TILE:3:3:7:' ':7]
- Color 1:?:1 water indicator, 4:?:1 magma indicator:
- [COLOR:3:1:7:0:0:MAT:7:0:0]
- [COLOR:3:2:6:0:0:0:0:1:6:0:0]
- [COLOR:3:3:1:7:1:0:0:0:4:7:1]
- [BUILD_ITEM:1:BARREL:NONE:INORGANIC:NONE][EMPTY][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:PIPE_SECTION:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON:INORGANIC:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:CHAIN:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:TRAPPARTS:NONE:NONE:NONE][CAN_USE_ARTIFACT]
- [BUILD_ITEM:1:BLOCKS:NONE:NONE:NONE][BUILDMAT][MAGMA_BUILD_SAFE]
diff --git a/plugins/devel/item_trapcomp_steam_engine.txt b/plugins/devel/item_trapcomp_steam_engine.txt
deleted file mode 100644
index bae6f5b2..00000000
--- a/plugins/devel/item_trapcomp_steam_engine.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-item_trapcomp_steam_engine
-
-[OBJECT:ITEM]
-
-[ITEM_TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON]
-[NAME:piston:pistons]
-[ADJECTIVE:huge]
-[SIZE:1600]
-[HITS:1]
-[MATERIAL_SIZE:6]
-[METAL]
-[ATTACK:BLUNT:40:200:bash:bashes:NO_SUB:2000]
diff --git a/plugins/devel/reaction_steam_engine.txt b/plugins/devel/reaction_steam_engine.txt
deleted file mode 100644
index 175ffdd5..00000000
--- a/plugins/devel/reaction_steam_engine.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-reaction_steam_engine
-
-[OBJECT:REACTION]
-
-[REACTION:STOKE_BOILER]
- [NAME:stoke the boiler]
- [BUILDING:STEAM_ENGINE:CUSTOM_S]
- [BUILDING:MAGMA_STEAM_ENGINE:CUSTOM_S]
- [FUEL]
- [SKILL:SMELT]
- Dimension is the number of days it can produce 100 power * 100.
- I.e. with 2000 it means energy of 1 job = 1 water wheel for 20 days.
- [PRODUCT:100:1:LIQUID_MISC:NONE:WATER][PRODUCT_DIMENSION:2000]
-
diff --git a/plugins/devel/siege-engine.cpp b/plugins/devel/siege-engine.cpp
new file mode 100644
index 00000000..a41bfe5f
--- /dev/null
+++ b/plugins/devel/siege-engine.cpp
@@ -0,0 +1,1649 @@
+#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 <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.h"
+#include "df/items_other_id.h"
+#include "df/building_stockpilest.h"
+#include "df/stockpile_links.h"
+#include "df/workshop_profile.h"
+
+#include "MiscUtils.h"
+
+using std::vector;
+using std::string;
+using std::stack;
+using namespace DFHack;
+using namespace df::enums;
+
+using df::global::gamemode;
+using df::global::gps;
+using df::global::world;
+using df::global::ui;
+using df::global::ui_build_selector;
+
+using Screen::Pen;
+
+DFHACK_PLUGIN("siege-engine");
+
+/*
+ * Misc. utils
+ */
+
+typedef std::pair<df::coord, df::coord> coord_range;
+
+static void set_range(coord_range *target, df::coord p1, df::coord p2)
+{
+ if (!p1.isValid() || !p2.isValid())
+ {
+ *target = coord_range();
+ }
+ else
+ {
+ target->first.x = std::min(p1.x, p2.x);
+ target->first.y = std::min(p1.y, p2.y);
+ target->first.z = std::min(p1.z, p2.z);
+ target->second.x = std::max(p1.x, p2.x);
+ target->second.y = std::max(p1.y, p2.y);
+ target->second.z = std::max(p1.z, p2.z);
+ }
+}
+
+static bool is_range_valid(const coord_range &target)
+{
+ return target.first.isValid() && target.second.isValid();
+}
+
+static bool is_in_range(const coord_range &target, df::coord pos)
+{
+ return target.first.isValid() && target.second.isValid() &&
+ target.first.x <= pos.x && pos.x <= target.second.x &&
+ target.first.y <= pos.y && pos.y <= target.second.y &&
+ target.first.z <= pos.z && pos.z <= target.second.z;
+}
+
+static std::pair<int, int> get_engine_range(df::building_siegeenginest *bld)
+{
+ if (bld->type == siegeengine_type::Ballista)
+ return std::make_pair(0, 200);
+ else
+ return std::make_pair(30, 100);
+}
+
+static void orient_engine(df::building_siegeenginest *bld, df::coord target)
+{
+ int dx = target.x - bld->centerx;
+ int dy = target.y - bld->centery;
+
+ if (abs(dx) > abs(dy))
+ bld->facing = (dx > 0) ?
+ df::building_siegeenginest::Right :
+ df::building_siegeenginest::Left;
+ else
+ bld->facing = (dy > 0) ?
+ df::building_siegeenginest::Down :
+ df::building_siegeenginest::Up;
+}
+
+static int random_int(int val)
+{
+ return int(int64_t(rand())*val/RAND_MAX);
+}
+
+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;
+}
+
+/*
+ * 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 = 3;
+ 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;
+
+ 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;
+}
+
+/*
+ * Projectile hook
+ */
+
+struct projectile_hook : df::proj_itemst {
+ typedef df::proj_itemst interpose_base;
+
+ void aimAtPoint(EngineInfo *engine, const ProjectilePath &path)
+ {
+ target_pos = path.target;
+
+ PathMetrics raytrace(path);
+
+ // Materialize map blocks, or the projectile will crash into them
+ for (int i = 0; i < raytrace.collision_step; i++)
+ Maps::ensureTileBlock(path[i]);
+
+ // 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 aimAtArea(EngineInfo *engine)
+ {
+ 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, path);
+ return;
+ }
+ }
+
+ if (!last_passable.isValid())
+ last_passable = target;
+
+ aimAtPoint(engine, 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);
+ else
+ proj->aimAtPoint(engine, 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);
+
+ 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);
+
+ 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;
+
+ // Flight direction vector
+ df::coord dist = target_pos - origin_pos;
+ float vx = dist.x, vy = dist.y, vz = fabs(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;
+
+ 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];
+ auto proj = Items::makeProjectile(mc, child);
+ if (!proj) continue;
+
+ proj->flags.bits.no_impact_destroy = true;
+ //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 = 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_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:
+ {
+ 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;
+}
diff --git a/plugins/devel/steam-engine.cpp b/plugins/devel/steam-engine.cpp
deleted file mode 100644
index 5c344f78..00000000
--- a/plugins/devel/steam-engine.cpp
+++ /dev/null
@@ -1,929 +0,0 @@
-#include "Core.h"
-#include <Console.h>
-#include <Export.h>
-#include <PluginManager.h>
-#include <modules/Gui.h>
-#include <modules/Screen.h>
-#include <modules/Maps.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_workshopst.h"
-#include "df/building_def_workshopst.h"
-#include "df/item_liquid_miscst.h"
-#include "df/power_info.h"
-#include "df/workshop_type.h"
-#include "df/builtin_mats.h"
-#include "df/world.h"
-#include "df/buildings_other_id.h"
-#include "df/machine.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 "MiscUtils.h"
-
-using std::vector;
-using std::string;
-using std::stack;
-using namespace DFHack;
-using namespace df::enums;
-
-using df::global::gps;
-using df::global::world;
-using df::global::ui;
-using df::global::ui_build_selector;
-
-DFHACK_PLUGIN("steam-engine");
-
-/*
- * List of known steam engine workshop raws.
- */
-
-struct steam_engine_workshop {
- int id;
- df::building_def_workshopst *def;
- // Cached properties
- bool is_magma;
- int max_power, max_capacity;
- int wear_temp;
- // Special tiles (relative position)
- std::vector<df::coord2d> gear_tiles;
- df::coord2d hearth_tile;
- df::coord2d water_tile;
- df::coord2d magma_tile;
-};
-
-std::vector<steam_engine_workshop> engines;
-
-steam_engine_workshop *find_steam_engine(int id)
-{
- for (size_t i = 0; i < engines.size(); i++)
- if (engines[i].id == id)
- return &engines[i];
-
- return NULL;
-}
-
-/*
- * Misc utilities.
- */
-
-static const int hearth_colors[6][2] = {
- { COLOR_BLACK, 1 },
- { COLOR_BROWN, 0 },
- { COLOR_RED, 0 },
- { COLOR_RED, 1 },
- { COLOR_BROWN, 1 },
- { COLOR_GREY, 1 }
-};
-
-void enable_updates_at(df::coord pos, bool flow, bool temp)
-{
- static const int delta[4][2] = { { -1, -1 }, { 1, -1 }, { -1, 1 }, { 1, 1 } };
-
- for (int i = 0; i < 4; i++)
- {
- auto blk = Maps::getTileBlock(pos.x+delta[i][0], pos.y+delta[i][1], pos.z);
- Maps::enableBlockUpdates(blk, flow, temp);
- }
-}
-
-void decrement_flow(df::coord pos, int amount)
-{
- auto pldes = Maps::getTileDesignation(pos);
- if (!pldes) return;
-
- int nsize = std::max(0, int(pldes->bits.flow_size - amount));
- pldes->bits.flow_size = nsize;
- pldes->bits.flow_forbid = (nsize > 3 || pldes->bits.liquid_type == tile_liquid::Magma);
-
- enable_updates_at(pos, true, false);
-}
-
-void make_explosion(df::coord center, int power)
-{
- static const int bias[9] = {
- 60, 30, 60,
- 30, 0, 30,
- 60, 30, 60
- };
-
- int mat_type = builtin_mats::WATER, mat_index = -1;
- int i = 0;
-
- for (int dx = -1; dx <= 1; dx++)
- {
- for (int dy = -1; dy <= 1; dy++)
- {
- int size = power - bias[i++];
- auto pos = center + df::coord(dx,dy,0);
-
- if (size > 0)
- Maps::spawnFlow(pos, flow_type::MaterialDust, mat_type, mat_index, size);
- }
- }
-
- Gui::showAutoAnnouncement(
- announcement_type::CAVE_COLLAPSE, center,
- "A boiler has exploded!", COLOR_RED, true
- );
-}
-
-static const int WEAR_TICKS = 806400;
-
-bool add_wear_nodestroy(df::item_actual *item, int rate)
-{
- if (item->incWearTimer(rate))
- {
- while (item->wear_timer >= WEAR_TICKS)
- {
- item->wear_timer -= WEAR_TICKS;
- item->wear++;
- }
- }
-
- return item->wear > 3;
-}
-
-/*
- * Hook for the liquid item. Implements a special 'boiling'
- * matter state with a modified description and temperature
- * locked at boiling-1.
- */
-
-struct liquid_hook : df::item_liquid_miscst {
- typedef df::item_liquid_miscst interpose_base;
-
- static const uint32_t BOILING_FLAG = 0x80000000U;
-
- DEFINE_VMETHOD_INTERPOSE(void, getItemDescription, (std::string *buf, int8_t mode))
- {
- if (mat_state.whole & BOILING_FLAG)
- buf->append("boiling ");
-
- INTERPOSE_NEXT(getItemDescription)(buf, mode);
- }
-
- DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t unk))
- {
- if (mat_state.whole & BOILING_FLAG)
- temp = std::max(int(temp), getBoilingPoint()-1);
-
- return INTERPOSE_NEXT(adjustTemperature)(temp, unk);
- }
-
- DEFINE_VMETHOD_INTERPOSE(bool, checkTemperatureDamage, ())
- {
- if (mat_state.whole & BOILING_FLAG)
- temperature = std::max(int(temperature), getBoilingPoint()-1);
-
- return INTERPOSE_NEXT(checkTemperatureDamage)();
- }
-};
-
-IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, getItemDescription);
-IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, adjustTemperature);
-IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, checkTemperatureDamage);
-
-/*
- * Hook for the workshop itself. Implements core logic.
- */
-
-struct workshop_hook : df::building_workshopst {
- typedef df::building_workshopst interpose_base;
-
- // Engine detection
-
- steam_engine_workshop *get_steam_engine()
- {
- if (type == workshop_type::Custom)
- return find_steam_engine(custom_type);
-
- return NULL;
- }
-
- inline bool is_fully_built()
- {
- return getBuildStage() >= getMaxBuildStage();
- }
-
- // Use high bits of flags to store current steam amount.
- // This is necessary for consistency if items disappear unexpectedly.
-
- int get_steam_amount()
- {
- return (flags.whole >> 28) & 15;
- }
-
- void set_steam_amount(int count)
- {
- flags.whole = (flags.whole & 0x0FFFFFFFU) | uint32_t((count & 15) << 28);
- }
-
- // Find liquids to consume below the engine.
-
- bool find_liquids(df::coord *pwater, df::coord *pmagma, bool is_magma, int min_level)
- {
- if (!is_magma)
- pmagma = NULL;
-
- for (int x = x1; x <= x2; x++)
- {
- for (int y = y1; y <= y2; y++)
- {
- auto ptile = Maps::getTileType(x,y,z);
- if (!ptile || !LowPassable(*ptile))
- continue;
-
- auto pltile = Maps::getTileType(x,y,z-1);
- if (!pltile || !FlowPassable(*pltile))
- continue;
-
- auto pldes = Maps::getTileDesignation(x,y,z-1);
- if (!pldes || pldes->bits.flow_size < min_level)
- continue;
-
- if (pldes->bits.liquid_type == tile_liquid::Magma)
- {
- if (pmagma)
- *pmagma = df::coord(x,y,z-1);
- if (pwater->isValid())
- return true;
- }
- else
- {
- *pwater = df::coord(x,y,z-1);
- if (!pmagma || pmagma->isValid())
- return true;
- }
- }
- }
-
- return false;
- }
-
- // Absorbs a water item produced by stoke reaction into the engine.
-
- bool absorb_unit(steam_engine_workshop *engine, df::item_liquid_miscst *liquid)
- {
- // Consume liquid inputs
- df::coord water, magma;
-
- if (!find_liquids(&water, &magma, engine->is_magma, 1))
- {
- // Destroy the item with enormous wear amount.
- liquid->addWear(WEAR_TICKS*5, true, false);
- return false;
- }
-
- decrement_flow(water, 1);
- if (engine->is_magma)
- decrement_flow(magma, 1);
-
- // Update flags
- liquid->flags.bits.in_building = true;
- liquid->mat_state.whole |= liquid_hook::BOILING_FLAG;
- liquid->temperature = liquid->getBoilingPoint()-1;
- liquid->temperature_fraction = 0;
-
- // This affects where the steam appears to come from
- if (engine->hearth_tile.isValid())
- liquid->pos = df::coord(x1+engine->hearth_tile.x, y1+engine->hearth_tile.y, z);
-
- // Enable block temperature updates
- enable_updates_at(liquid->pos, false, true);
- return true;
- }
-
- bool boil_unit(df::item_liquid_miscst *liquid)
- {
- liquid->wear = 4;
- liquid->flags.bits.in_building = false;
- liquid->temperature = liquid->getBoilingPoint() + 10;
-
- return liquid->checkMeltBoil();
- }
-
- void suspend_jobs(bool suspend)
- {
- for (size_t i = 0; i < jobs.size(); i++)
- if (jobs[i]->job_type == job_type::CustomReaction)
- jobs[i]->flags.bits.suspend = suspend;
- }
-
- // Scan contained items for boiled steam to absorb.
-
- df::item_liquid_miscst *collect_steam(steam_engine_workshop *engine, int *count)
- {
- df::item_liquid_miscst *first = NULL;
- *count = 0;
-
- for (int i = contained_items.size()-1; i >= 0; i--)
- {
- auto item = contained_items[i];
- if (item->use_mode != 0)
- continue;
-
- auto liquid = strict_virtual_cast<df::item_liquid_miscst>(item->item);
- if (!liquid)
- continue;
-
- if (!liquid->flags.bits.in_building)
- {
- if (liquid->mat_type != builtin_mats::WATER ||
- liquid->age > 1 ||
- liquid->wear != 0)
- continue;
-
- // This may destroy the item
- if (!absorb_unit(engine, liquid))
- continue;
- }
-
- if (*count < engine->max_capacity)
- {
- first = liquid;
- ++*count;
- }
- else
- {
- // Overpressure valve
- boil_unit(liquid);
- suspend_jobs(true);
- }
- }
-
- return first;
- }
-
- void random_boil()
- {
- int cnt = 0;
-
- for (int i = contained_items.size()-1; i >= 0; i--)
- {
- auto item = contained_items[i];
- if (item->use_mode != 0 || !item->item->flags.bits.in_building)
- continue;
-
- auto liquid = strict_virtual_cast<df::item_liquid_miscst>(item->item);
- if (!liquid)
- continue;
-
- if (cnt == 0 || rand() < RAND_MAX/2)
- {
- cnt++;
- boil_unit(liquid);
- }
- }
- }
-
- int classify_component(df::building_actual::T_contained_items *item)
- {
- if (item->use_mode != 2 || item->item->isBuildMat())
- return -1;
-
- switch (item->item->getType())
- {
- case item_type::TRAPPARTS:
- case item_type::CHAIN:
- return 0;
- case item_type::BARREL:
- return 2;
- default:
- return 1;
- }
- }
-
- bool check_component_wear(steam_engine_workshop *engine, int count, int power)
- {
- int coeffs[3] = { 0, power, count };
-
- for (int i = contained_items.size()-1; i >= 0; i--)
- {
- int type = classify_component(contained_items[i]);
- if (type < 0)
- continue;
-
- df::item *item = contained_items[i]->item;
- int melt_temp = item->getMeltingPoint();
- if (coeffs[type] == 0 || melt_temp >= engine->wear_temp)
- continue;
-
- // let 500 degree delta at 4 pressure work 1 season
- float ticks = coeffs[type]*(engine->wear_temp - melt_temp)*3.0f/500.0f/4.0f;
- if (item->addWear(int(8*(1 + ticks)), true, true))
- return true;
- }
-
- return false;
- }
-
- float get_component_quality(int use_type)
- {
- float sum = 0, cnt = 0;
-
- for (size_t i = 0; i < contained_items.size(); i++)
- {
- int type = classify_component(contained_items[i]);
- if (type != use_type)
- continue;
-
- sum += contained_items[i]->item->getQuality();
- cnt += 1;
- }
-
- return (cnt > 0 ? sum/cnt : 0);
- }
-
- int get_steam_use_rate(steam_engine_workshop *engine, int dimension, int power_level)
- {
- // total ticks to wear off completely
- float ticks = WEAR_TICKS * 4.0f;
- // dimension == days it lasts * 100
- ticks /= 1200.0f * dimension / 100.0f;
- // true power use
- float power_rate = 1.0f;
- // check the actual load
- if (auto mptr = df::machine::find(machine.machine_id))
- {
- if (mptr->cur_power >= mptr->min_power)
- power_rate = float(mptr->min_power) / mptr->cur_power;
- else
- power_rate = 0.0f;
- }
- // waste rate: 1-10% depending on piston assembly quality
- float waste = 0.1f - 0.015f * get_component_quality(1);
- // apply rate and waste factor
- ticks *= (waste + 0.9f*power_rate)*power_level;
- // end result
- return std::max(1, int(ticks));
- }
-
- void update_under_construction(steam_engine_workshop *engine)
- {
- if (machine.machine_id != -1)
- return;
-
- int cur_count = 0;
-
- if (auto first = collect_steam(engine, &cur_count))
- {
- if (add_wear_nodestroy(first, WEAR_TICKS*4/10))
- {
- boil_unit(first);
- cur_count--;
- }
- }
-
- set_steam_amount(cur_count);
- }
-
- void update_working(steam_engine_workshop *engine)
- {
- int old_count = get_steam_amount();
- int old_power = std::min(engine->max_power, old_count);
- int cur_count = 0;
-
- if (auto first = collect_steam(engine, &cur_count))
- {
- int rate = get_steam_use_rate(engine, first->dimension, old_power);
-
- if (add_wear_nodestroy(first, rate))
- {
- boil_unit(first);
- cur_count--;
- }
-
- if (check_component_wear(engine, old_count, old_power))
- return;
- }
-
- if (old_count < engine->max_capacity && cur_count == engine->max_capacity)
- suspend_jobs(true);
- else if (cur_count <= engine->max_power+1 && old_count > engine->max_power+1)
- suspend_jobs(false);
-
- set_steam_amount(cur_count);
-
- int cur_power = std::min(engine->max_power, cur_count);
- if (cur_power != old_power)
- {
- auto mptr = df::machine::find(machine.machine_id);
- if (mptr)
- mptr->cur_power += (cur_power - old_power)*100;
- }
- }
-
- // Furnaces need architecture, and this is a workshop
- // only because furnaces cannot connect to machines.
- DEFINE_VMETHOD_INTERPOSE(bool, needsDesign, ())
- {
- if (get_steam_engine())
- return true;
-
- return INTERPOSE_NEXT(needsDesign)();
- }
-
- // Machine interface
- DEFINE_VMETHOD_INTERPOSE(void, getPowerInfo, (df::power_info *info))
- {
- if (auto engine = get_steam_engine())
- {
- info->produced = std::min(engine->max_power, get_steam_amount())*100;
- info->consumed = 10 - int(get_component_quality(0));
- return;
- }
-
- INTERPOSE_NEXT(getPowerInfo)(info);
- }
-
- DEFINE_VMETHOD_INTERPOSE(df::machine_info*, getMachineInfo, ())
- {
- if (get_steam_engine())
- return &machine;
-
- return INTERPOSE_NEXT(getMachineInfo)();
- }
-
- DEFINE_VMETHOD_INTERPOSE(bool, isPowerSource, ())
- {
- if (get_steam_engine())
- return true;
-
- return INTERPOSE_NEXT(isPowerSource)();
- }
-
- DEFINE_VMETHOD_INTERPOSE(void, categorize, (bool free))
- {
- if (get_steam_engine())
- {
- auto &vec = world->buildings.other[buildings_other_id::ANY_MACHINE];
- insert_into_vector(vec, &df::building::id, (df::building*)this);
- }
-
- INTERPOSE_NEXT(categorize)(free);
- }
-
- DEFINE_VMETHOD_INTERPOSE(void, uncategorize, ())
- {
- if (get_steam_engine())
- {
- auto &vec = world->buildings.other[buildings_other_id::ANY_MACHINE];
- erase_from_vector(vec, &df::building::id, id);
- }
-
- INTERPOSE_NEXT(uncategorize)();
- }
-
- DEFINE_VMETHOD_INTERPOSE(bool, canConnectToMachine, (df::machine_tile_set *info))
- {
- if (auto engine = get_steam_engine())
- {
- int real_cx = centerx, real_cy = centery;
- bool ok = false;
-
- for (size_t i = 0; i < engine->gear_tiles.size(); i++)
- {
- // the original function connects to the center tile
- centerx = x1 + engine->gear_tiles[i].x;
- centery = y1 + engine->gear_tiles[i].y;
-
- if (!INTERPOSE_NEXT(canConnectToMachine)(info))
- continue;
-
- ok = true;
- break;
- }
-
- centerx = real_cx; centery = real_cy;
- return ok;
- }
- else
- return INTERPOSE_NEXT(canConnectToMachine)(info);
- }
-
- // Operation logic
- DEFINE_VMETHOD_INTERPOSE(bool, isUnpowered, ())
- {
- if (auto engine = get_steam_engine())
- {
- df::coord water, magma;
- return !find_liquids(&water, &magma, engine->is_magma, 3);
- }
-
- return INTERPOSE_NEXT(isUnpowered)();
- }
-
- DEFINE_VMETHOD_INTERPOSE(void, updateAction, ())
- {
- if (auto engine = get_steam_engine())
- {
- if (is_fully_built())
- update_working(engine);
- else
- update_under_construction(engine);
-
- if (flags.bits.almost_deleted)
- return;
- }
-
- INTERPOSE_NEXT(updateAction)();
- }
-
- DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (df::building_drawbuffer *db, void *unk))
- {
- INTERPOSE_NEXT(drawBuilding)(db, unk);
-
- if (auto engine = get_steam_engine())
- {
- if (!is_fully_built())
- return;
-
- // If machine is running, tweak gear assemblies
- auto mptr = df::machine::find(machine.machine_id);
- if (mptr && (mptr->visual_phase & 1) != 0)
- {
- for (size_t i = 0; i < engine->gear_tiles.size(); i++)
- {
- auto pos = engine->gear_tiles[i];
- db->tile[pos.x][pos.y] = 42;
- }
- }
-
- // Use the hearth color to display power level
- if (engine->hearth_tile.isValid())
- {
- auto pos = engine->hearth_tile;
- int power = std::min(engine->max_power, get_steam_amount());
- db->fore[pos.x][pos.y] = hearth_colors[power][0];
- db->bright[pos.x][pos.y] = hearth_colors[power][1];
- }
-
- // Set liquid indicator state
- if (engine->water_tile.isValid() || engine->magma_tile.isValid())
- {
- df::coord water, magma;
- find_liquids(&water, &magma, engine->is_magma, 3);
- df::coord dwater, dmagma;
- find_liquids(&dwater, &dmagma, engine->is_magma, 5);
-
- if (engine->water_tile.isValid())
- {
- if (!water.isValid())
- db->fore[engine->water_tile.x][engine->water_tile.y] = 0;
- else if (!dwater.isValid())
- db->bright[engine->water_tile.x][engine->water_tile.y] = 0;
- }
- if (engine->magma_tile.isValid() && engine->is_magma)
- {
- if (!magma.isValid())
- db->fore[engine->magma_tile.x][engine->magma_tile.y] = 0;
- else if (!dmagma.isValid())
- db->bright[engine->magma_tile.x][engine->magma_tile.y] = 0;
- }
- }
- }
- }
-
- DEFINE_VMETHOD_INTERPOSE(void, deconstructItems, (bool noscatter, bool lost))
- {
- if (get_steam_engine())
- {
- // Explode if any steam left
- if (int amount = get_steam_amount())
- {
- make_explosion(
- df::coord((x1+x2)/2, (y1+y2)/2, z),
- 40 + amount * 20
- );
-
- random_boil();
- }
- }
-
- INTERPOSE_NEXT(deconstructItems)(noscatter, lost);
- }
-};
-
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, needsDesign);
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, getPowerInfo);
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, getMachineInfo);
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, isPowerSource);
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, categorize);
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, uncategorize);
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, canConnectToMachine);
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, isUnpowered);
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, updateAction);
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, drawBuilding);
-IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, deconstructItems);
-
-/*
- * Hook for the dwarfmode screen. Tweaks the build menu
- * behavior to suit the steam engine building more.
- */
-
-struct dwarfmode_hook : df::viewscreen_dwarfmodest
-{
- typedef df::viewscreen_dwarfmodest interpose_base;
-
- steam_engine_workshop *get_steam_engine()
- {
- if (ui->main.mode == ui_sidebar_mode::Build &&
- ui_build_selector->stage == 1 &&
- ui_build_selector->building_type == building_type::Workshop &&
- ui_build_selector->building_subtype == workshop_type::Custom)
- {
- return find_steam_engine(ui_build_selector->custom_type);
- }
-
- return NULL;
- }
-
- void check_hanging_tiles(steam_engine_workshop *engine)
- {
- using df::global::cursor;
-
- if (!engine) return;
-
- bool error = false;
-
- int x1 = cursor->x - engine->def->workloc_x;
- int y1 = cursor->y - engine->def->workloc_y;
-
- for (int x = 0; x < engine->def->dim_x; x++)
- {
- for (int y = 0; y < engine->def->dim_y; y++)
- {
- if (ui_build_selector->tiles[x][y] >= 5)
- continue;
-
- auto ptile = Maps::getTileType(x1+x,y1+y,cursor->z);
- if (ptile && !isOpenTerrain(*ptile))
- continue;
-
- ui_build_selector->tiles[x][y] = 6;
- error = true;
- }
- }
-
- if (error)
- {
- const char *msg = "Hanging - cover channels with down stairs.";
- ui_build_selector->errors.push_back(new std::string(msg));
- }
- }
-
- DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
- {
- steam_engine_workshop *engine = get_steam_engine();
-
- // Selector insists that workshops cannot be placed hanging
- // unless they require magma, so pretend we always do.
- if (engine)
- engine->def->needs_magma = true;
-
- INTERPOSE_NEXT(feed)(input);
-
- // Restore the flag
- if (engine)
- engine->def->needs_magma = engine->is_magma;
-
- // And now, check for open space. Since these workshops
- // are machines, they will collapse over true open space.
- check_hanging_tiles(get_steam_engine());
- }
-};
-
-IMPLEMENT_VMETHOD_INTERPOSE(dwarfmode_hook, feed);
-
-/*
- * Scan raws for matching workshop buildings.
- */
-
-static bool find_engines()
-{
- engines.clear();
-
- auto &wslist = world->raws.buildings.workshops;
-
- for (size_t i = 0; i < wslist.size(); i++)
- {
- if (strstr(wslist[i]->code.c_str(), "STEAM_ENGINE") == NULL)
- continue;
-
- steam_engine_workshop ws;
- ws.def = wslist[i];
- ws.id = ws.def->id;
-
- int bs = ws.def->build_stages;
- for (int x = 0; x < ws.def->dim_x; x++)
- {
- for (int y = 0; y < ws.def->dim_y; y++)
- {
- switch (ws.def->tile[bs][x][y])
- {
- case 15:
- ws.gear_tiles.push_back(df::coord2d(x,y));
- break;
- case 19:
- ws.hearth_tile = df::coord2d(x,y);
- break;
- }
-
- if (ws.def->tile_color[2][bs][x][y])
- {
- switch (ws.def->tile_color[0][bs][x][y])
- {
- case 1:
- ws.water_tile = df::coord2d(x,y);
- break;
- case 4:
- ws.magma_tile = df::coord2d(x,y);
- break;
- }
- }
- }
- }
-
- ws.is_magma = ws.def->needs_magma;
- ws.max_power = ws.is_magma ? 5 : 3;
- ws.max_capacity = ws.is_magma ? 10 : 6;
- ws.wear_temp = ws.is_magma ? 12000 : 11000;
-
- if (!ws.gear_tiles.empty())
- engines.push_back(ws);
- }
-
- return !engines.empty();
-}
-
-static void enable_hooks(bool enable)
-{
- INTERPOSE_HOOK(liquid_hook, getItemDescription).apply(enable);
- INTERPOSE_HOOK(liquid_hook, adjustTemperature).apply(enable);
- INTERPOSE_HOOK(liquid_hook, checkTemperatureDamage).apply(enable);
-
- INTERPOSE_HOOK(workshop_hook, needsDesign).apply(enable);
- INTERPOSE_HOOK(workshop_hook, getPowerInfo).apply(enable);
- INTERPOSE_HOOK(workshop_hook, getMachineInfo).apply(enable);
- INTERPOSE_HOOK(workshop_hook, isPowerSource).apply(enable);
- INTERPOSE_HOOK(workshop_hook, categorize).apply(enable);
- INTERPOSE_HOOK(workshop_hook, uncategorize).apply(enable);
- INTERPOSE_HOOK(workshop_hook, canConnectToMachine).apply(enable);
- INTERPOSE_HOOK(workshop_hook, isUnpowered).apply(enable);
- INTERPOSE_HOOK(workshop_hook, updateAction).apply(enable);
- INTERPOSE_HOOK(workshop_hook, drawBuilding).apply(enable);
- INTERPOSE_HOOK(workshop_hook, deconstructItems).apply(enable);
-
- INTERPOSE_HOOK(dwarfmode_hook, feed).apply(enable);
-}
-
-DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
-{
- switch (event) {
- case SC_MAP_LOADED:
- if (find_engines())
- {
- out.print("Detected steam engine workshops - enabling plugin.\n");
- enable_hooks(true);
- }
- else
- enable_hooks(false);
- break;
- case SC_MAP_UNLOADED:
- enable_hooks(false);
- engines.clear();
- 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;
-}