summaryrefslogtreecommitdiff
path: root/plugins/devel
diff options
context:
space:
mode:
authorKelly Martin2012-09-04 12:05:05 -0500
committerKelly Martin2012-09-04 12:05:05 -0500
commit96fec768c73aee3fca8937017ec8ef59cf0d7adb (patch)
tree09e821999fd94b96918fdf2a7cb2e885d03f4e8a /plugins/devel
parenta8158cb19ac25ec69db4cbb09c1eaf853a559e9a (diff)
parentaa449a2180144961308e1e357c013724921175b3 (diff)
downloaddfhack-96fec768c73aee3fca8937017ec8ef59cf0d7adb.tar.gz
dfhack-96fec768c73aee3fca8937017ec8ef59cf0d7adb.tar.bz2
dfhack-96fec768c73aee3fca8937017ec8ef59cf0d7adb.tar.xz
Merge remote-tracking branch 'angavrilov/master'
Diffstat (limited to 'plugins/devel')
-rw-r--r--plugins/devel/CMakeLists.txt1
-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/steam-engine.cpp929
5 files changed, 1048 insertions, 0 deletions
diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt
index 134d5cb6..f126ae53 100644
--- a/plugins/devel/CMakeLists.txt
+++ b/plugins/devel/CMakeLists.txt
@@ -18,6 +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)
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
new file mode 100644
index 00000000..48657b0c
--- /dev/null
+++ b/plugins/devel/building_steam_engine.txt
@@ -0,0 +1,92 @@
+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
new file mode 100644
index 00000000..bae6f5b2
--- /dev/null
+++ b/plugins/devel/item_trapcomp_steam_engine.txt
@@ -0,0 +1,12 @@
+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
new file mode 100644
index 00000000..175ffdd5
--- /dev/null
+++ b/plugins/devel/reaction_steam_engine.txt
@@ -0,0 +1,14 @@
+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/steam-engine.cpp b/plugins/devel/steam-engine.cpp
new file mode 100644
index 00000000..5c344f78
--- /dev/null
+++ b/plugins/devel/steam-engine.cpp
@@ -0,0 +1,929 @@
+#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;
+}