-
+
 Any files with the .lua extension placed into hack/scripts/*
 are automatically used by the DFHack core as commands. The
 matching command name consists of the name of the file sans
diff --git a/NEWS b/NEWS
new file mode 100644
index 000000000..43707f9a7
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,61 @@
+DFHack v0.34.11-r2 (UNRELEASED)
+
+  Internals:
+    - full support for Mac OS X.
+    - a plugin that adds scripting in ruby.
+    - support for interposing virtual methods in DF from C++ plugins.
+    - support for creating new interface screens from C++ and lua.
+    - added various other API functions.
+  Notable bugfixes:
+    - better terminal reset after exit on linux.
+    - seedwatch now works on reclaim.
+    - the sort plugin won't crash on cages anymore.
+  Misc improvements:
+    - autodump: can move items to any walkable tile, not just floors.
+    - stripcaged: by default keep armor, new dumparmor option.
+    - zone: allow non-domesticated birds in nestboxes.
+    - workflow: quality range in constraints.
+    - cleanplants: new command to remove rain water from plants.
+    - liquids: can paint permaflow, i.e. what makes rivers power water wheels.
+    - prospect: pre-embark prospector accounts for caves & magma sea in its estimate.
+    - rename: supports renaming stockpiles, workshops, traps, siege engines.
+  New tweaks:
+    - tweak stable-cursor: keeps exact cursor position between d/k/t/q/v etc menus.
+    - tweak patrol-duty: makes Train orders reduce patrol timer, like the binary patch does.
+    - tweak readable-build-plate: fix unreadable truncation in unit pressure plate build ui.
+    - tweak stable-temp: fixes bug 6012; may improve FPS by 50-100% on a slow item-heavy fort.
+    - tweak fast-heat: speeds up item heating & cooling, thus making stable-temp act faster.
+  New scripts:
+    - fixnaked: removes thoughts about nakedness.
+    - setfps: set FPS cap at runtime, in case you want slow motion or speed-up.
+    - fix/population-cap: run after every migrant wave to prevent exceeding the cap.
+    - fix/stable-temp: counts items with temperature updates; does instant one-shot stable-temp.
+  New GUI scripts:
+    - gui/mechanisms: browse mechanism links of the current building.
+    - gui/room-list: browse other rooms owned by the unit when assigning one.
+    - gui/liquids: a GUI front-end for the liquids plugin.
+    - gui/rename: renaming stockpiles, workshops and units via an in-game dialog.
+    - gui/power-meter: front-end for the Power Meter plugin.
+    - gui/siege-engine: front-end for the Siege Engine plugin.
+  Autolabor plugin:
+    - can set nonidle hauler percentage.
+    - broker excluded from all labors when needed at depot.
+    - likewise, anybody with a scheduled diplomat meeting.
+  New Dwarf Manipulator plugin:
+    Open the unit list, and press 'l' to access a Dwarf Therapist like UI in the game.
+  New Steam Engine plugin:
+    Dwarven Water Reactors don't make any sense whatsoever, so this is a potential
+    replacement for those concerned by it. The plugin detects if a workshop with a
+    certain name is in the raws used by the current world, and provides the necessary
+    behavior. See hack/raw/*_steam_engine.txt for the necessary raw definitions.
+    Note: Stuff like animal treadmills might be more period, but can't be done with dfhack.
+  New Power Meter plugin:
+    When activated, implements a pressure plate modification that detects power in gear
+    boxes built on the four adjacent N/S/W/E tiles. The gui/power-meter script implements
+    the build configuration UI.
+  New Siege Engine plugin (INCOMPLETE):
+    When enabled and configured via gui/siege-engine, allows aiming siege engines
+    at a designated rectangular area across Z levels. Also supports loading catapults
+    with non-boulder projectiles, taking from a stockpile, and restricting operator
+    skill range, like with ordinary workshops.
+
diff --git a/depends/protobuf/CMakeLists.txt b/depends/protobuf/CMakeLists.txt
index 24c4b275a..5034f00f4 100644
--- a/depends/protobuf/CMakeLists.txt
+++ b/depends/protobuf/CMakeLists.txt
@@ -7,10 +7,10 @@ IF(CMAKE_COMPILER_IS_GNUCC)
     STRING(REGEX MATCHALL "[0-9]+" GCC_VERSION_COMPONENTS ${GCC_VERSION})
     LIST(GET GCC_VERSION_COMPONENTS 0 GCC_MAJOR)
     LIST(GET GCC_VERSION_COMPONENTS 1 GCC_MINOR)
-    IF(GCC_MAJOR LESS 4 OR (GCC_MAJOR EQUAL 4 AND GCC_MINOR LESS 2))
+    #IF(GCC_MAJOR LESS 4 OR (GCC_MAJOR EQUAL 4 AND GCC_MINOR LESS 2))
         #GCC is too old
-        SET(STL_HASH_OLD_GCC 1)
-    ENDIF()
+    #    SET(STL_HASH_OLD_GCC 1)
+    #ENDIF()
 
     #SET(CMAKE_CXX_FLAGS "-std=c++0x")
     SET(HAVE_HASH_MAP 0)
diff --git a/dfhack.init-example b/dfhack.init-example
index d3a28b9b0..a9b69b826 100644
--- a/dfhack.init-example
+++ b/dfhack.init-example
@@ -16,6 +16,10 @@ keybinding add Ctrl-K autodump-destroy-item
 # quicksave, only in main dwarfmode screen and menu page
 keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
 
+# gui/rename script
+keybinding add Ctrl-Shift-N gui/rename
+keybinding add Ctrl-Shift-P "gui/rename unit-profession"
+
 ##############################
 # Generic adv mode bindings  #
 ##############################
@@ -47,17 +51,31 @@ keybinding add Shift-G "job-material GLASS_GREEN"
 keybinding add Ctrl-M@dwarfmode/QueryBuilding/Some gui/mechanisms
 
 # browse rooms of same owner
-keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list.work
+keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list
 
 # interface for the liquids plugin
 keybinding add Alt-L@dwarfmode/LookAround gui/liquids
 
-###################
-# UI logic tweaks #
-###################
+# machine power sensitive pressure plate construction
+keybinding add Ctrl-Shift-M@dwarfmode/Build/Position/Trap gui/power-meter
+
+############################
+# UI and game logic tweaks #
+############################
 
 # stabilize the cursor of dwarfmode when switching menus
 tweak stable-cursor
 
 # stop military from considering training as 'patrol duty'
 tweak patrol-duty
+
+# display creature weight in build plate menu as ??K, instead of (???df: Max
+tweak readable-build-plate
+
+# improve FPS by squashing endless item temperature update loops
+tweak stable-temp
+
+# speed up items reaching temp equilibrium with environment by
+# capping the rate to no less than 1 degree change per 500 frames
+# Note: will also cause stuff to melt faster in magma etc
+tweak fast-heat 500
diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt
index 109a97e7c..536f4d34d 100644
--- a/library/CMakeLists.txt
+++ b/library/CMakeLists.txt
@@ -286,6 +286,10 @@ SET_TARGET_PROPERTIES(dfhack PROPERTIES LINK_INTERFACE_LIBRARIES "")
 TARGET_LINK_LIBRARIES(dfhack-client protobuf-lite clsocket)
 TARGET_LINK_LIBRARIES(dfhack-run dfhack-client)
 
+if(APPLE)
+	add_custom_command(TARGET dfhack-run COMMAND ${dfhack_SOURCE_DIR}/package/darwin/fix-libs.sh WORKING_DIRECTORY ../ COMMENT "Fixing library dependencies...")
+endif()
+
 IF(UNIX)
 	if (APPLE)
 		install(PROGRAMS ${dfhack_SOURCE_DIR}/package/darwin/dfhack
diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp
index 341164441..fa2aacf78 100644
--- a/library/DataDefs.cpp
+++ b/library/DataDefs.cpp
@@ -374,7 +374,7 @@ void DFHack::bitfieldToString(std::vector *pvec, const void *p,
                               unsigned size, const bitfield_item_info *items)
 {
     for (unsigned i = 0; i < size; i++) {
-        int value = getBitfieldField(p, i, std::min(1,items[i].size));
+        int value = getBitfieldField(p, i, std::max(1,items[i].size));
 
         if (value) {
             std::string name = format_key(items[i].name, i);
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 6dfb2f354..f69fa7a1b 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -78,6 +78,9 @@ distribution.
 #include "df/burrow.h"
 #include "df/building_civzonest.h"
 #include "df/region_map_entry.h"
+#include "df/flow_info.h"
+#include "df/unit_misc_trait.h"
+#include "df/proj_itemst.h"
 
 #include 
 #include 
@@ -727,6 +730,7 @@ static std::string getOSType()
 }
 
 static std::string getDFVersion() { return Core::getInstance().vinfo->getVersion(); }
+static uint32_t getTickCount() { return Core::getInstance().p->getTickCount(); }
 
 static std::string getDFPath() { return Core::getInstance().p->getPath(); }
 static std::string getHackPath() { return Core::getInstance().getHackPath(); }
@@ -738,6 +742,7 @@ static const LuaWrapper::FunctionReg dfhack_module[] = {
     WRAP(getOSType),
     WRAP(getDFVersion),
     WRAP(getDFPath),
+    WRAP(getTickCount),
     WRAP(getHackPath),
     WRAP(isWorldLoaded),
     WRAP(isMapLoaded),
@@ -756,7 +761,9 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
     WRAPM(Gui, getSelectedUnit),
     WRAPM(Gui, getSelectedItem),
     WRAPM(Gui, showAnnouncement),
+    WRAPM(Gui, showZoomAnnouncement),
     WRAPM(Gui, showPopupAnnouncement),
+    WRAPM(Gui, showAutoAnnouncement),
     { NULL, NULL }
 };
 
@@ -808,12 +815,20 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
     WRAPM(Units, getVisibleName),
     WRAPM(Units, getIdentity),
     WRAPM(Units, getNemesis),
+    WRAPM(Units, isCrazed),
+    WRAPM(Units, isOpposedToLife),
+    WRAPM(Units, hasExtravision),
+    WRAPM(Units, isBloodsucker),
+    WRAPM(Units, isMischievous),
+    WRAPM(Units, getMiscTrait),
     WRAPM(Units, isDead),
     WRAPM(Units, isAlive),
     WRAPM(Units, isSane),
     WRAPM(Units, isDwarf),
     WRAPM(Units, isCitizen),
     WRAPM(Units, getAge),
+    WRAPM(Units, getEffectiveSkill),
+    WRAPM(Units, computeMovementSpeed),
     WRAPM(Units, getProfessionName),
     WRAPM(Units, getCasteProfessionName),
     WRAPM(Units, getProfessionColor),
@@ -871,6 +886,12 @@ static bool items_moveToInventory
     return Items::moveToInventory(mc, item, unit, mode, body_part);
 }
 
+static df::proj_itemst *items_makeProjectile(df::item *item)
+{
+    MapExtras::MapCache mc;
+    return Items::makeProjectile(mc, item);
+}
+
 static const LuaWrapper::FunctionReg dfhack_items_module[] = {
     WRAPM(Items, getGeneralRef),
     WRAPM(Items, getSpecificRef),
@@ -882,6 +903,7 @@ static const LuaWrapper::FunctionReg dfhack_items_module[] = {
     WRAPN(moveToContainer, items_moveToContainer),
     WRAPN(moveToBuilding, items_moveToBuilding),
     WRAPN(moveToInventory, items_moveToInventory),
+    WRAPN(makeProjectile, items_makeProjectile),
     { NULL, NULL }
 };
 
@@ -912,9 +934,17 @@ static const LuaWrapper::FunctionReg dfhack_maps_module[] = {
     WRAPM(Maps, getGlobalInitFeature),
     WRAPM(Maps, getLocalInitFeature),
     WRAPM(Maps, canWalkBetween),
+    WRAPM(Maps, spawnFlow),
     { NULL, NULL }
 };
 
+static int maps_isValidTilePos(lua_State *L)
+{
+    auto pos = CheckCoordXYZ(L, 1, true);
+    lua_pushboolean(L, Maps::isValidTilePos(pos));
+    return 1;
+}
+
 static int maps_getTileBlock(lua_State *L)
 {
     auto pos = CheckCoordXYZ(L, 1, true);
@@ -922,6 +952,13 @@ static int maps_getTileBlock(lua_State *L)
     return 1;
 }
 
+static int maps_ensureTileBlock(lua_State *L)
+{
+    auto pos = CheckCoordXYZ(L, 1, true);
+    Lua::PushDFObject(L, Maps::ensureTileBlock(pos));
+    return 1;
+}
+
 static int maps_getRegionBiome(lua_State *L)
 {
     auto pos = CheckCoordXY(L, 1, true);
@@ -936,7 +973,9 @@ static int maps_getTileBiomeRgn(lua_State *L)
 }
 
 static const luaL_Reg dfhack_maps_funcs[] = {
+    { "isValidTilePos", maps_isValidTilePos },
     { "getTileBlock", maps_getTileBlock },
+    { "ensureTileBlock", maps_ensureTileBlock },
     { "getRegionBiome", maps_getRegionBiome },
     { "getTileBiomeRgn", maps_getTileBiomeRgn },
     { NULL, NULL }
@@ -1132,6 +1171,45 @@ static int screen_paintTile(lua_State *L)
     return 1;
 }
 
+static int screen_readTile(lua_State *L)
+{
+    int x = luaL_checkint(L, 1);
+    int y = luaL_checkint(L, 2);
+    Pen pen = Screen::readTile(x, y);
+
+    if (!pen.valid())
+    {
+        lua_pushnil(L);
+    }
+    else
+    {
+        lua_newtable(L);
+        lua_pushinteger(L, pen.ch); lua_setfield(L, -2, "ch");
+        lua_pushinteger(L, pen.fg); lua_setfield(L, -2, "fg");
+        lua_pushinteger(L, pen.bg); lua_setfield(L, -2, "bg");
+        lua_pushboolean(L, pen.bold); lua_setfield(L, -2, "bold");
+
+        if (pen.tile)
+        {
+            lua_pushinteger(L, pen.tile); lua_setfield(L, -2, "tile");
+
+            switch (pen.tile_mode) {
+                case Pen::CharColor:
+                    lua_pushboolean(L, true); lua_setfield(L, -2, "tile_color");
+                    break;
+                case Pen::TileColor:
+                    lua_pushinteger(L, pen.tile_fg); lua_setfield(L, -2, "tile_fg");
+                    lua_pushinteger(L, pen.tile_bg); lua_setfield(L, -2, "tile_bg");
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    return 1;
+}
+
 static int screen_paintString(lua_State *L)
 {
     Pen pen;
@@ -1236,6 +1314,7 @@ static const luaL_Reg dfhack_screen_funcs[] = {
     { "getMousePos", screen_getMousePos },
     { "getWindowSize", screen_getWindowSize },
     { "paintTile", screen_paintTile },
+    { "readTile", screen_readTile },
     { "paintString", screen_paintString },
     { "fillRect", screen_fillRect },
     { "findGraphicsTile", screen_findGraphicsTile },
diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp
index 7c2c8f8d6..a283d215c 100644
--- a/library/LuaTools.cpp
+++ b/library/LuaTools.cpp
@@ -1211,6 +1211,39 @@ static int dfhack_open_plugin(lua_State *L)
     return 0;
 }
 
+static int dfhack_curry_wrap(lua_State *L)
+{
+    int nargs = lua_gettop(L);
+    int ncurry = lua_tointeger(L, lua_upvalueindex(1));
+    int scount = nargs + ncurry;
+
+    luaL_checkstack(L, ncurry, "stack overflow in curry");
+
+    // Insert values in O(N+M) by first shifting the existing data
+    lua_settop(L, scount);
+    for (int i = 0; i < nargs; i++)
+        lua_copy(L, nargs-i, scount-i);
+    for (int i = 1; i <= ncurry; i++)
+        lua_copy(L, lua_upvalueindex(i+1), i);
+
+    lua_callk(L, scount-1, LUA_MULTRET, 0, lua_gettop);
+
+    return lua_gettop(L);
+}
+
+static int dfhack_curry(lua_State *L)
+{
+    luaL_checkany(L, 1);
+    if (lua_isnil(L, 1))
+        luaL_argerror(L, 1, "nil function in curry");
+    if (lua_gettop(L) == 1)
+        return 1;
+    lua_pushinteger(L, lua_gettop(L));
+    lua_insert(L, 1);
+    lua_pushcclosure(L, dfhack_curry_wrap, lua_gettop(L));
+    return 1;
+}
+
 bool Lua::IsCoreContext(lua_State *state)
 {
     // This uses a private field of the lua state to
@@ -1234,6 +1267,7 @@ static const luaL_Reg dfhack_funcs[] = {
     { "call_with_finalizer", dfhack_call_with_finalizer },
     { "with_suspend", lua_dfhack_with_suspend },
     { "open_plugin", dfhack_open_plugin },
+    { "curry", dfhack_curry },
     { NULL, NULL }
 };
 
@@ -1546,6 +1580,9 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state)
     lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_BASE_G_TOKEN);
     lua_setfield(state, -2, "BASE_G");
 
+    lua_pushstring(state, DFHACK_VERSION);
+    lua_setfield(state, -2, "VERSION");
+
     lua_pushboolean(state, IsCoreContext(state));
     lua_setfield(state, -2, "is_core_context");
 
diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp
index e71977960..9f2689fa9 100644
--- a/library/LuaTypes.cpp
+++ b/library/LuaTypes.cpp
@@ -894,7 +894,7 @@ static int meta_bitfield_len(lua_State *state)
 
 static void read_bitfield(lua_State *state, uint8_t *ptr, bitfield_identity *id, int idx)
 {
-    int size = id->getBits()[idx].size;
+    int size = std::max(1, id->getBits()[idx].size);
 
     int value = getBitfieldField(ptr, idx, size);
     if (size <= 1)
@@ -951,7 +951,7 @@ static int meta_bitfield_newindex(lua_State *state)
     }
 
     int idx = check_container_index(state, id->getNumBits(), 2, iidx, "write");
-    int size = id->getBits()[idx].size;
+    int size = std::max(1, id->getBits()[idx].size);
 
     if (lua_isboolean(state, 3) || lua_isnil(state, 3))
         setBitfieldField(ptr, idx, size, lua_toboolean(state, 3));
diff --git a/library/Process-darwin.cpp b/library/Process-darwin.cpp
index 5a97d9e00..3893cfc5f 100644
--- a/library/Process-darwin.cpp
+++ b/library/Process-darwin.cpp
@@ -27,6 +27,7 @@ distribution.
 #include 
 #include 
 #include 
+#include 
 
 #include 
 
@@ -262,6 +263,13 @@ bool Process::getThreadIDs(vector & threads )
     return true;
 }
 
+uint32_t Process::getTickCount()
+{
+    struct timeval tp;
+    gettimeofday(&tp, NULL);
+    return (tp.tv_sec * 1000) + (tp.tv_usec / 1000);
+}
+
 string Process::getPath()
 {
 	char path[1024];
diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp
index fe8647845..1fecbab78 100644
--- a/library/Process-linux.cpp
+++ b/library/Process-linux.cpp
@@ -27,6 +27,7 @@ distribution.
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -126,6 +127,9 @@ void Process::getMemRanges( vector & ranges )
     char permissions[5]; // r/-, w/-, x/-, p/s, 0
 
     FILE *mapFile = ::fopen("/proc/self/maps", "r");
+    if (!mapFile)
+        return;
+
     size_t start, end, offset, device1, device2, node;
 
     while (fgets(buffer, 1024, mapFile))
@@ -147,6 +151,8 @@ void Process::getMemRanges( vector & ranges )
         temp.valid = true;
         ranges.push_back(temp);
     }
+
+    fclose(mapFile);
 }
 
 uint32_t Process::getBase()
@@ -192,6 +198,13 @@ bool Process::getThreadIDs(vector & threads )
     return true;
 }
 
+uint32_t Process::getTickCount()
+{
+    struct timeval tp;
+    gettimeofday(&tp, NULL);
+    return (tp.tv_sec * 1000) + (tp.tv_usec / 1000);
+}
+
 string Process::getPath()
 {
     const char * cwd_name = "/proc/self/cwd";
diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp
index 7eb6ff5f7..db58c4d33 100644
--- a/library/Process-windows.cpp
+++ b/library/Process-windows.cpp
@@ -410,6 +410,11 @@ string Process::doReadClassName (void * vptr)
     return raw;
 }
 
+uint32_t Process::getTickCount()
+{
+    return GetTickCount();
+}
+
 string Process::getPath()
 {
     HMODULE hmod;
diff --git a/library/RemoteClient.cpp b/library/RemoteClient.cpp
index 4d30988c6..09861ad5f 100644
--- a/library/RemoteClient.cpp
+++ b/library/RemoteClient.cpp
@@ -394,7 +394,7 @@ command_result RemoteFunctionBase::execute(color_ostream &out,
 
         //out.print("Received %d:%d\n", header.id, header.size);
 
-        if (header.id == RPC_REPLY_FAIL)
+        if ((DFHack::DFHackReplyCode)header.id == RPC_REPLY_FAIL)
             return header.size == CR_OK ? CR_FAILURE : command_result(header.size);
 
         if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE)
diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp
index 53428f2bd..06a9f859c 100644
--- a/library/RemoteServer.cpp
+++ b/library/RemoteServer.cpp
@@ -250,7 +250,7 @@ void ServerConnection::threadFn()
             break;
         }
 
-        if (header.id == RPC_REQUEST_QUIT)
+        if ((DFHack::DFHackReplyCode)header.id == RPC_REQUEST_QUIT)
             break;
 
         if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE)
diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp
index 95c495e93..b371d60fa 100644
--- a/library/RemoteTools.cpp
+++ b/library/RemoteTools.cpp
@@ -287,7 +287,7 @@ void DFHack::describeUnit(BasicUnitInfo *info, df::unit *unit,
 
     if (mask && mask->profession())
     {
-        if (unit->profession >= 0)
+        if (unit->profession >= (df::profession)0)
             info->set_profession(unit->profession);
         if (!unit->custom_profession.empty())
             info->set_custom_profession(unit->custom_profession);
diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp
index 079890fe4..583ef5184 100644
--- a/library/VTableInterpose.cpp
+++ b/library/VTableInterpose.cpp
@@ -335,8 +335,14 @@ void VMethodInterposeLinkBase::on_host_delete(virtual_identity *from)
     }
 }
 
-bool VMethodInterposeLinkBase::apply()
+bool VMethodInterposeLinkBase::apply(bool enable)
 {
+    if (!enable)
+    {
+        remove();
+        return true;
+    }
+
     if (is_applied())
         return true;
     if (!host->vtable_ptr)
diff --git a/library/include/BitArray.h b/library/include/BitArray.h
index fd9bd98fc..ff68ea1d1 100644
--- a/library/include/BitArray.h
+++ b/library/include/BitArray.h
@@ -64,7 +64,7 @@ namespace DFHack
             if (newsize == size)
                 return;
             uint8_t* mem = (uint8_t *) realloc(bits, newsize);
-            if(!mem)
+            if(!mem && newsize != 0)
                 throw std::bad_alloc();
             bits = mem;
             if (newsize > size)
@@ -207,7 +207,7 @@ namespace DFHack
             else
             {
                 T* mem = (T*) realloc(m_data, sizeof(T)*new_size);
-                if(!mem)
+                if(!mem && new_size != 0)
                     throw std::bad_alloc();
                 m_data = mem;
             }
diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h
index 591a0c3ff..61d5dec41 100644
--- a/library/include/DataDefs.h
+++ b/library/include/DataDefs.h
@@ -518,7 +518,7 @@ namespace DFHack {
     template
     inline const char *enum_item_raw_key(T val) {
         typedef df::enum_traits traits;
-        return traits::is_valid(val) ? traits::key_table[val - traits::first_item_value] : NULL;
+        return traits::is_valid(val) ? traits::key_table[(short)val - traits::first_item_value] : NULL;
     }
 
     /**
diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h
index 0f5fd9e7c..21dc68d1a 100644
--- a/library/include/DataIdentity.h
+++ b/library/include/DataIdentity.h
@@ -390,7 +390,7 @@ namespace df
         }
 
         virtual bool resize(void *ptr, int size) {
-            ((container*)ptr)->resize(size);
+            ((container*)ptr)->resize(size*8);
             return true;
         }
 
diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h
index 6b1afb88b..3330e23e7 100644
--- a/library/include/LuaTools.h
+++ b/library/include/LuaTools.h
@@ -287,6 +287,11 @@ namespace DFHack {namespace Lua {
         PushDFObject(state, ptr);
     }
 
+    template inline void SetField(lua_State *L, T val, int idx, const char *name) {
+        if (idx < 0) idx = lua_absindex(L, idx);
+        Push(L, val); lua_setfield(L, idx, name);
+    }
+
     template
     void PushVector(lua_State *state, const T &pvec, bool addn = false)
     {
diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h
index 0e5f618e2..a226018a6 100644
--- a/library/include/MemAccess.h
+++ b/library/include/MemAccess.h
@@ -281,6 +281,9 @@ namespace DFHack
             /// get the DF Process FilePath
             std::string getPath();
 
+            /// millisecond tick count, exactly as DF uses
+            uint32_t getTickCount();
+
             /// modify permisions of memory range
             bool setPermisions(const t_memrange & range,const t_memrange &trgrange);
 
diff --git a/library/include/RemoteTools.h b/library/include/RemoteTools.h
index 65884badc..e87e8026b 100644
--- a/library/include/RemoteTools.h
+++ b/library/include/RemoteTools.h
@@ -88,7 +88,7 @@ namespace DFHack
     {
         typedef df::enum_traits traits;
         int base = traits::first_item;
-        int size = traits::last_item - base + 1;
+        int size = (int)traits::last_item - base + 1;
         describeEnum(pf, base, size, traits::key_table);
     }
 
diff --git a/library/include/VTableInterpose.h b/library/include/VTableInterpose.h
index c9482f82c..7ba6b67aa 100644
--- a/library/include/VTableInterpose.h
+++ b/library/include/VTableInterpose.h
@@ -159,7 +159,7 @@ namespace DFHack
         ~VMethodInterposeLinkBase();
 
         bool is_applied() { return applied; }
-        bool apply();
+        bool apply(bool enable = true);
         void remove();
     };
 
diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h
index 58f222419..97e8bd422 100644
--- a/library/include/modules/Gui.h
+++ b/library/include/modules/Gui.h
@@ -32,6 +32,7 @@ distribution.
 #include "DataDefs.h"
 #include "df/init.h"
 #include "df/ui.h"
+#include "df/announcement_type.h"
 
 namespace df {
     struct viewscreen;
@@ -92,14 +93,32 @@ namespace DFHack
 
         // Show a plain announcement, or a titan-style popup message
         DFHACK_EXPORT void showAnnouncement(std::string message, int color = 7, bool bright = true);
+        DFHACK_EXPORT void showZoomAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true);
         DFHACK_EXPORT void showPopupAnnouncement(std::string message, int color = 7, bool bright = true);
 
+        // Show an announcement with effects determined by announcements.txt
+        DFHACK_EXPORT void showAutoAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true);
+
         /*
          * Cursor and window coords
          */
         DFHACK_EXPORT df::coord getViewportPos();
         DFHACK_EXPORT df::coord getCursorPos();
 
+        static const int AREA_MAP_WIDTH = 23;
+        static const int MENU_WIDTH = 30;
+
+        struct DwarfmodeDims {
+            int map_x1, map_x2, menu_x1, menu_x2, area_x1, area_x2;
+            int y1, y2;
+            bool menu_on, area_on, menu_forced;
+        };
+
+        DFHACK_EXPORT DwarfmodeDims getDwarfmodeViewDims();
+
+        DFHACK_EXPORT void resetDwarfmodeView(bool pause = false);
+        DFHACK_EXPORT bool revealInDwarfmodeMap(df::coord pos, bool center = false);
+
         DFHACK_EXPORT bool getViewCoords (int32_t &x, int32_t &y, int32_t &z);
         DFHACK_EXPORT bool setViewCoords (const int32_t x, const int32_t y, const int32_t z);
 
diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h
index 4236f068a..7493d22fc 100644
--- a/library/include/modules/Items.h
+++ b/library/include/modules/Items.h
@@ -44,6 +44,7 @@ distribution.
 namespace df
 {
     struct itemdef;
+    struct proj_itemst;
 }
 
 namespace MapExtras {
@@ -155,5 +156,8 @@ DFHACK_EXPORT bool moveToContainer(MapExtras::MapCache &mc, df::item *item, df::
 DFHACK_EXPORT bool moveToBuilding(MapExtras::MapCache &mc, df::item *item, df::building_actual *building,int16_t use_mode);
 DFHACK_EXPORT bool moveToInventory(MapExtras::MapCache &mc, df::item *item, df::unit *unit,
     df::unit_inventory_item::T_mode mode = df::unit_inventory_item::Carried, int body_part = -1);
+
+/// Detaches the items from its current location and turns it into a projectile
+DFHACK_EXPORT df::proj_itemst *makeProjectile(MapExtras::MapCache &mc, df::item *item);
 }
 }
diff --git a/library/include/modules/MapCache.h b/library/include/modules/MapCache.h
index 109a20a41..262e70bbf 100644
--- a/library/include/modules/MapCache.h
+++ b/library/include/modules/MapCache.h
@@ -253,6 +253,8 @@ public:
     bool is_valid() { return valid; }
     df::map_block *getRaw() { return block; }
 
+    bool Allocate();
+
     MapCache *getParent() { return parent; }
 
 private:
@@ -262,6 +264,8 @@ private:
     MapCache *parent;
     df::map_block *block;
 
+    void init();
+
     int biomeIndexAt(df::coord2d p);
 
     bool valid;
@@ -347,6 +351,12 @@ class DFHACK_EXPORT MapCache
         return BlockAt(df::coord(coord.x>>4,coord.y>>4,coord.z));
     }
 
+    bool ensureBlockAt(df::coord coord)
+    {
+        Block *b = BlockAtTile(coord);
+        return b ? b->Allocate() : false;
+    }
+
     df::tiletype baseTiletypeAt (DFCoord tilecoord)
     {
         Block *b = BlockAtTile(tilecoord);
diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h
index e6e9682eb..869b21580 100644
--- a/library/include/modules/Maps.h
+++ b/library/include/modules/Maps.h
@@ -50,6 +50,7 @@ distribution.
 #include "df/tile_dig_designation.h"
 #include "df/tile_traffic.h"
 #include "df/feature_init.h"
+#include "df/flow_type.h"
 
 /**
  * \defgroup grp_maps Maps module and its types
@@ -232,14 +233,19 @@ extern DFHACK_EXPORT void getSize(uint32_t& x, uint32_t& y, uint32_t& z);
 /// get the position of the map on world map
 extern DFHACK_EXPORT void getPosition(int32_t& x, int32_t& y, int32_t& z);
 
+extern DFHACK_EXPORT bool isValidTilePos(int32_t x, int32_t y, int32_t z);
+inline bool isValidTilePos(df::coord pos) { return isValidTilePos(pos.x, pos.y, pos.z); }
+
 /**
  * Get the map block or NULL if block is not valid
  */
 extern DFHACK_EXPORT df::map_block * getBlock (int32_t blockx, int32_t blocky, int32_t blockz);
 extern DFHACK_EXPORT df::map_block * getTileBlock (int32_t x, int32_t y, int32_t z);
+extern DFHACK_EXPORT df::map_block * ensureTileBlock (int32_t x, int32_t y, int32_t z);
 
 inline df::map_block * getBlock (df::coord pos) { return getBlock(pos.x, pos.y, pos.z); }
 inline df::map_block * getTileBlock (df::coord pos) { return getTileBlock(pos.x, pos.y, pos.z); }
+inline df::map_block * ensureTileBlock (df::coord pos) { return ensureTileBlock(pos.x, pos.y, pos.z); }
 
 extern DFHACK_EXPORT df::tiletype *getTileType(int32_t x, int32_t y, int32_t z);
 extern DFHACK_EXPORT df::tile_designation *getTileDesignation(int32_t x, int32_t y, int32_t z);
@@ -272,6 +278,8 @@ inline df::coord2d getTileBiomeRgn(df::coord pos) {
 // Enables per-frame updates for liquid flow and/or temperature.
 DFHACK_EXPORT void enableBlockUpdates(df::map_block *blk, bool flow = false, bool temperature = false);
 
+DFHACK_EXPORT df::flow_info *spawnFlow(df::coord pos, df::flow_type type, int mat_type = 0, int mat_index = -1, int density = 100);
+
 /// sorts the block event vector into multiple vectors by type
 /// mineral veins, what's under ice, blood smears and mud
 extern DFHACK_EXPORT bool SortBlockEvents(df::map_block *block,
diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h
index 492e1eecc..4f47205f2 100644
--- a/library/include/modules/Screen.h
+++ b/library/include/modules/Screen.h
@@ -65,6 +65,9 @@ namespace DFHack
             } tile_mode;
             int8_t tile_fg, tile_bg;
 
+            bool valid() const { return tile >= 0; }
+            bool empty() const { return ch == 0 && tile == 0; }
+
             Pen(char ch = 0, int8_t fg = 7, int8_t bg = 0, int tile = 0, bool color_tile = false)
               : ch(ch), fg(fg&7), bg(bg), bold(!!(fg&8)),
                 tile(tile), tile_mode(color_tile ? CharColor : AsIs), tile_fg(0), tile_bg(0)
@@ -92,6 +95,9 @@ namespace DFHack
         /// Paint one screen tile with the given pen
         DFHACK_EXPORT bool paintTile(const Pen &pen, int x, int y);
 
+        /// Retrieves one screen tile from the buffer
+        DFHACK_EXPORT Pen readTile(int x, int y);
+
         /// Paint a string onto the screen. Ignores ch and tile of pen.
         DFHACK_EXPORT bool paintString(const Pen &pen, int x, int y, const std::string &text);
 
diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h
index 9003dc3af..65f0b58a0 100644
--- a/library/include/modules/Units.h
+++ b/library/include/modules/Units.h
@@ -32,6 +32,10 @@ distribution.
 #include "modules/Items.h"
 #include "DataDefs.h"
 #include "df/unit.h"
+#include "df/misc_trait_type.h"
+#include "df/physical_attribute_type.h"
+#include "df/mental_attribute_type.h"
+#include "df/job_skill.h"
 
 namespace df
 {
@@ -41,6 +45,7 @@ namespace df
     struct historical_entity;
     struct entity_position_assignment;
     struct entity_position;
+    struct unit_misc_trait;
 }
 
 /**
@@ -208,6 +213,18 @@ DFHACK_EXPORT df::language_name *getVisibleName(df::unit *unit);
 DFHACK_EXPORT df::assumed_identity *getIdentity(df::unit *unit);
 DFHACK_EXPORT df::nemesis_record *getNemesis(df::unit *unit);
 
+DFHACK_EXPORT bool isHidingCurse(df::unit *unit);
+DFHACK_EXPORT int getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr);
+DFHACK_EXPORT int getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr);
+
+DFHACK_EXPORT bool isCrazed(df::unit *unit);
+DFHACK_EXPORT bool isOpposedToLife(df::unit *unit);
+DFHACK_EXPORT bool hasExtravision(df::unit *unit);
+DFHACK_EXPORT bool isBloodsucker(df::unit *unit);
+DFHACK_EXPORT bool isMischievous(df::unit *unit);
+
+DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create = false);
+
 DFHACK_EXPORT bool isDead(df::unit *unit);
 DFHACK_EXPORT bool isAlive(df::unit *unit);
 DFHACK_EXPORT bool isSane(df::unit *unit);
@@ -216,6 +233,9 @@ DFHACK_EXPORT bool isDwarf(df::unit *unit);
 
 DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false);
 
+DFHACK_EXPORT int getEffectiveSkill(df::unit *unit, df::job_skill skill_id);
+DFHACK_EXPORT int computeMovementSpeed(df::unit *unit);
+
 struct NoblePosition {
     df::historical_entity *entity;
     df::entity_position_assignment *assignment;
diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua
index 2cbd019a6..baf0d42e0 100644
--- a/library/lua/dfhack.lua
+++ b/library/lua/dfhack.lua
@@ -46,6 +46,7 @@ end
 -- Error handling
 
 safecall = dfhack.safecall
+curry = dfhack.curry
 
 function dfhack.pcall(f, ...)
     return xpcall(f, dfhack.onerror, ...)
@@ -83,7 +84,7 @@ function mkmodule(module,env)
             error("Not a table in package.loaded["..module.."]")
         end
     end
-    local plugname = string.match(module,'^plugins%.(%w+)$')
+    local plugname = string.match(module,'^plugins%.([%w%-]+)$')
     if plugname then
         dfhack.open_plugin(pkg,plugname)
     end
@@ -118,7 +119,12 @@ function defclass(class,parent)
     if parent then
         setmetatable(class, parent)
     else
-        rawset_default(class, { init_fields = rawset_default })
+        rawset_default(class, {
+            init_fields = rawset_default,
+            callback = function(self, name, ...)
+                return dfhack.curry(self[name], self, ...)
+            end
+        })
     end
     return class
 end
@@ -163,6 +169,23 @@ function xyz2pos(x,y,z)
     end
 end
 
+function pos2xy(pos)
+    if pos then
+        local x = pos.x
+        if x and x ~= -30000 then
+            return x, pos.y
+        end
+    end
+end
+
+function xy2pos(x,y)
+    if x then
+        return {x=x,y=y}
+    else
+        return {x=-30000,y=-30000}
+    end
+end
+
 function safe_index(obj,idx,...)
     if obj == nil or idx == nil then
         return nil
diff --git a/library/lua/gui.lua b/library/lua/gui.lua
index 9e189ea13..f9b6ab6d2 100644
--- a/library/lua/gui.lua
+++ b/library/lua/gui.lua
@@ -94,6 +94,9 @@ function Painter:isValidPos()
 end
 
 function Painter:viewport(x,y,w,h)
+    if type(x) == 'table' then
+        x,y,w,h = x.x1, x.y1, x.width, x.height
+    end
     local x1,y1 = self.x1+x, self.y1+y
     local x2,y2 = x1+w-1, y1+h-1
     local vp = {
@@ -159,10 +162,10 @@ function Painter:fill(x1,y1,x2,y2,pen,bg,bold)
     if type(x1) == 'table' then
         x1, y1, x2, y2, pen, bg, bold = x1.x1, x1.y1, x1.x2, x1.y2, y1, x2, y2
     end
-    x1 = math.max(x1,self.clip_x1)
-    y1 = math.max(y1,self.clip_y1)
-    x2 = math.min(x2,self.clip_x2)
-    y2 = math.min(y2,self.clip_y2)
+    x1 = math.max(x1+self.x1,self.clip_x1)
+    y1 = math.max(y1+self.y1,self.clip_y1)
+    x2 = math.min(x2+self.x1,self.clip_x2)
+    y2 = math.min(y2+self.y1,self.clip_y2)
     dscreen.fillRect(to_pen(self.cur_pen,pen,bg,bold),x1,y1,x2,y2)
     return self
 end
@@ -353,11 +356,16 @@ local function hint_coord(gap,hint)
     end
 end
 
+function FramedScreen:getWantedFrameSize()
+    return self.frame_width, self.frame_height
+end
+
 function FramedScreen:updateFrameSize()
     local sw, sh = dscreen.getWindowSize()
     local iw, ih = sw-2, sh-2
-    local width = math.min(self.frame_width or iw, iw)
-    local height = math.min(self.frame_height or ih, ih)
+    local fw, fh = self:getWantedFrameSize()
+    local width = math.min(fw or iw, iw)
+    local height = math.min(fh or ih, ih)
     local gw, gh = iw-width, ih-height
     local x1, y1 = hint_coord(gw,self.frame_xhint), hint_coord(gh,self.frame_yhint)
     self.frame_rect = mkdims_wh(x1+1,y1+1,width,height)
diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua
new file mode 100644
index 000000000..eb883465f
--- /dev/null
+++ b/library/lua/gui/dialogs.lua
@@ -0,0 +1,271 @@
+-- Some simple dialog screens
+
+local _ENV = mkmodule('gui.dialogs')
+
+local gui = require('gui')
+local utils = require('utils')
+
+local dscreen = dfhack.screen
+
+MessageBox = defclass(MessageBox, gui.FramedScreen)
+
+MessageBox.focus_path = 'MessageBox'
+MessageBox.frame_style = gui.GREY_LINE_FRAME
+
+function MessageBox:init(info)
+    info = info or {}
+    self:init_fields{
+        text = info.text or {},
+        frame_title = info.title,
+        frame_width = info.frame_width,
+        on_accept = info.on_accept,
+        on_cancel = info.on_cancel,
+        on_close = info.on_close,
+        text_pen = info.text_pen
+    }
+    if type(self.text) == 'string' then
+        self.text = utils.split_string(self.text, "\n")
+    end
+    gui.FramedScreen.init(self, info)
+    return self
+end
+
+function MessageBox:getWantedFrameSize()
+    local text = self.text
+    local w = #(self.frame_title or '') + 4
+    w = math.max(w, 20)
+    w = math.max(self.frame_width or w, w)
+    for _, l in ipairs(text) do
+        w = math.max(w, #l)
+    end
+    local h = #text+1
+    if h > 1 then
+        h = h+1
+    end
+    return w+2, #text+2
+end
+
+function MessageBox:onRenderBody(dc)
+    if #self.text > 0 then
+        dc:newline(1):pen(self.text_pen or COLOR_GREY)
+        for _, l in ipairs(self.text or {}) do
+            dc:string(l):newline(1)
+        end
+    end
+
+    if self.on_accept then
+        local x,y = self.frame_rect.x1+1, self.frame_rect.y2+1
+        dscreen.paintString({fg=COLOR_LIGHTGREEN},x,y,'ESC')
+        dscreen.paintString({fg=COLOR_GREY},x+3,y,'/')
+        dscreen.paintString({fg=COLOR_LIGHTGREEN},x+4,y,'y')
+    end
+end
+
+function MessageBox:onDestroy()
+    if self.on_close then
+        self.on_close()
+    end
+end
+
+function MessageBox:onInput(keys)
+    if keys.MENU_CONFIRM then
+        self:dismiss()
+        if self.on_accept then
+            self.on_accept()
+        end
+    elseif keys.LEAVESCREEN or (keys.SELECT and not self.on_accept) then
+        self:dismiss()
+        if self.on_cancel then
+            self.on_cancel()
+        end
+    end
+end
+
+function showMessage(title, text, tcolor, on_close)
+    mkinstance(MessageBox):init{
+        text = text,
+        title = title,
+        text = text,
+        text_pen = tcolor,
+        on_close = on_close
+    }:show()
+end
+
+function showYesNoPrompt(title, text, tcolor, on_accept, on_cancel)
+    mkinstance(MessageBox):init{
+        title = title,
+        text = text,
+        text_pen = tcolor,
+        on_accept = on_accept,
+        on_cancel = on_cancel,
+    }:show()
+end
+
+InputBox = defclass(InputBox, MessageBox)
+
+InputBox.focus_path = 'InputBox'
+
+function InputBox:init(info)
+    info = info or {}
+    self:init_fields{
+        input = info.input or '',
+        input_pen = info.input_pen,
+        on_input = info.on_input,
+    }
+    MessageBox.init(self, info)
+    self.on_accept = nil
+    return self
+end
+
+function InputBox:getWantedFrameSize()
+    local mw, mh = MessageBox.getWantedFrameSize(self)
+    return mw, mh+2
+end
+
+function InputBox:onRenderBody(dc)
+    MessageBox.onRenderBody(self, dc)
+
+    dc:newline(1)
+    dc:pen(self.input_pen or COLOR_LIGHTCYAN)
+    dc:fill(1,dc:localY(),dc.width-2,dc:localY())
+
+    local cursor = '_'
+    if math.floor(dfhack.getTickCount()/300) % 2 == 0 then
+        cursor = ' '
+    end
+    local txt = self.input .. cursor
+    if #txt > dc.width-2 then
+        txt = string.char(27)..string.sub(txt, #txt-dc.width+4)
+    end
+    dc:string(txt)
+end
+
+function InputBox:onInput(keys)
+    if keys.SELECT then
+        self:dismiss()
+        if self.on_input then
+            self.on_input(self.input)
+        end
+    elseif keys.LEAVESCREEN then
+        self:dismiss()
+        if self.on_cancel then
+            self.on_cancel()
+        end
+    elseif keys._STRING then
+        if keys._STRING == 0 then
+            self.input = string.sub(self.input, 1, #self.input-1)
+        else
+            self.input = self.input .. string.char(keys._STRING)
+        end
+    end
+end
+
+function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width)
+    mkinstance(InputBox):init{
+        title = title,
+        text = text,
+        text_pen = tcolor,
+        input = input,
+        on_input = on_input,
+        on_cancel = on_cancel,
+        frame_width = min_width,
+    }:show()
+end
+
+ListBox = defclass(ListBox, MessageBox)
+
+ListBox.focus_path = 'ListBox'
+
+function ListBox:init(info)
+    info = info or {}
+    self:init_fields{
+        selection = info.selection or 0,
+        choices = info.choices or {},
+        select_pen = info.select_pen,
+        on_input = info.on_input,
+        page_top = 0
+    }
+    MessageBox.init(self, info)
+    self.on_accept = nil
+    return self
+end
+
+function ListBox:getWantedFrameSize()
+    local mw, mh = MessageBox.getWantedFrameSize(self)
+    return mw, mh+#self.choices
+end
+
+function ListBox:onRenderBody(dc)
+    MessageBox.onRenderBody(self, dc)
+
+    dc:newline(1)
+
+    if self.selection>dc.height-3 then
+        self.page_top=self.selection-(dc.height-3)
+    elseif self.selection0  then
+        self.page_top=self.selection-1
+    end
+    for i,entry in ipairs(self.choices) do
+        if type(entry)=="table" then
+            entry=entry[1]
+        end
+        if i>self.page_top then
+            if i == self.selection then
+                dc:pen(self.select_pen or COLOR_LIGHTCYAN)
+            else
+                dc:pen(self.text_pen or COLOR_GREY)
+            end
+            dc:string(entry)
+            dc:newline(1)
+        end
+    end
+end
+function ListBox:moveCursor(delta)
+    local newsel=self.selection+delta
+    if #self.choices ~=0 then
+        if newsel<1 or newsel>#self.choices then 
+            newsel=newsel % #self.choices
+        end
+    end
+    self.selection=newsel
+end
+function ListBox:onInput(keys)
+    if keys.SELECT then
+        self:dismiss()
+        local choice=self.choices[self.selection]
+        if self.on_input then
+            self.on_input(self.selection,choice)
+        end
+
+        if choice and choice[2] then
+            choice[2](choice,self.selection) -- maybe reverse the arguments?
+        end
+    elseif keys.LEAVESCREEN then
+        self:dismiss()
+        if self.on_cancel then
+            self.on_cancel()
+        end
+    elseif keys.CURSOR_UP then
+        self:moveCursor(-1)
+    elseif keys.CURSOR_DOWN then
+        self:moveCursor(1)
+    elseif keys.CURSOR_UP_FAST then
+        self:moveCursor(-10)
+    elseif keys.CURSOR_DOWN_FAST then
+        self:moveCursor(10)
+    end
+end
+
+function showListPrompt(title, text, tcolor, choices, on_input, on_cancel, min_width)
+    mkinstance(ListBox):init{
+        title = title,
+        text = text,
+        text_pen = tcolor,
+        choices = choices,
+        on_input = on_input,
+        on_cancel = on_cancel,
+        frame_width = min_width,
+    }:show()
+end
+
+return _ENV
diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua
index 1f7ae1b03..661e15591 100644
--- a/library/lua/gui/dwarfmode.lua
+++ b/library/lua/gui/dwarfmode.lua
@@ -46,7 +46,7 @@ function getPanelLayout()
 end
 
 function getCursorPos()
-    if g_cursor ~= -30000 then
+    if g_cursor.x ~= -30000 then
         return copyall(g_cursor)
     end
 end
@@ -136,6 +136,14 @@ function Viewport:set()
     return vp
 end
 
+function Viewport:getPos()
+    return xyz2pos(self.x1, self.y1, self.z)
+end
+
+function Viewport:getSize()
+    return xy2pos(self.width, self.height)
+end
+
 function Viewport:clip(x,y,z)
     return self:make(
         math.max(0, math.min(x or self.x1, world_map.x_count-self.width)),
@@ -159,6 +167,18 @@ function Viewport:isVisible(target,gap)
     return self:isVisibleXY(target,gap) and target.z == self.z
 end
 
+function Viewport:tileToScreen(coord)
+    return xyz2pos(coord.x - self.x1, coord.y - self.y1, coord.z - self.z)
+end
+
+function Viewport:getCenter()
+    return xyz2pos(
+        math.floor((self.x2+self.x1)/2),
+        math.floor((self.y2+self.y1)/2),
+        self.z
+    )
+end
+
 function Viewport:centerOn(target)
     return self:clip(
         target.x - math.floor(self.width/2),
@@ -207,16 +227,24 @@ MOVEMENT_KEYS = {
     CURSOR_UP_Z_AUX = { 0, 0, 1 }, CURSOR_DOWN_Z_AUX = { 0, 0, -1 },
 }
 
-function Viewport:scrollByKey(key)
+local function get_movement_delta(key, delta, big_step)
     local info = MOVEMENT_KEYS[key]
     if info then
-        local delta = 10
-        if info[4] then delta = 20 end
+        if info[4] then
+            delta = big_step
+        end
+
+        return delta*info[1], delta*info[2], info[3]
+    end
+end
 
+function Viewport:scrollByKey(key)
+    local dx, dy, dz = get_movement_delta(key, 10, 20)
+    if dx then
         return self:clip(
-            self.x1 + delta*info[1],
-            self.y1 + delta*info[2],
-            self.z + info[3]
+            self.x1 + dx,
+            self.y1 + dy,
+            self.z + dz
         )
     else
         return self
@@ -237,16 +265,23 @@ function DwarfOverlay:getViewport(old_vp)
     end
 end
 
-function DwarfOverlay:moveCursorTo(cursor,viewport)
+function DwarfOverlay:moveCursorTo(cursor,viewport,gap)
     setCursorPos(cursor)
-    self:getViewport(viewport):reveal(cursor, 5, 0, 10):set()
+    self:zoomViewportTo(cursor,viewport,gap)
+end
+
+function DwarfOverlay:zoomViewportTo(target, viewport, gap)
+    if gap and self:getViewport():isVisible(target, gap) then
+        return
+    end
+    self:getViewport(viewport):reveal(target, 5, 0, 10):set()
 end
 
-function DwarfOverlay:selectBuilding(building,cursor,viewport)
+function DwarfOverlay:selectBuilding(building,cursor,viewport,gap)
     cursor = cursor or utils.getBuildingCenter(building)
 
     df.global.world.selected_building = building
-    self:moveCursorTo(cursor, viewport)
+    self:moveCursorTo(cursor, viewport, gap)
 end
 
 function DwarfOverlay:propagateMoveKeys(keys)
@@ -282,6 +317,31 @@ function DwarfOverlay:simulateViewScroll(keys, anchor, no_clip_cursor)
     end
 end
 
+function DwarfOverlay:simulateCursorMovement(keys, anchor)
+    local layout = self.df_layout
+    local cursor = getCursorPos()
+    local cx, cy, cz = pos2xyz(cursor)
+
+    if anchor and keys.A_MOVE_SAME_SQUARE then
+        setCursorPos(anchor)
+        self:getViewport():centerOn(anchor):set()
+        return 'A_MOVE_SAME_SQUARE'
+    end
+
+    for code,_ in pairs(MOVEMENT_KEYS) do
+        if keys[code] then
+            local dx, dy, dz = get_movement_delta(code, 1, 10)
+            local ncur = xyz2pos(cx+dx, cy+dy, cz+dz)
+
+            if dfhack.maps.isValidTilePos(ncur) then
+                setCursorPos(ncur)
+                self:getViewport():reveal(ncur,4,10,6,true):set()
+                return code
+            end
+        end
+    end
+end
+
 function DwarfOverlay:onAboutToShow(below)
     local screen = dfhack.gui.getCurViewscreen()
     if below then screen = below.parent end
diff --git a/library/lua/utils.lua b/library/lua/utils.lua
index 19a4e6f6a..9fa473ed8 100644
--- a/library/lua/utils.lua
+++ b/library/lua/utils.lua
@@ -381,6 +381,19 @@ function getBuildingCenter(building)
     return xyz2pos(building.centerx, building.centery, building.z)
 end
 
+function split_string(self, delimiter)
+    local result = { }
+    local from  = 1
+    local delim_from, delim_to = string.find( self, delimiter, from  )
+    while delim_from do
+        table.insert( result, string.sub( self, from , delim_from-1 ) )
+        from  = delim_to + 1
+        delim_from, delim_to = string.find( self, delimiter, from  )
+    end
+    table.insert( result, string.sub( self, from  ) )
+    return result
+end
+
 -- Ask a yes-no question
 function prompt_yes_no(msg,default)
     local prompt = msg
diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp
index d1aed8979..9e78edd3e 100644
--- a/library/modules/Buildings.cpp
+++ b/library/modules/Buildings.cpp
@@ -324,7 +324,7 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in
         {
             auto obj = (df::building_trapst*)bld;
             if (obj->trap_type == trap_type::PressurePlate)
-                obj->unk_cc = 500;
+                obj->ready_timeout = 500;
             break;
         }
     default:
diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp
index 0f28860bf..91df14eaf 100644
--- a/library/modules/Gui.cpp
+++ b/library/modules/Gui.cpp
@@ -43,6 +43,7 @@ using namespace DFHack;
 
 #include "modules/Job.h"
 #include "modules/Screen.h"
+#include "modules/Maps.h"
 
 #include "DataDefs.h"
 #include "df/world.h"
@@ -81,6 +82,8 @@ using namespace DFHack;
 #include "df/graphic.h"
 #include "df/layer_object_listst.h"
 #include "df/assign_trade_status.h"
+#include "df/announcement_flags.h"
+#include "df/announcements.h"
 
 using namespace df::enums;
 using df::global::gview;
@@ -88,6 +91,9 @@ using df::global::init;
 using df::global::gps;
 using df::global::ui;
 using df::global::world;
+using df::global::selection_rect;
+using df::global::ui_menu_width;
+using df::global::ui_area_map_width;
 
 static df::layer_object_listst *getLayerList(df::viewscreen_layerst *layer, int idx)
 {
@@ -167,10 +173,9 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
             else if (id == &df::building_trapst::_identity)
             {
                 auto trap = (df::building_trapst*)selected;
-                if (trap->trap_type == trap_type::Lever) {
-                    focus += "/Lever";
+                focus += "/" + enum_item_key(trap->trap_type);
+                if (trap->trap_type == trap_type::Lever)
                     jobs = true;
-                }
             }
             else if (ui_building_in_assign && *ui_building_in_assign &&
                      ui_building_assign_type && ui_building_assign_units &&
@@ -183,6 +188,8 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
                     focus += unit ? "/Unit" : "/None";
                 }
             }
+            else
+                focus += "/" + enum_item_key(selected->getType());
 
             if (jobs)
             {
@@ -205,7 +212,14 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
             if (ui_build_selector->building_type < 0)
                 focus += "/Type";
             else if (ui_build_selector->stage != 2)
-                focus += "/Position";
+            {
+                if (ui_build_selector->stage != 1)
+                    focus += "/NoMaterials";
+                else
+                    focus += "/Position";
+
+                focus += "/" + enum_item_key(ui_build_selector->building_type);
+            }
             else
             {
                 focus += "/Material";
@@ -921,8 +935,9 @@ df::item *Gui::getSelectedItem(color_ostream &out, bool quiet)
 
 //
 
-void Gui::showAnnouncement(std::string message, int color, bool bright)
-{
+static void doShowAnnouncement(
+    df::announcement_type type, df::coord pos, std::string message, int color, bool bright
+) {
     using df::global::world;
     using df::global::cur_year;
     using df::global::cur_year_tick;
@@ -948,6 +963,9 @@ void Gui::showAnnouncement(std::string message, int color, bool bright)
     {
         df::report *new_rep = new df::report();
 
+        new_rep->type = type;
+        new_rep->pos = pos;
+
         new_rep->color = color;
         new_rep->bright = bright;
         new_rep->year = year;
@@ -969,7 +987,17 @@ void Gui::showAnnouncement(std::string message, int color, bool bright)
         world->status.announcements.push_back(new_rep);
         world->status.display_timer = 2000;
     }
+}
+
+void Gui::showAnnouncement(std::string message, int color, bool bright)
+{
+    doShowAnnouncement(df::announcement_type(0), df::coord(), message, color, bright);
+}
 
+void Gui::showZoomAnnouncement(
+    df::announcement_type type, df::coord pos, std::string message, int color, bool bright
+) {
+    doShowAnnouncement(type, pos, message, color, bright);
 }
 
 void Gui::showPopupAnnouncement(std::string message, int color, bool bright)
@@ -983,6 +1011,29 @@ void Gui::showPopupAnnouncement(std::string message, int color, bool bright)
     world->status.popups.push_back(popup);
 }
 
+void Gui::showAutoAnnouncement(
+    df::announcement_type type, df::coord pos, std::string message, int color, bool bright
+) {
+    using df::global::announcements;
+
+    df::announcement_flags flags;
+    if (is_valid_enum_item(type) && announcements)
+        flags = announcements->flags[type];
+
+    doShowAnnouncement(type, pos, message, color, bright);
+
+    if (flags.bits.DO_MEGA || flags.bits.PAUSE || flags.bits.RECENTER)
+    {
+        resetDwarfmodeView(flags.bits.DO_MEGA || flags.bits.PAUSE);
+
+        if (flags.bits.RECENTER && pos.isValid())
+            revealInDwarfmodeMap(pos, true);
+    }
+
+    if (flags.bits.DO_MEGA)
+        showPopupAnnouncement(message, color, bright);
+}
+
 df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed)
 {
     df::viewscreen * ws = &gview->view;
@@ -1015,6 +1066,110 @@ df::coord Gui::getCursorPos()
     return df::coord(cursor->x, cursor->y, cursor->z);
 }
 
+Gui::DwarfmodeDims Gui::getDwarfmodeViewDims()
+{
+    DwarfmodeDims dims;
+
+    auto ws = Screen::getWindowSize();
+    dims.y1 = 1;
+    dims.y2 = ws.y-2;
+    dims.map_x1 = 1;
+    dims.map_x2 = ws.x-2;
+    dims.area_x1 = dims.area_x2 = dims.menu_x1 = dims.menu_x2 = -1;
+    dims.menu_forced = false;
+
+    int menu_pos = (ui_menu_width ? *ui_menu_width : 2);
+    int area_pos = (ui_area_map_width ? *ui_area_map_width : 3);
+
+    if (ui && ui->main.mode && menu_pos >= area_pos)
+    {
+        dims.menu_forced = true;
+        menu_pos = area_pos-1;
+    }
+
+    dims.area_on = (area_pos < 3);
+    dims.menu_on = (menu_pos < area_pos);
+
+    if (dims.menu_on)
+    {
+        dims.menu_x2 = ws.x - 2;
+        dims.menu_x1 = dims.menu_x2 - Gui::MENU_WIDTH + 1;
+        if (menu_pos == 1)
+            dims.menu_x1 -= Gui::AREA_MAP_WIDTH + 1;
+        dims.map_x2 = dims.menu_x1 - 2;
+    }
+    if (dims.area_on)
+    {
+        dims.area_x2 = ws.x-2;
+        dims.area_x1 = dims.area_x2 - Gui::AREA_MAP_WIDTH + 1;
+        if (dims.menu_on)
+            dims.menu_x2 = dims.area_x1 - 2;
+        else
+            dims.map_x2 = dims.area_x1 - 2;
+    }
+
+    return dims;
+}
+
+void Gui::resetDwarfmodeView(bool pause)
+{
+    using df::global::cursor;
+
+    if (ui)
+    {
+        ui->follow_unit = -1;
+        ui->follow_item = -1;
+        ui->main.mode = ui_sidebar_mode::Default;
+    }
+
+    if (selection_rect)
+    {
+        selection_rect->start_x = -30000;
+        selection_rect->end_x = -30000;
+    }
+
+    if (cursor)
+        cursor->x = cursor->y = cursor->z = -30000;
+
+    if (pause && df::global::pause_state)
+        *df::global::pause_state = true;
+}
+
+bool Gui::revealInDwarfmodeMap(df::coord pos, bool center)
+{
+    using df::global::window_x;
+    using df::global::window_y;
+    using df::global::window_z;
+
+    if (!window_x || !window_y || !window_z || !world)
+        return false;
+    if (!Maps::isValidTilePos(pos))
+        return false;
+
+    auto dims = getDwarfmodeViewDims();
+    int w = dims.map_x2 - dims.map_x1 + 1;
+    int h = dims.y2 - dims.y1 + 1;
+
+    *window_z = pos.z;
+
+    if (center)
+    {
+        *window_x = pos.x - w/2;
+        *window_y = pos.y - h/2;
+    }
+    else
+    {
+        while (*window_x + w < pos.x+5) *window_x += 10;
+        while (*window_y + h < pos.y+5) *window_y += 10;
+        while (*window_x + 5 > pos.x) *window_x -= 10;
+        while (*window_y + 5 > pos.y) *window_y -= 10;
+    }
+
+    *window_x = std::max(0, std::min(*window_x, world->map.x_count-w));
+    *window_y = std::max(0, std::min(*window_y, world->map.y_count-h));
+    return true;
+}
+
 bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z)
 {
     x = *df::global::window_x;
diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp
index dc64143c9..751797f06 100644
--- a/library/modules/Items.cpp
+++ b/library/modules/Items.cpp
@@ -72,8 +72,11 @@ using namespace std;
 #include "df/general_ref_contains_itemst.h"
 #include "df/general_ref_contained_in_itemst.h"
 #include "df/general_ref_building_holderst.h"
+#include "df/general_ref_projectile.h"
 #include "df/viewscreen_itemst.h"
 #include "df/vermin.h"
+#include "df/proj_itemst.h"
+#include "df/proj_list_link.h"
 
 #include "df/unit_inventory_item.h"
 #include "df/body_part_raw.h"
@@ -88,6 +91,7 @@ using namespace df::enums;
 using df::global::world;
 using df::global::ui;
 using df::global::ui_selected_unit;
+using df::global::proj_next_id;
 
 #define ITEMDEF_VECTORS \
     ITEM(WEAPON, weapons, itemdef_weaponst) \
@@ -866,3 +870,44 @@ bool DFHack::Items::moveToInventory(
 
     return true;
 }
+
+df::proj_itemst *Items::makeProjectile(MapExtras::MapCache &mc, df::item *item)
+{
+    CHECK_NULL_POINTER(item);
+
+    if (!world || !proj_next_id)
+        return NULL;
+
+    auto pos = getPosition(item);
+    if (!pos.isValid())
+        return NULL;
+
+    auto ref = df::allocate();
+    if (!ref)
+        return NULL;
+
+    if (!detachItem(mc, item))
+    {
+        delete ref;
+        return NULL;
+    }
+
+    item->pos = pos;
+    item->flags.bits.in_job = true;
+
+    auto proj = new df::proj_itemst();
+    proj->link = new df::proj_list_link();
+    proj->link->item = proj;
+    proj->id = (*proj_next_id)++;
+
+    proj->origin_pos = proj->target_pos = pos;
+    proj->cur_pos = proj->prev_pos = pos;
+    proj->item = item;
+
+    ref->projectile_id = proj->id;
+    item->itemrefs.push_back(ref);
+
+    linked_list_append(&world->proj_list, proj->link);
+
+    return proj;
+}
diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp
index 54b4eb27e..b74a4b73f 100644
--- a/library/modules/Job.cpp
+++ b/library/modules/Job.cpp
@@ -181,7 +181,7 @@ void DFHack::Job::printItemDetails(color_ostream &out, df::job_item *item, int i
         out << "    reaction class: " << item->reaction_class << endl;
     if (!item->has_material_reaction_product.empty())
         out << "    reaction product: " << item->has_material_reaction_product << endl;
-    if (item->has_tool_use >= 0)
+    if (item->has_tool_use >= (df::tool_uses)0)
         out << "    tool use: " << ENUM_KEY_STR(tool_uses, item->has_tool_use) << endl;
 }
 
diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp
index 4107680b0..f1f40f19c 100644
--- a/library/modules/Maps.cpp
+++ b/library/modules/Maps.cpp
@@ -58,6 +58,7 @@ using namespace std;
 #include "df/block_square_event_grassst.h"
 #include "df/z_level_flags.h"
 #include "df/region_map_entry.h"
+#include "df/flow_info.h"
 
 using namespace DFHack;
 using namespace df::enums;
@@ -138,17 +139,57 @@ df::map_block *Maps::getBlock (int32_t blockx, int32_t blocky, int32_t blockz)
     return world->map.block_index[blockx][blocky][blockz];
 }
 
-df::map_block *Maps::getTileBlock (int32_t x, int32_t y, int32_t z)
+bool Maps::isValidTilePos(int32_t x, int32_t y, int32_t z)
 {
     if (!IsValid())
-        return NULL;
+        return false;
     if ((x < 0) || (y < 0) || (z < 0))
-        return NULL;
+        return false;
     if ((x >= world->map.x_count) || (y >= world->map.y_count) || (z >= world->map.z_count))
+        return false;
+    return true;
+}
+
+df::map_block *Maps::getTileBlock (int32_t x, int32_t y, int32_t z)
+{
+    if (!isValidTilePos(x,y,z))
         return NULL;
     return world->map.block_index[x >> 4][y >> 4][z];
 }
 
+df::map_block *Maps::ensureTileBlock (int32_t x, int32_t y, int32_t z)
+{
+    if (!isValidTilePos(x,y,z))
+        return NULL;
+
+    auto column = world->map.block_index[x >> 4][y >> 4];
+    auto &slot = column[z];
+    if (slot)
+        return slot;
+
+    // Find another block below
+    int z2 = z;
+    while (z2 >= 0 && !column[z2]) z2--;
+    if (z2 < 0)
+        return NULL;
+
+    slot = new df::map_block();
+    slot->region_pos = column[z2]->region_pos;
+    slot->map_pos = column[z2]->map_pos;
+    slot->map_pos.z = z;
+
+    // Assume sky
+    df::tile_designation dsgn(0);
+    dsgn.bits.light = true;
+    dsgn.bits.outside = true;
+
+    for (int tx = 0; tx < 16; tx++)
+        for (int ty = 0; ty < 16; ty++)
+            slot->designation[tx][ty] = dsgn;
+
+    return slot;
+}
+
 df::tiletype *Maps::getTileType(int32_t x, int32_t y, int32_t z)
 {
     df::map_block *block = getTileBlock(x,y,z);
@@ -204,6 +245,26 @@ void Maps::enableBlockUpdates(df::map_block *blk, bool flow, bool temperature)
     }
 }
 
+df::flow_info *Maps::spawnFlow(df::coord pos, df::flow_type type, int mat_type, int mat_index, int density)
+{
+    using df::global::flows;
+
+    auto block = getTileBlock(pos);
+    if (!flows || !block)
+        return NULL;
+
+    auto flow = new df::flow_info();
+    flow->type = type;
+    flow->mat_type = mat_type;
+    flow->mat_index = mat_index;
+    flow->density = std::min(100, density);
+    flow->pos = pos;
+
+    block->flows.push_back(flow);
+    flows->push_back(flow);
+    return flow;
+}
+
 df::feature_init *Maps::getGlobalInitFeature(int32_t index)
 {
     auto data = world->world_data;
@@ -246,7 +307,7 @@ df::feature_init *Maps::getLocalInitFeature(df::coord2d rgn_pos, int32_t index)
     df::coord2d bigregion = rgn_pos / 16;
 
     // bigregion is 16x16 regions. for each bigregion in X dimension:
-    auto fptr = data->unk_204[bigregion.x][bigregion.y].features;
+    auto fptr = data->feature_map[bigregion.x][bigregion.y].features;
     if (!fptr)
         return NULL;
 
@@ -485,8 +546,14 @@ MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent)
     valid = false;
     bcoord = _bcoord;
     block = Maps::getBlock(bcoord);
-    item_counts = NULL;
     tags = NULL;
+
+    init();
+}
+
+void MapExtras::Block::init()
+{
+    item_counts = NULL;
     tiles = NULL;
     basemats = NULL;
 
@@ -509,6 +576,23 @@ MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent)
     }
 }
 
+bool MapExtras::Block::Allocate()
+{
+    if (block)
+        return true;
+
+    block = Maps::ensureTileBlock(bcoord.x*16, bcoord.y*16, bcoord.z);
+    if (!block)
+        return false;
+
+    delete item_counts;
+    delete tiles;
+    delete basemats;
+    init();
+
+    return true;
+}
+
 MapExtras::Block::~Block()
 {
     delete[] item_counts;
diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp
index c2377f2ca..9f258fe02 100644
--- a/library/modules/Screen.cpp
+++ b/library/modules/Screen.cpp
@@ -100,7 +100,7 @@ static void doSetTile(const Pen &pen, int index)
 
 bool Screen::paintTile(const Pen &pen, int x, int y)
 {
-    if (!gps) return false;
+    if (!gps || !pen.valid()) return false;
 
     int dimx = gps->dimx, dimy = gps->dimy;
     if (x < 0 || x >= dimx || y < 0 || y >= dimy) return false;
@@ -109,6 +109,41 @@ bool Screen::paintTile(const Pen &pen, int x, int y)
     return true;
 }
 
+Pen Screen::readTile(int x, int y)
+{
+    if (!gps) return Pen(0,0,0,-1);
+
+    int dimx = gps->dimx, dimy = gps->dimy;
+    if (x < 0 || x >= dimx || y < 0 || y >= dimy)
+        return Pen(0,0,0,-1);
+
+    int index = x*dimy + y;
+    auto screen = gps->screen + index*4;
+    if (screen[3] & 0x80)
+        return Pen(0,0,0,-1);
+
+    Pen pen(
+        screen[0], screen[1], screen[2], screen[3]?true:false,
+        gps->screentexpos[index]
+    );
+
+    if (pen.tile)
+    {
+        if (gps->screentexpos_grayscale[index])
+        {
+            pen.tile_mode = Screen::Pen::TileColor;
+            pen.tile_fg = gps->screentexpos_cf[index];
+            pen.tile_bg = gps->screentexpos_cbr[index];
+        }
+        else if (gps->screentexpos_addcolor[index])
+        {
+            pen.tile_mode = Screen::Pen::CharColor;
+        }
+    }
+
+    return pen;
+}
+
 bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text)
 {
     if (!gps || y < 0 || y >= gps->dimy) return false;
@@ -132,7 +167,7 @@ bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text)
 
 bool Screen::fillRect(const Pen &pen, int x1, int y1, int x2, int y2)
 {
-    if (!gps) return false;
+    if (!gps || !pen.valid()) return false;
 
     if (x1 < 0) x1 = 0;
     if (y1 < 0) y1 = 0;
diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp
index 874dabc3d..01b7b50f4 100644
--- a/library/modules/Units.cpp
+++ b/library/modules/Units.cpp
@@ -63,11 +63,15 @@ using namespace std;
 #include "df/burrow.h"
 #include "df/creature_raw.h"
 #include "df/caste_raw.h"
+#include "df/game_mode.h"
+#include "df/unit_misc_trait.h"
+#include "df/unit_skill.h"
 
 using namespace DFHack;
 using namespace df::enums;
 using df::global::world;
 using df::global::ui;
+using df::global::gamemode;
 
 bool Units::isValid()
 {
@@ -613,6 +617,58 @@ df::nemesis_record *Units::getNemesis(df::unit *unit)
     return NULL;
 }
 
+
+bool Units::isHidingCurse(df::unit *unit)
+{
+    if (!unit->job.hunt_target)
+    {
+        auto identity = Units::getIdentity(unit);
+        if (identity && identity->unk_4c == 0)
+            return true;
+    }
+
+    return false;
+}
+
+int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr)
+{
+    auto &aobj = unit->body.physical_attrs[attr];
+    int value = std::max(0, aobj.value - aobj.soft_demotion);
+
+    if (auto mod = unit->curse.attr_change)
+    {
+        int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr];
+
+        if (isHidingCurse(unit))
+            value = std::min(value, mvalue);
+        else
+            value = mvalue;
+    }
+
+    return std::max(0, value);
+}
+
+int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr)
+{
+    auto soul = unit->status.current_soul;
+    if (!soul) return 0;
+
+    auto &aobj = soul->mental_attrs[attr];
+    int value = std::max(0, aobj.value - aobj.soft_demotion);
+
+    if (auto mod = unit->curse.attr_change)
+    {
+        int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr];
+
+        if (isHidingCurse(unit))
+            value = std::min(value, mvalue);
+        else
+            value = mvalue;
+    }
+
+    return std::max(0, value);
+}
+
 static bool casteFlagSet(int race, int caste, df::caste_raw_flags flag)
 {
     auto creature = df::creature_raw::find(race);
@@ -626,8 +682,9 @@ static bool casteFlagSet(int race, int caste, df::caste_raw_flags flag)
     return craw->flags.is_set(flag);
 }
 
-static bool isCrazed(df::unit *unit)
+bool Units::isCrazed(df::unit *unit)
 {
+    CHECK_NULL_POINTER(unit);
     if (unit->flags3.bits.scuttle)
         return false;
     if (unit->curse.rem_tags1.bits.CRAZED)
@@ -637,13 +694,64 @@ static bool isCrazed(df::unit *unit)
     return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED);
 }
 
-static bool isOpposedToLife(df::unit *unit)
+bool Units::isOpposedToLife(df::unit *unit)
 {
+    CHECK_NULL_POINTER(unit);
     if (unit->curse.rem_tags1.bits.OPPOSED_TO_LIFE)
         return false;
     if (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE)
         return true;
-    return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CANNOT_UNDEAD);
+    return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE);
+}
+
+bool Units::hasExtravision(df::unit *unit)
+{
+    CHECK_NULL_POINTER(unit);
+    if (unit->curse.rem_tags1.bits.EXTRAVISION)
+        return false;
+    if (unit->curse.add_tags1.bits.EXTRAVISION)
+        return true;
+    return casteFlagSet(unit->race, unit->caste, caste_raw_flags::EXTRAVISION);
+}
+
+bool Units::isBloodsucker(df::unit *unit)
+{
+    CHECK_NULL_POINTER(unit);
+    if (unit->curse.rem_tags1.bits.BLOODSUCKER)
+        return false;
+    if (unit->curse.add_tags1.bits.BLOODSUCKER)
+        return true;
+    return casteFlagSet(unit->race, unit->caste, caste_raw_flags::BLOODSUCKER);
+}
+
+bool Units::isMischievous(df::unit *unit)
+{
+    CHECK_NULL_POINTER(unit);
+    if (unit->curse.rem_tags1.bits.MISCHIEVOUS)
+        return false;
+    if (unit->curse.add_tags1.bits.MISCHIEVOUS)
+        return true;
+    return casteFlagSet(unit->race, unit->caste, caste_raw_flags::MISCHIEVOUS);
+}
+
+df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create)
+{
+    CHECK_NULL_POINTER(unit);
+
+    auto &vec = unit->status.misc_traits;
+    for (size_t i = 0; i < vec.size(); i++)
+        if (vec[i]->id == type)
+            return vec[i];
+
+    if (create)
+    {
+        auto obj = new df::unit_misc_trait();
+        obj->id = type;
+        vec.push_back(obj);
+        return obj;
+    }
+
+    return NULL;
 }
 
 bool DFHack::Units::isDead(df::unit *unit)
@@ -753,6 +861,371 @@ double DFHack::Units::getAge(df::unit *unit, bool true_age)
     return cur_time - birth_time;
 }
 
+inline void adjust_skill_rating(int &rating, bool is_adventure, int value, int dwarf3_4, int dwarf1_2, int adv9_10, int adv3_4, int adv1_2)
+{
+    if  (is_adventure)
+    {
+        if (value >= adv1_2) rating >>= 1;
+        else if (value >= adv3_4) rating = rating*3/4;
+        else if (value >= adv9_10) rating = rating*9/10;
+    }
+    else
+    {
+        if (value >= dwarf1_2) rating >>= 1;
+        else if (value >= dwarf3_4) rating = rating*3/4;
+    }
+}
+
+int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id)
+{
+    CHECK_NULL_POINTER(unit);
+
+    /*
+     * This is 100% reverse-engineered from DF code.
+     */
+
+    if (!unit->status.current_soul)
+        return 0;
+
+    // Retrieve skill from unit soul:
+
+    df::enum_field key(skill_id);
+    auto skill = binsearch_in_vector(unit->status.current_soul->skills, &df::unit_skill::id, key);
+
+    int rating = 0;
+    if (skill)
+        rating = std::max(0, int(skill->rating) - skill->rusty);
+
+    // Apply special states
+
+    if (unit->counters.soldier_mood == df::unit::T_counters::None)
+    {
+        if (unit->counters.nausea > 0) rating >>= 1;
+        if (unit->counters.winded > 0) rating >>= 1;
+        if (unit->counters.stunned > 0) rating >>= 1;
+        if (unit->counters.dizziness > 0) rating >>= 1;
+        if (unit->counters2.fever > 0) rating >>= 1;
+    }
+
+    if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance)
+    {
+        if (!unit->flags3.bits.ghostly && !unit->flags3.bits.scuttle &&
+            !unit->flags2.bits.vision_good && !unit->flags2.bits.vision_damaged &&
+            !hasExtravision(unit))
+        {
+            rating >>= 2;
+        }
+        if (unit->counters.pain >= 100 && unit->mood == -1)
+        {
+            rating >>= 1;
+        }
+        if (unit->counters2.exhaustion >= 2000)
+        {
+            rating = rating*3/4;
+            if (unit->counters2.exhaustion >= 4000)
+            {
+                rating = rating*3/4;
+                if (unit->counters2.exhaustion >= 6000)
+                    rating = rating*3/4;
+            }
+        }
+    }
+
+    // Hunger etc timers
+
+    bool is_adventure = (gamemode && *gamemode == game_mode::ADVENTURE);
+
+    if (!unit->flags3.bits.scuttle && isBloodsucker(unit))
+    {
+        using namespace df::enums::misc_trait_type;
+
+        if (auto trait = getMiscTrait(unit, TimeSinceSuckedBlood))
+        {
+            adjust_skill_rating(
+                rating, is_adventure, trait->value,
+                302400, 403200,           // dwf 3/4; 1/2
+                1209600, 1209600, 2419200 // adv 9/10; 3/4; 1/2
+            );
+        }
+    }
+
+    adjust_skill_rating(
+        rating, is_adventure, unit->counters2.thirst_timer,
+        50000, 50000, 115200, 172800, 345600
+    );
+    adjust_skill_rating(
+        rating, is_adventure, unit->counters2.hunger_timer,
+        75000, 75000, 172800, 1209600, 2592000
+    );
+    if (is_adventure && unit->counters2.sleepiness_timer >= 846000)
+        rating >>= 2;
+    else
+        adjust_skill_rating(
+            rating, is_adventure, unit->counters2.sleepiness_timer,
+            150000, 150000, 172800, 259200, 345600
+        );
+
+    return rating;
+}
+
+inline void adjust_speed_rating(int &rating, bool is_adventure, int value, int dwarf100, int dwarf200, int adv50, int adv75, int adv100, int adv200)
+{
+    if  (is_adventure)
+    {
+        if (value >= adv200) rating += 200;
+        else if (value >= adv100) rating += 100;
+        else if (value >= adv75) rating += 75;
+        else if (value >= adv50) rating += 50;
+    }
+    else
+    {
+        if (value >= dwarf200) rating += 200;
+        else if (value >= dwarf100) rating += 100;
+    }
+}
+
+static int calcInventoryWeight(df::unit *unit)
+{
+    int armor_skill = Units::getEffectiveSkill(unit, job_skill::ARMOR);
+    int armor_mul = 15 - std::min(15, armor_skill);
+
+    int inv_weight = 0, inv_weight_fraction = 0;
+
+    for (size_t i = 0; i < unit->inventory.size(); i++)
+    {
+        auto item = unit->inventory[i]->item;
+        if (!item->flags.bits.weight_computed)
+            continue;
+
+        int wval = item->weight;
+        int wfval = item->weight_fraction;
+        auto mode = unit->inventory[i]->mode;
+
+        if ((mode == df::unit_inventory_item::Worn ||
+             mode == df::unit_inventory_item::WrappedAround) &&
+             item->isArmor() && armor_skill > 1)
+        {
+            wval = wval * armor_mul / 16;
+            wfval = wfval * armor_mul / 16;
+        }
+
+        inv_weight += wval;
+        inv_weight_fraction += wfval;
+    }
+
+    return inv_weight*100 + inv_weight_fraction/10000;
+}
+
+int Units::computeMovementSpeed(df::unit *unit)
+{
+    using namespace df::enums::physical_attribute_type;
+
+    /*
+     * Pure reverse-engineered computation of unit _slowness_,
+     * i.e. number of ticks to move * 100.
+     */
+
+    // Base speed
+
+    auto creature = df::creature_raw::find(unit->race);
+    if (!creature)
+        return 0;
+
+    auto craw = vector_get(creature->caste, unit->caste);
+    if (!craw)
+        return 0;
+
+    int speed = craw->misc.speed;
+
+    if (unit->flags3.bits.ghostly)
+        return speed;
+
+    // Curse multiplier
+
+    if (unit->curse.speed_mul_percent != 100)
+    {
+        speed *= 100;
+        if (unit->curse.speed_mul_percent != 0)
+            speed /= unit->curse.speed_mul_percent;
+    }
+
+    speed += unit->curse.speed_add;
+
+    // Swimming
+
+    auto cur_liquid = unit->status2.liquid_type.bits.liquid_type;
+    bool in_magma = (cur_liquid == tile_liquid::Magma);
+
+    if (unit->flags2.bits.swimming)
+    {
+        speed = craw->misc.swim_speed;
+        if (in_magma)
+            speed *= 2;
+
+        if (craw->flags.is_set(caste_raw_flags::SWIMS_LEARNED))
+        {
+            int skill = Units::getEffectiveSkill(unit, job_skill::SWIMMING);
+
+            // Originally a switch:
+            if (skill > 1)
+                speed = speed * std::max(6, 21-skill) / 20;
+        }
+    }
+    else
+    {
+        int delta = 150*unit->status2.liquid_depth;
+        if (in_magma)
+            delta *= 2;
+        speed += delta;
+    }
+
+    // General counters and flags
+
+    if (unit->profession == profession::BABY)
+        speed += 3000;
+
+    if (unit->flags3.bits.unk15)
+        speed /= 20;
+
+    if (unit->counters2.exhaustion >= 2000)
+    {
+        speed += 200;
+        if (unit->counters2.exhaustion >= 4000)
+        {
+            speed += 200;
+            if (unit->counters2.exhaustion >= 6000)
+                speed += 200;
+        }
+    }
+
+    if (unit->flags2.bits.gutted) speed += 2000;
+
+    if (unit->counters.soldier_mood == df::unit::T_counters::None)
+    {
+        if (unit->counters.nausea > 0) speed += 1000;
+        if (unit->counters.winded > 0) speed += 1000;
+        if (unit->counters.stunned > 0) speed += 1000;
+        if (unit->counters.dizziness > 0) speed += 1000;
+        if (unit->counters2.fever > 0) speed += 1000;
+    }
+
+    if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance)
+    {
+        if (unit->counters.pain >= 100 && unit->mood == -1)
+            speed += 1000;
+    }
+
+    // Hunger etc timers
+
+    bool is_adventure = (gamemode && *gamemode == game_mode::ADVENTURE);
+
+    if (!unit->flags3.bits.scuttle && Units::isBloodsucker(unit))
+    {
+        using namespace df::enums::misc_trait_type;
+
+        if (auto trait = Units::getMiscTrait(unit, TimeSinceSuckedBlood))
+        {
+            adjust_speed_rating(
+                speed, is_adventure, trait->value,
+                302400, 403200,                    // dwf 100; 200
+                1209600, 1209600, 1209600, 2419200 // adv 50; 75; 100; 200
+            );
+        }
+    }
+
+    adjust_speed_rating(
+        speed, is_adventure, unit->counters2.thirst_timer,
+        50000, 0x7fffffff, 172800, 172800, 172800, 345600
+    );
+    adjust_speed_rating(
+        speed, is_adventure, unit->counters2.hunger_timer,
+        75000, 0x7fffffff, 1209600, 1209600, 1209600, 2592000
+    );
+    adjust_speed_rating(
+        speed, is_adventure, unit->counters2.sleepiness_timer,
+        57600, 150000, 172800, 259200, 345600, 864000
+    );
+
+    // Activity state
+
+    if (unit->relations.draggee_id != -1) speed += 1000;
+
+    if (unit->flags1.bits.on_ground)
+        speed += 2000;
+    else if (unit->flags3.bits.on_crutch)
+    {
+        int skill = Units::getEffectiveSkill(unit, job_skill::CRUTCH_WALK);
+        speed += 2000 - 100*std::min(20, skill);
+    }
+
+    if (unit->flags1.bits.hidden_in_ambush && !Units::isMischievous(unit))
+    {
+        int skill = Units::getEffectiveSkill(unit, job_skill::SNEAK);
+        speed += 2000 - 100*std::min(20, skill);
+    }
+
+    if (unsigned(unit->counters2.paralysis-1) <= 98)
+        speed += unit->counters2.paralysis*10;
+    if (unsigned(unit->counters.webbed-1) <= 8)
+        speed += unit->counters.webbed*100;
+
+    // Muscle weight vs vascular tissue (?)
+
+    auto &attr_tissue = unit->body.physical_attr_tissues;
+    int muscle = attr_tissue[STRENGTH];
+    int blood = attr_tissue[AGILITY];
+    speed = std::max(speed*3/4, std::min(speed*3/2, int(int64_t(speed)*muscle/blood)));
+
+    // Attributes
+
+    int strength_attr = Units::getPhysicalAttrValue(unit, STRENGTH);
+    int agility_attr = Units::getPhysicalAttrValue(unit, AGILITY);
+
+    int total_attr = std::max(200, std::min(3800, strength_attr + agility_attr));
+    speed = ((total_attr-200)*(speed/2) + (3800-total_attr)*(speed*3/2))/3600;
+
+    // Stance
+
+    if (!unit->flags1.bits.on_ground && unit->status2.able_stand > 2)
+    {
+        // WTF
+        int as = unit->status2.able_stand;
+        int x = (as-1) - (as>>1);
+        int y = as - unit->status2.able_stand_impair;
+        if (unit->flags3.bits.on_crutch) y--;
+        y = y * 500 / x;
+        if (y > 0) speed += y;
+    }
+
+    // Mood
+
+    if (unit->mood == mood_type::Melancholy) speed += 8000;
+
+    // Inventory encumberance
+
+    int total_weight = calcInventoryWeight(unit);
+    int free_weight = std::max(1, muscle/10 + strength_attr*3);
+
+    if (free_weight < total_weight)
+    {
+        int delta = (total_weight - free_weight)/10 + 1;
+        if (!is_adventure)
+            delta = std::min(5000, delta);
+        speed += delta;
+    }
+
+    // skipped: unknown loop on inventory items that amounts to 0 change
+
+    if (is_adventure)
+    {
+        auto player = vector_get(world->units.active, 0);
+        if (player && player->id == unit->relations.group_leader_id)
+            speed = std::min(speed, computeMovementSpeed(player));
+    }
+
+    return std::min(10000, std::max(0, speed));
+}
+
 static bool noble_pos_compare(const Units::NoblePosition &a, const Units::NoblePosition &b)
 {
     if (a.position->precedence < b.position->precedence)
@@ -838,7 +1311,7 @@ std::string DFHack::Units::getCasteProfessionName(int race, int casteid, df::pro
 {
     std::string prof, race_prefix;
 
-    if (pid < 0 || !is_valid_enum_item(pid))
+    if (pid < (df::profession)0 || !is_valid_enum_item(pid))
         return "";
 
     bool use_race_prefix = (race >= 0 && race != df::global::ui->race_id);
diff --git a/library/modules/World.cpp b/library/modules/World.cpp
index 393e7cbfe..67b8c1236 100644
--- a/library/modules/World.cpp
+++ b/library/modules/World.cpp
@@ -285,13 +285,13 @@ PersistentDataItem World::GetPersistentData(int entry_id)
 
 PersistentDataItem World::GetPersistentData(const std::string &key, bool *added)
 {
-    *added = false;
+    if (added) *added = false;
 
     PersistentDataItem rv = GetPersistentData(key);
 
     if (!rv.isValid())
     {
-        *added = true;
+        if (added) *added = true;
         rv = AddPersistentData(key);
     }
 
@@ -300,6 +300,8 @@ PersistentDataItem World::GetPersistentData(const std::string &key, bool *added)
 
 void World::GetPersistentData(std::vector *vec, const std::string &key, bool prefix)
 {
+    vec->clear();
+
     if (!BuildPersistentCache())
         return;
 
@@ -343,8 +345,10 @@ bool World::DeletePersistentData(const PersistentDataItem &item)
 
     auto eqrange = d->persistent_index.equal_range(item.key_value);
 
-    for (auto it = eqrange.first; it != eqrange.second; ++it)
+    for (auto it2 = eqrange.first; it2 != eqrange.second; )
     {
+        auto it = it2; ++it2;
+
         if (it->second != -item.id)
             continue;
 
diff --git a/library/modules/kitchen.cpp b/library/modules/kitchen.cpp
index 4300d63df..aa235780d 100644
--- a/library/modules/kitchen.cpp
+++ b/library/modules/kitchen.cpp
@@ -114,7 +114,7 @@ void Kitchen::fillWatchMap(std::map& watchMap)
     watchMap.clear();
     for(std::size_t i = 0; i < size(); ++i)
     {
-        if(ui->kitchen.item_subtypes[i] == limitType && ui->kitchen.item_subtypes[i] == limitSubtype && ui->kitchen.exc_types[i] == limitExclusion)
+        if(ui->kitchen.item_subtypes[i] == (short)limitType && ui->kitchen.item_subtypes[i] == (short)limitSubtype && ui->kitchen.exc_types[i] == limitExclusion)
         {
             watchMap[ui->kitchen.mat_indices[i]] = (unsigned int) ui->kitchen.mat_types[i];
         }
diff --git a/library/xml b/library/xml
index 328a8dbdc..2bc8fbdf7 160000
--- a/library/xml
+++ b/library/xml
@@ -1 +1 @@
-Subproject commit 328a8dbdc7d9e1e838798abf79861cc18a387e3f
+Subproject commit 2bc8fbdf71143398817d31e06e169a01cce37c50
diff --git a/package/darwin/dfhack-run b/package/darwin/dfhack-run
index 865c8bd21..cc69db964 100755
--- a/package/darwin/dfhack-run
+++ b/package/darwin/dfhack-run
@@ -3,7 +3,6 @@
 DF_DIR=$(dirname "$0")
 cd "${DF_DIR}"
 
-export DYLD_LIBRARY_PATH=${PWD}/hack:${PWD}/libs
-export DYLD_FRAMEWORK_PATH=${PWD}/hack${PWD}/libs
+export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"./stonesense/deplibs":"./hack"
 
 exec hack/dfhack-run "$@"
diff --git a/package/darwin/fix-libs.sh b/package/darwin/fix-libs.sh
new file mode 100755
index 000000000..cff98b6a6
--- /dev/null
+++ b/package/darwin/fix-libs.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+BUILD_DIR=`pwd`
+
+echo "Fixing library dependencies in $BUILD_DIR/library"
+
+install_name_tool -change $BUILD_DIR/library/libdfhack.1.0.0.dylib @executable_path/hack/libdfhack.1.0.0.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change $BUILD_DIR/library/libdfhack-client.dylib @executable_path/hack/libdfhack-client.dylib library/libdfhack-client.dylib
+install_name_tool -change $BUILD_DIR/library/libdfhack-client.dylib @executable_path/hack/libdfhack-client.dylib library/dfhack-run
+install_name_tool -change $BUILD_DIR/depends/protobuf/libprotobuf-lite.dylib @executable_path/hack/libprotobuf-lite.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change $BUILD_DIR/depends/protobuf/libprotobuf-lite.dylib @executable_path/hack/libprotobuf-lite.dylib library/libdfhack-client.dylib
+install_name_tool -change $BUILD_DIR/depends/protobuf/libprotobuf-lite.dylib @executable_path/hack/libprotobuf-lite.dylib library/dfhack-run
+install_name_tool -change $BUILD_DIR/depends/lua/liblua.dylib @executable_path/hack/liblua.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change @executable_path/../Frameworks/SDL.framework/Versions/A/SDL @executable_path/libs/SDL.framework/Versions/A/SDL library/libdfhack.1.0.0.dylib
+install_name_tool -change /usr/local/lib/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change /opt/local/lib/i386/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change /opt/local/lib/i386/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/libdfhack-client.dylib
+install_name_tool -change /opt/local/lib/i386/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/dfhack-run
+install_name_tool -change /opt/local/lib/i386/libgcc_s.1.dylib @executable_path/libs/libgcc_s.1.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change /opt/local/lib/i386/libgcc_s.1.dylib @executable_path/libs/libgcc_s.1.dylib library/libdfhack-client.dylib
+install_name_tool -change /opt/local/lib/i386/libgcc_s.1.dylib @executable_path/libs/libgcc_s.1.dylib library/dfhack-run
\ No newline at end of file
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index a2e520178..6e207385e 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -44,6 +44,9 @@ endif()
 install(DIRECTORY lua/
         DESTINATION ${DFHACK_LUA_DESTINATION}/plugins
         FILES_MATCHING PATTERN "*.lua")
+install(DIRECTORY raw/
+        DESTINATION ${DFHACK_DATA_DESTINATION}/raw
+        FILES_MATCHING PATTERN "*.txt")
 
 # Protobuf
 FILE(GLOB PROJECT_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto)
@@ -92,7 +95,7 @@ if (BUILD_SUPPORTED)
     DFHACK_PLUGIN(seedwatch seedwatch.cpp)
     DFHACK_PLUGIN(initflags initflags.cpp)
     DFHACK_PLUGIN(stockpiles stockpiles.cpp)
-    DFHACK_PLUGIN(rename rename.cpp PROTOBUFS rename)
+    DFHACK_PLUGIN(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename)
     DFHACK_PLUGIN(jobutils jobutils.cpp)
     DFHACK_PLUGIN(workflow workflow.cpp)
     DFHACK_PLUGIN(showmood showmood.cpp)
@@ -114,6 +117,8 @@ if (BUILD_SUPPORTED)
     # this one exports functions to lua
     DFHACK_PLUGIN(burrows burrows.cpp LINK_LIBRARIES lua)
     DFHACK_PLUGIN(sort sort.cpp LINK_LIBRARIES lua)
+    DFHACK_PLUGIN(steam-engine steam-engine.cpp)
+    DFHACK_PLUGIN(power-meter power-meter.cpp LINK_LIBRARIES lua)
     # not yet. busy with other crud again...
     #DFHACK_PLUGIN(versionosd versionosd.cpp)
 endif()
diff --git a/plugins/autolabor.cpp b/plugins/autolabor.cpp
index c3a2b313e..c39b126c9 100644
--- a/plugins/autolabor.cpp
+++ b/plugins/autolabor.cpp
@@ -9,6 +9,7 @@
 #include 
 #include 
 
+#include "modules/Units.h"
 #include "modules/World.h"
 
 // DF data structure definition headers
@@ -358,11 +359,11 @@ static const dwarf_state dwarf_states[] = {
     OTHER /* DrinkBlood */,
     OTHER /* ReportCrime */,
     OTHER /* ExecuteCriminal */,
-        BUSY /* TrainAnimal */,
-        BUSY /* CarveTrack */,
-        BUSY /* PushTrackVehicle */,
-        BUSY /* PlaceTrackVehicle */,
-        BUSY /* StoreItemInVehicle */
+    BUSY /* TrainAnimal */,
+    BUSY /* CarveTrack */,
+    BUSY /* PushTrackVehicle */,
+    BUSY /* PlaceTrackVehicle */,
+    BUSY /* StoreItemInVehicle */
 };
 
 struct labor_info
@@ -397,108 +398,108 @@ static int hauler_pct = 33;
 static std::vector labor_infos;
 
 static const struct labor_default default_labor_infos[] = {
-    /* MINE */                {AUTOMATIC, true, 2, 200, 0},
-    /* HAUL_STONE */        {HAULERS, false, 1, 200, 0},
-    /* HAUL_WOOD */            {HAULERS, false, 1, 200, 0},
-    /* HAUL_BODY */            {HAULERS, false, 1, 200, 0},
-    /* HAUL_FOOD */            {HAULERS, false, 1, 200, 0},
-    /* HAUL_REFUSE */        {HAULERS, false, 1, 200, 0},
-    /* HAUL_ITEM */            {HAULERS, false, 1, 200, 0},
-    /* HAUL_FURNITURE */    {HAULERS, false, 1, 200, 0},
-    /* HAUL_ANIMAL */        {HAULERS, false, 1, 200, 0},
-    /* CLEAN */                {HAULERS, false, 1, 200, 0},
-    /* CUTWOOD */            {AUTOMATIC, true, 1, 200, 0},
-    /* CARPENTER */            {AUTOMATIC, false, 1, 200, 0},
-    /* DETAIL */            {AUTOMATIC, false, 1, 200, 0},
-    /* MASON */                {AUTOMATIC, false, 1, 200, 0},
-    /* ARCHITECT */            {AUTOMATIC, false, 1, 200, 0},
-    /* ANIMALTRAIN */        {AUTOMATIC, false, 1, 200, 0},
-    /* ANIMALCARE */        {AUTOMATIC, false, 1, 200, 0},
-    /* DIAGNOSE */            {AUTOMATIC, false, 1, 200, 0},
-    /* SURGERY */            {AUTOMATIC, false, 1, 200, 0},
-    /* BONE_SETTING */        {AUTOMATIC, false, 1, 200, 0},
-    /* SUTURING */            {AUTOMATIC, false, 1, 200, 0},
-    /* DRESSING_WOUNDS */    {AUTOMATIC, false, 1, 200, 0},
-    /* FEED_WATER_CIVILIANS */ {AUTOMATIC, false, 200, 200, 0},
-    /* RECOVER_WOUNDED */    {HAULERS, false, 1, 200, 0},
-    /* BUTCHER */            {AUTOMATIC, false, 1, 200, 0},
-    /* TRAPPER */            {AUTOMATIC, false, 1, 200, 0},
-    /* DISSECT_VERMIN */    {AUTOMATIC, false, 1, 200, 0},
-    /* LEATHER */            {AUTOMATIC, false, 1, 200, 0},
-    /* TANNER */            {AUTOMATIC, false, 1, 200, 0},
-    /* BREWER */            {AUTOMATIC, false, 1, 200, 0},
-    /* ALCHEMIST */            {AUTOMATIC, false, 1, 200, 0},
-    /* SOAP_MAKER */        {AUTOMATIC, false, 1, 200, 0},
-    /* WEAVER */            {AUTOMATIC, false, 1, 200, 0},
-    /* CLOTHESMAKER */        {AUTOMATIC, false, 1, 200, 0},
-    /* MILLER */            {AUTOMATIC, false, 1, 200, 0},
-    /* PROCESS_PLANT */        {AUTOMATIC, false, 1, 200, 0},
-    /* MAKE_CHEESE */        {AUTOMATIC, false, 1, 200, 0},
-    /* MILK */                {AUTOMATIC, false, 1, 200, 0},
-    /* COOK */                {AUTOMATIC, false, 1, 200, 0},
-    /* PLANT */                {AUTOMATIC, false, 1, 200, 0},
-    /* HERBALIST */            {AUTOMATIC, false, 1, 200, 0},
-    /* FISH */                {AUTOMATIC, false, 1, 1, 0},
-    /* CLEAN_FISH */        {AUTOMATIC, false, 1, 200, 0},
-    /* DISSECT_FISH */        {AUTOMATIC, false, 1, 200, 0},
-    /* HUNT */                {AUTOMATIC, true, 1, 1, 0},
-    /* SMELT */                {AUTOMATIC, false, 1, 200, 0},
-    /* FORGE_WEAPON */        {AUTOMATIC, false, 1, 200, 0},
-    /* FORGE_ARMOR */        {AUTOMATIC, false, 1, 200, 0},
-    /* FORGE_FURNITURE */    {AUTOMATIC, false, 1, 200, 0},
-    /* METAL_CRAFT */        {AUTOMATIC, false, 1, 200, 0},
-    /* CUT_GEM */            {AUTOMATIC, false, 1, 200, 0},
-    /* ENCRUST_GEM */        {AUTOMATIC, false, 1, 200, 0},
-    /* WOOD_CRAFT */        {AUTOMATIC, false, 1, 200, 0},
-    /* STONE_CRAFT */        {AUTOMATIC, false, 1, 200, 0},
-    /* BONE_CARVE */        {AUTOMATIC, false, 1, 200, 0},
-    /* GLASSMAKER */        {AUTOMATIC, false, 1, 200, 0},
-    /* EXTRACT_STRAND */    {AUTOMATIC, false, 1, 200, 0},
-    /* SIEGECRAFT */        {AUTOMATIC, false, 1, 200, 0},
-    /* SIEGEOPERATE */        {AUTOMATIC, false, 1, 200, 0},
-    /* BOWYER */            {AUTOMATIC, false, 1, 200, 0},
-    /* MECHANIC */            {AUTOMATIC, false, 1, 200, 0},
-    /* POTASH_MAKING */        {AUTOMATIC, false, 1, 200, 0},
-    /* LYE_MAKING */        {AUTOMATIC, false, 1, 200, 0},
-    /* DYER */                {AUTOMATIC, false, 1, 200, 0},
-    /* BURN_WOOD */            {AUTOMATIC, false, 1, 200, 0},
-    /* OPERATE_PUMP */        {AUTOMATIC, false, 1, 200, 0},
-    /* SHEARER */            {AUTOMATIC, false, 1, 200, 0},
-    /* SPINNER */            {AUTOMATIC, false, 1, 200, 0},
-    /* POTTERY */            {AUTOMATIC, false, 1, 200, 0},
-    /* GLAZING */            {AUTOMATIC, false, 1, 200, 0},
-    /* PRESSING */            {AUTOMATIC, false, 1, 200, 0},
-    /* BEEKEEPING */        {AUTOMATIC, false, 1, 1, 0}, // reduce risk of stuck beekeepers (see http://www.bay12games.com/dwarves/mantisbt/view.php?id=3981)
-    /* WAX_WORKING */        {AUTOMATIC, false, 1, 200, 0},
+    /* MINE */                  {AUTOMATIC, true, 2, 200, 0},
+    /* HAUL_STONE */            {HAULERS, false, 1, 200, 0},
+    /* HAUL_WOOD */             {HAULERS, false, 1, 200, 0},
+    /* HAUL_BODY */             {HAULERS, false, 1, 200, 0},
+    /* HAUL_FOOD */             {HAULERS, false, 1, 200, 0},
+    /* HAUL_REFUSE */           {HAULERS, false, 1, 200, 0},
+    /* HAUL_ITEM */             {HAULERS, false, 1, 200, 0},
+    /* HAUL_FURNITURE */        {HAULERS, false, 1, 200, 0},
+    /* HAUL_ANIMAL */           {HAULERS, false, 1, 200, 0},
+    /* CLEAN */                 {HAULERS, false, 1, 200, 0},
+    /* CUTWOOD */               {AUTOMATIC, true, 1, 200, 0},
+    /* CARPENTER */             {AUTOMATIC, false, 1, 200, 0},
+    /* DETAIL */                {AUTOMATIC, false, 1, 200, 0},
+    /* MASON */                 {AUTOMATIC, false, 1, 200, 0},
+    /* ARCHITECT */             {AUTOMATIC, false, 1, 200, 0},
+    /* ANIMALTRAIN */           {AUTOMATIC, false, 1, 200, 0},
+    /* ANIMALCARE */            {AUTOMATIC, false, 1, 200, 0},
+    /* DIAGNOSE */              {AUTOMATIC, false, 1, 200, 0},
+    /* SURGERY */               {AUTOMATIC, false, 1, 200, 0},
+    /* BONE_SETTING */          {AUTOMATIC, false, 1, 200, 0},
+    /* SUTURING */              {AUTOMATIC, false, 1, 200, 0},
+    /* DRESSING_WOUNDS */       {AUTOMATIC, false, 1, 200, 0},
+    /* FEED_WATER_CIVILIANS */  {AUTOMATIC, false, 200, 200, 0},
+    /* RECOVER_WOUNDED */       {HAULERS, false, 1, 200, 0},
+    /* BUTCHER */               {AUTOMATIC, false, 1, 200, 0},
+    /* TRAPPER */               {AUTOMATIC, false, 1, 200, 0},
+    /* DISSECT_VERMIN */        {AUTOMATIC, false, 1, 200, 0},
+    /* LEATHER */               {AUTOMATIC, false, 1, 200, 0},
+    /* TANNER */                {AUTOMATIC, false, 1, 200, 0},
+    /* BREWER */                {AUTOMATIC, false, 1, 200, 0},
+    /* ALCHEMIST */             {AUTOMATIC, false, 1, 200, 0},
+    /* SOAP_MAKER */            {AUTOMATIC, false, 1, 200, 0},
+    /* WEAVER */                {AUTOMATIC, false, 1, 200, 0},
+    /* CLOTHESMAKER */          {AUTOMATIC, false, 1, 200, 0},
+    /* MILLER */                {AUTOMATIC, false, 1, 200, 0},
+    /* PROCESS_PLANT */         {AUTOMATIC, false, 1, 200, 0},
+    /* MAKE_CHEESE */           {AUTOMATIC, false, 1, 200, 0},
+    /* MILK */                  {AUTOMATIC, false, 1, 200, 0},
+    /* COOK */                  {AUTOMATIC, false, 1, 200, 0},
+    /* PLANT */                 {AUTOMATIC, false, 1, 200, 0},
+    /* HERBALIST */             {AUTOMATIC, false, 1, 200, 0},
+    /* FISH */                  {AUTOMATIC, false, 1, 1, 0},
+    /* CLEAN_FISH */            {AUTOMATIC, false, 1, 200, 0},
+    /* DISSECT_FISH */          {AUTOMATIC, false, 1, 200, 0},
+    /* HUNT */                  {AUTOMATIC, true, 1, 1, 0},
+    /* SMELT */                 {AUTOMATIC, false, 1, 200, 0},
+    /* FORGE_WEAPON */          {AUTOMATIC, false, 1, 200, 0},
+    /* FORGE_ARMOR */           {AUTOMATIC, false, 1, 200, 0},
+    /* FORGE_FURNITURE */       {AUTOMATIC, false, 1, 200, 0},
+    /* METAL_CRAFT */           {AUTOMATIC, false, 1, 200, 0},
+    /* CUT_GEM */               {AUTOMATIC, false, 1, 200, 0},
+    /* ENCRUST_GEM */           {AUTOMATIC, false, 1, 200, 0},
+    /* WOOD_CRAFT */            {AUTOMATIC, false, 1, 200, 0},
+    /* STONE_CRAFT */           {AUTOMATIC, false, 1, 200, 0},
+    /* BONE_CARVE */            {AUTOMATIC, false, 1, 200, 0},
+    /* GLASSMAKER */            {AUTOMATIC, false, 1, 200, 0},
+    /* EXTRACT_STRAND */        {AUTOMATIC, false, 1, 200, 0},
+    /* SIEGECRAFT */            {AUTOMATIC, false, 1, 200, 0},
+    /* SIEGEOPERATE */          {AUTOMATIC, false, 1, 200, 0},
+    /* BOWYER */                {AUTOMATIC, false, 1, 200, 0},
+    /* MECHANIC */              {AUTOMATIC, false, 1, 200, 0},
+    /* POTASH_MAKING */         {AUTOMATIC, false, 1, 200, 0},
+    /* LYE_MAKING */            {AUTOMATIC, false, 1, 200, 0},
+    /* DYER */                  {AUTOMATIC, false, 1, 200, 0},
+    /* BURN_WOOD */             {AUTOMATIC, false, 1, 200, 0},
+    /* OPERATE_PUMP */          {AUTOMATIC, false, 1, 200, 0},
+    /* SHEARER */               {AUTOMATIC, false, 1, 200, 0},
+    /* SPINNER */               {AUTOMATIC, false, 1, 200, 0},
+    /* POTTERY */               {AUTOMATIC, false, 1, 200, 0},
+    /* GLAZING */               {AUTOMATIC, false, 1, 200, 0},
+    /* PRESSING */              {AUTOMATIC, false, 1, 200, 0},
+    /* BEEKEEPING */            {AUTOMATIC, false, 1, 1, 0}, // reduce risk of stuck beekeepers (see http://www.bay12games.com/dwarves/mantisbt/view.php?id=3981)
+    /* WAX_WORKING */           {AUTOMATIC, false, 1, 200, 0},
     /* PUSH_HAUL_VEHICLES */    {HAULERS, false, 1, 200, 0}
 };
 
 static const int responsibility_penalties[] = {
-        0,      /* LAW_MAKING */
-        0,      /* LAW_ENFORCEMENT */
-        3000,    /* RECEIVE_DIPLOMATS */
-        0,      /* MEET_WORKERS */
-        1000,    /* MANAGE_PRODUCTION */
-        3000,   /* TRADE */
-        1000,    /* ACCOUNTING */
-        0,      /* ESTABLISH_COLONY_TRADE_AGREEMENTS */
-        0,      /* MAKE_INTRODUCTIONS */
-        0,      /* MAKE_PEACE_AGREEMENTS */
-        0,      /* MAKE_TOPIC_AGREEMENTS */
-        0,      /* COLLECT_TAXES */
-        0,      /* ESCORT_TAX_COLLECTOR */
-        0,      /* EXECUTIONS */
-        0,      /* TAME_EXOTICS */
-        0,      /* RELIGION */
-        0,      /* ATTACK_ENEMIES */
-        0,      /* PATROL_TERRITORY */
-        0,      /* MILITARY_GOALS */
-        0,      /* MILITARY_STRATEGY */
-        0,      /* UPGRADE_SQUAD_EQUIPMENT */
-        0,      /* EQUIPMENT_MANIFESTS */
-        0,      /* SORT_AMMUNITION */
-        0,      /* BUILD_MORALE */
-        5000    /* HEALTH_MANAGEMENT */
+    0,      /* LAW_MAKING */
+    0,      /* LAW_ENFORCEMENT */
+    3000,   /* RECEIVE_DIPLOMATS */
+    0,      /* MEET_WORKERS */
+    1000,   /* MANAGE_PRODUCTION */
+    3000,   /* TRADE */
+    1000,   /* ACCOUNTING */
+    0,      /* ESTABLISH_COLONY_TRADE_AGREEMENTS */
+    0,      /* MAKE_INTRODUCTIONS */
+    0,      /* MAKE_PEACE_AGREEMENTS */
+    0,      /* MAKE_TOPIC_AGREEMENTS */
+    0,      /* COLLECT_TAXES */
+    0,      /* ESCORT_TAX_COLLECTOR */
+    0,      /* EXECUTIONS */
+    0,      /* TAME_EXOTICS */
+    0,      /* RELIGION */
+    0,      /* ATTACK_ENEMIES */
+    0,      /* PATROL_TERRITORY */
+    0,      /* MILITARY_GOALS */
+    0,      /* MILITARY_STRATEGY */
+    0,      /* UPGRADE_SQUAD_EQUIPMENT */
+    0,      /* EQUIPMENT_MANIFESTS */
+    0,      /* SORT_AMMUNITION */
+    0,      /* BUILD_MORALE */
+    5000    /* HEALTH_MANAGEMENT */
 };
 
 struct dwarf_info_t
@@ -537,7 +538,7 @@ static void cleanup_state()
     labor_infos.clear();
 }
 
-static void reset_labor(df::enums::unit_labor::unit_labor labor)
+static void reset_labor(df::unit_labor labor)
 {
     labor_infos[labor].set_minimum_dwarfs(default_labor_infos[labor].minimum_dwarfs);
     labor_infos[labor].set_maximum_dwarfs(default_labor_infos[labor].maximum_dwarfs);
@@ -576,7 +577,7 @@ static void init_state()
     for (auto p = items.begin(); p != items.end(); p++)
     {
         string key = p->key();
-        df::enums::unit_labor::unit_labor labor = (df::enums::unit_labor::unit_labor) atoi(key.substr(strlen("autolabor/labors/")).c_str());
+        df::unit_labor labor = (df::unit_labor) atoi(key.substr(strlen("autolabor/labors/")).c_str());
         if (labor >= 0 && labor <= labor_infos.size())
         {
             labor_infos[labor].config = *p;
@@ -597,7 +598,7 @@ static void init_state()
 
         labor_infos[i].is_exclusive = default_labor_infos[i].is_exclusive;
         labor_infos[i].active_dwarfs = 0;
-        reset_labor((df::enums::unit_labor::unit_labor) i);
+        reset_labor((df::unit_labor) i);
     }
 
     generate_labor_to_skill_map();
@@ -611,12 +612,12 @@ static void generate_labor_to_skill_map()
     // Generate labor -> skill mapping
 
     for (int i = 0; i <= ENUM_LAST_ITEM(unit_labor); i++)
-        labor_to_skill[i] = df::enums::job_skill::NONE;
+        labor_to_skill[i] = job_skill::NONE;
 
     FOR_ENUM_ITEMS(job_skill, skill)
     {
         int labor = ENUM_ATTR(job_skill, labor, skill);
-        if (labor != df::enums::unit_labor::NONE)
+        if (labor != unit_labor::NONE)
         {
             /*
             assert(labor >= 0);
@@ -779,7 +780,7 @@ static void assign_labor(unit_labor::unit_labor labor,
 
             int value = dwarf_info[dwarf].mastery_penalty;
 
-            if (skill != df::enums::job_skill::NONE)
+            if (skill != job_skill::NONE)
             {
                 int skill_level = 0;
                 int skill_experience = 0;
@@ -843,9 +844,9 @@ static void assign_labor(unit_labor::unit_labor labor,
         int max_dwarfs = labor_infos[labor].maximum_dwarfs();
 
         // Special - don't assign hunt without a butchers, or fish without a fishery
-        if (df::enums::unit_labor::HUNT == labor && !has_butchers)
+        if (unit_labor::HUNT == labor && !has_butchers)
             min_dwarfs = max_dwarfs = 0;
-        if (df::enums::unit_labor::FISH == labor && !has_fishery)
+        if (unit_labor::FISH == labor && !has_fishery)
             min_dwarfs = max_dwarfs = 0;
 
         bool want_idle_dwarf = true;
@@ -956,15 +957,15 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
     {
         df::building *build = world->buildings.all[i];
         auto type = build->getType();
-        if (df::enums::building_type::Workshop == type)
+        if (building_type::Workshop == type)
         {
-            auto subType = build->getSubtype();
-            if (df::enums::workshop_type::Butchers == subType)
+            df::workshop_type subType = (df::workshop_type)build->getSubtype();
+            if (workshop_type::Butchers == subType)
                 has_butchers = true;
-            if (df::enums::workshop_type::Fishery == subType)
+            if (workshop_type::Fishery == subType)
                 has_fishery = true;
         }
-        else if (df::enums::building_type::TradeDepot == type)
+        else if (building_type::TradeDepot == type)
         {
             df::building_tradedepotst* depot = (df::building_tradedepotst*) build;
             trader_requested = depot->trade_flags.bits.trader_requested;
@@ -978,11 +979,10 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
         }
     }
 
-    for (int i = 0; i < world->units.all.size(); ++i)
+    for (int i = 0; i < world->units.active.size(); ++i)
     {
-        df::unit* cre = world->units.all[i];
-        if (cre->race == race && cre->civ_id == civ && !cre->flags1.bits.marauder && !cre->flags1.bits.diplomat && !cre->flags1.bits.merchant &&
-            !cre->flags1.bits.dead && !cre->flags1.bits.forest)
+        df::unit* cre = world->units.active[i];
+        if (Units::isCitizen(cre))
         {
             if (cre->burrows.size() > 0)
                 continue;        // dwarfs assigned to burrows are skipped entirely
@@ -1003,9 +1003,6 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
     {
         dwarf_info[dwarf].single_labor = -1;
 
-//        assert(dwarfs[dwarf]->status.souls.size() > 0);
-//      assert fails can cause DF to crash, so don't do that
-
         if (dwarfs[dwarf]->status.souls.size() <= 0)
             continue;
 
@@ -1076,7 +1073,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
 
             // Track total & highest skill among normal/medical skills. (We don't care about personal or social skills.)
 
-            if (skill_class != df::enums::job_skill_class::Normal && skill_class != df::enums::job_skill_class::Medical)
+            if (skill_class != job_skill_class::Normal && skill_class != job_skill_class::Medical)
                 continue;
 
             if (dwarf_info[dwarf].highest_skill < skill_level)
@@ -1093,16 +1090,11 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
         dwarf_info[dwarf].mastery_penalty -= 10 * dwarf_info[dwarf].total_skill;
         dwarf_info[dwarf].mastery_penalty -= dwarf_info[dwarf].noble_penalty;
 
-        for (int labor = ENUM_FIRST_ITEM(unit_labor); labor <= ENUM_LAST_ITEM(unit_labor); labor++)
+        FOR_ENUM_ITEMS(unit_labor, labor)
         {
-            if (labor == df::enums::unit_labor::NONE)
+            if (labor == unit_labor::NONE)
                 continue;
 
-            /*
-            assert(labor >= 0);
-            assert(labor < ARRAY_COUNT(labor_infos));
-            */
-
             if (labor_infos[labor].is_exclusive && dwarfs[dwarf]->status.labors[labor])
                 dwarf_info[dwarf].mastery_penalty -= 100;
         }
@@ -1120,15 +1112,13 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
 
         for (auto p = dwarfs[dwarf]->status.misc_traits.begin(); p < dwarfs[dwarf]->status.misc_traits.end(); p++)
         {
-            // 7 / 0x7 = Newly arrived migrant, will not work yet
-            // 17 / 0x11 = On break
-            if ((*p)->id == 0x07 || (*p)->id == 0x11)
+            if ((*p)->id == misc_trait_type::Migrant || (*p)->id == misc_trait_type::OnBreak)
                 is_on_break = true;
         }
 
-        if (dwarfs[dwarf]->profession == df::enums::profession::BABY ||
-            dwarfs[dwarf]->profession == df::enums::profession::CHILD ||
-            dwarfs[dwarf]->profession == df::enums::profession::DRUNK)
+        if (dwarfs[dwarf]->profession == profession::BABY ||
+            dwarfs[dwarf]->profession == profession::CHILD ||
+            dwarfs[dwarf]->profession == profession::DRUNK)
         {
             dwarf_info[dwarf].state = CHILD;
         }
@@ -1146,18 +1136,13 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
         else
         {
             int job = dwarfs[dwarf]->job.current_job->job_type;
-
-            /*
-            assert(job >= 0);
-            assert(job < ARRAY_COUNT(dwarf_states));
-            */
-                        if (job >= 0 && job < ARRAY_COUNT(dwarf_states))
-                            dwarf_info[dwarf].state = dwarf_states[job];
-                        else
-                        {
-                            out.print("Dwarf %i \"%s\" has unknown job %i\n", dwarf, dwarfs[dwarf]->name.first_name.c_str(), job);
-                            dwarf_info[dwarf].state = OTHER;
-                        }
+            if (job >= 0 && job < ARRAY_COUNT(dwarf_states))
+                dwarf_info[dwarf].state = dwarf_states[job];
+            else
+            {
+                out.print("Dwarf %i \"%s\" has unknown job %i\n", dwarf, dwarfs[dwarf]->name.first_name.c_str(), job);
+                dwarf_info[dwarf].state = OTHER;
+            }
         }
 
         state_count[dwarf_info[dwarf].state]++;
@@ -1170,14 +1155,9 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
 
     FOR_ENUM_ITEMS(unit_labor, labor)
     {
-        if (labor == df::enums::unit_labor::NONE)
+        if (labor == unit_labor::NONE)
             continue;
 
-        /*
-        assert(labor >= 0);
-        assert(labor < ARRAY_COUNT(labor_infos));
-        */
-
         labor_infos[labor].active_dwarfs = 0;
 
         labors.push_back(labor);
@@ -1217,11 +1197,6 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
     {
         auto labor = *lp;
 
-        /*
-        assert(labor >= 0);
-        assert(labor < ARRAY_COUNT(labor_infos));
-        */
-
         assign_labor(labor, n_dwarfs, dwarf_info, trader_requested, dwarfs, has_butchers, has_fishery, out);
     }
 
@@ -1241,7 +1216,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
         {
             FOR_ENUM_ITEMS(unit_labor, labor)
             {
-                if (labor == df::enums::unit_labor::NONE)
+                if (labor == unit_labor::NONE)
                     continue;
                 if (labor_infos[labor].mode() != HAULERS)
                     continue;
@@ -1264,14 +1239,9 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
 
     FOR_ENUM_ITEMS(unit_labor, labor)
     {
-        if (labor == df::enums::unit_labor::NONE)
+        if (labor == unit_labor::NONE)
             continue;
 
-        /*
-        assert(labor >= 0);
-        assert(labor < ARRAY_COUNT(labor_infos));
-        */
-
         if (labor_infos[labor].mode() != HAULERS)
             continue;
 
@@ -1311,7 +1281,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
     return CR_OK;
 }
 
-void print_labor (df::enums::unit_labor::unit_labor labor, color_ostream &out)
+void print_labor (df::unit_labor labor, color_ostream &out)
 {
     string labor_name = ENUM_KEY_STR(unit_labor, labor);
     out << labor_name << ": ";
@@ -1358,7 +1328,6 @@ command_result autolabor (color_ostream &out, std::vector  & parame
 
         return CR_OK;
     }
-
     else if (parameters.size() == 2 && parameters[0] == "haulpct")
     {
         if (!enable_autolabor)
@@ -1371,15 +1340,15 @@ command_result autolabor (color_ostream &out, std::vector  & parame
         hauler_pct = pct;
         return CR_OK;
     }
-    else if (parameters.size() == 2 || parameters.size() == 3) {
-
+    else if (parameters.size() == 2 || parameters.size() == 3)
+    {
         if (!enable_autolabor)
         {
             out << "Error: The plugin is not enabled." << endl;
             return CR_FAILURE;
         }
 
-        df::enums::unit_labor::unit_labor labor = df::enums::unit_labor::NONE;
+        df::unit_labor labor = unit_labor::NONE;
 
         FOR_ENUM_ITEMS(unit_labor, test_labor)
         {
@@ -1387,7 +1356,7 @@ command_result autolabor (color_ostream &out, std::vector  & parame
                 labor = test_labor;
         }
 
-        if (labor == df::enums::unit_labor::NONE)
+        if (labor == unit_labor::NONE)
         {
             out.printerr("Could not find labor %s.\n", parameters[0].c_str());
             return CR_WRONG_USAGE;
@@ -1430,7 +1399,8 @@ command_result autolabor (color_ostream &out, std::vector  & parame
 
         return CR_OK;
     }
-    else if (parameters.size() == 1 && parameters[0] == "reset-all") {
+    else if (parameters.size() == 1 && parameters[0] == "reset-all")
+    {
         if (!enable_autolabor)
         {
             out << "Error: The plugin is not enabled." << endl;
@@ -1439,12 +1409,13 @@ command_result autolabor (color_ostream &out, std::vector  & parame
 
         for (int i = 0; i < labor_infos.size(); i++)
         {
-            reset_labor((df::enums::unit_labor::unit_labor) i);
+            reset_labor((df::unit_labor) i);
         }
         out << "All labors reset." << endl;
         return CR_OK;
     }
-    else if (parameters.size() == 1 && parameters[0] == "list" || parameters[0] == "status") {
+    else if (parameters.size() == 1 && parameters[0] == "list" || parameters[0] == "status")
+    {
         if (!enable_autolabor)
         {
             out << "Error: The plugin is not enabled." << endl;
@@ -1467,7 +1438,7 @@ command_result autolabor (color_ostream &out, std::vector  & parame
         {
             FOR_ENUM_ITEMS(unit_labor, labor)
             {
-                if (labor == df::enums::unit_labor::NONE)
+                if (labor == unit_labor::NONE)
                     continue;
 
                 print_labor(labor, out);
@@ -1571,7 +1542,7 @@ static int stockcheck(color_ostream &out, vector  & parameters)
     {
         df::building *build = world->buildings.all[i];
         auto type = build->getType();
-        if (df::enums::building_type::Stockpile == type)
+        if (building_type::Stockpile == type)
         {
             df::building_stockpilest *sp = virtual_cast(build);
             StockpileInfo *spi = new StockpileInfo(sp);
@@ -1580,7 +1551,7 @@ static int stockcheck(color_ostream &out, vector  & parameters)
 
     }
 
-    std::vector &items = world->items.other[df::enums::items_other_id::ANY_FREE];
+    std::vector &items = world->items.other[items_other_id::ANY_FREE];
 
     // Precompute a bitmask with the bad flags
     df::item_flags bad_flags;
@@ -1602,13 +1573,13 @@ static int stockcheck(color_ostream &out, vector  & parameters)
         // we really only care about MEAT, FISH, FISH_RAW, PLANT, CHEESE, FOOD, and EGG
 
         df::item_type typ = item->getType();
-        if (typ != df::enums::item_type::MEAT &&
-            typ != df::enums::item_type::FISH &&
-            typ != df::enums::item_type::FISH_RAW &&
-            typ != df::enums::item_type::PLANT &&
-            typ != df::enums::item_type::CHEESE &&
-            typ != df::enums::item_type::FOOD &&
-            typ != df::enums::item_type::EGG)
+        if (typ != item_type::MEAT &&
+            typ != item_type::FISH &&
+            typ != item_type::FISH_RAW &&
+            typ != item_type::PLANT &&
+            typ != item_type::CHEESE &&
+            typ != item_type::FOOD &&
+            typ != item_type::EGG)
             continue;
 
         df::item *container = 0;
@@ -1673,11 +1644,11 @@ static int stockcheck(color_ostream &out, vector  & parameters)
 
         if (building) {
             df::building_type btype = building->getType();
-            if (btype == df::enums::building_type::TradeDepot ||
-                btype == df::enums::building_type::Wagon)
+            if (btype == building_type::TradeDepot ||
+                btype == building_type::Wagon)
                 continue; // items in trade depot or the embark wagon do not rot
 
-            if (typ == df::enums::item_type::EGG && btype ==df::enums::building_type::NestBox)
+            if (typ == item_type::EGG && btype ==building_type::NestBox)
                 continue; // eggs in nest box do not rot
         }
 
diff --git a/plugins/cleaners.cpp b/plugins/cleaners.cpp
index de204f611..c0301de7b 100644
--- a/plugins/cleaners.cpp
+++ b/plugins/cleaners.cpp
@@ -50,12 +50,12 @@ command_result cleanmap (color_ostream &out, bool snow, bool mud)
             // filter snow
             if(!snow
                 && spatter->mat_type == builtin_mats::WATER
-                && spatter->mat_state == matter_state::Powder)
+                && spatter->mat_state == (short)matter_state::Powder)
                 continue;
             // filter mud
             if(!mud
                 && spatter->mat_type == builtin_mats::MUD
-                && spatter->mat_state == matter_state::Solid)
+                && spatter->mat_state == (short)matter_state::Solid)
                 continue;
 
             delete evt;
diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp
index c1521b8de..cd01fd616 100644
--- a/plugins/cleanowned.cpp
+++ b/plugins/cleanowned.cpp
@@ -116,7 +116,7 @@ command_result df_cleanowned (color_ostream &out, vector  & parameters)
         }
         else if (item->flags.bits.on_ground)
         {
-            int32_t type = item->getType();
+            df::item_type type = item->getType();
 	    if(type == item_type::MEAT ||
                type == item_type::FISH ||
                type == item_type::VERMIN ||
diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt
index 134d5cb67..39e8f7b60 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(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
 IF(UNIX)
 DFHACK_PLUGIN(ref-index ref-index.cpp)
 ENDIF()
diff --git a/plugins/devel/siege-engine.cpp b/plugins/devel/siege-engine.cpp
new file mode 100644
index 000000000..a41bfe5f7
--- /dev/null
+++ b/plugins/devel/siege-engine.cpp
@@ -0,0 +1,1649 @@
+#include "Core.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include "df/graphic.h"
+#include "df/building_siegeenginest.h"
+#include "df/builtin_mats.h"
+#include "df/world.h"
+#include "df/buildings_other_id.h"
+#include "df/job.h"
+#include "df/building_drawbuffer.h"
+#include "df/ui.h"
+#include "df/viewscreen_dwarfmodest.h"
+#include "df/ui_build_selector.h"
+#include "df/flow_info.h"
+#include "df/report.h"
+#include "df/proj_itemst.h"
+#include "df/unit.h"
+#include "df/unit_soul.h"
+#include "df/unit_skill.h"
+#include "df/physical_attribute_type.h"
+#include "df/creature_raw.h"
+#include "df/caste_raw.h"
+#include "df/caste_raw_flags.h"
+#include "df/assumed_identity.h"
+#include "df/game_mode.h"
+#include "df/unit_misc_trait.h"
+#include "df/job.h"
+#include "df/job_item.h"
+#include "df/item.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 coord_range;
+
+static void set_range(coord_range *target, df::coord p1, df::coord p2)
+{
+    if (!p1.isValid() || !p2.isValid())
+    {
+        *target = coord_range();
+    }
+    else
+    {
+        target->first.x = std::min(p1.x, p2.x);
+        target->first.y = std::min(p1.y, p2.y);
+        target->first.z = std::min(p1.z, p2.z);
+        target->second.x = std::max(p1.x, p2.x);
+        target->second.y = std::max(p1.y, p2.y);
+        target->second.z = std::max(p1.z, p2.z);
+    }
+}
+
+static bool is_range_valid(const coord_range &target)
+{
+    return target.first.isValid() && target.second.isValid();
+}
+
+static bool is_in_range(const coord_range &target, df::coord pos)
+{
+    return target.first.isValid() && target.second.isValid() &&
+           target.first.x <= pos.x && pos.x <= target.second.x &&
+           target.first.y <= pos.y && pos.y <= target.second.y &&
+           target.first.z <= pos.z && pos.z <= target.second.z;
+}
+
+static std::pair get_engine_range(df::building_siegeenginest *bld)
+{
+    if (bld->type == siegeengine_type::Ballista)
+        return std::make_pair(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 fire_range;
+
+    coord_range target;
+
+    df::job_item_vector_id ammo_vector_id;
+    df::item_type ammo_item_type;
+
+    int operator_id, operator_frame;
+
+    std::set stockpiles;
+    df::stockpile_links links;
+    df::workshop_profile profile;
+
+    bool hasTarget() { return is_range_valid(target); }
+    bool onTarget(df::coord pos) { return is_in_range(target, pos); }
+    df::coord getTargetSize() { return target.second - target.first; }
+
+    bool isInRange(int dist) {
+        return dist >= fire_range.first && dist <= fire_range.second;
+    }
+};
+
+static std::map engines;
+static std::map coord_engines;
+
+static EngineInfo *find_engine(df::building *bld, bool create = false)
+{
+    auto ebld = strict_virtual_cast(bld);
+    if (!ebld)
+        return NULL;
+
+    auto &obj = engines[bld];
+
+    if (obj)
+    {
+        obj->bld = ebld;
+        return obj;
+    }
+
+    if (!create)
+        return NULL;
+
+    obj = new EngineInfo();
+
+    obj->id = bld->id;
+    obj->bld = ebld;
+    obj->center = df::coord(bld->centerx, bld->centery, bld->z);
+    obj->building_rect = coord_range(
+        df::coord(bld->x1, bld->y1, bld->z),
+        df::coord(bld->x2, bld->y2, bld->z)
+    );
+    obj->is_catapult = (ebld->type == siegeengine_type::Catapult);
+    obj->proj_speed = 2;
+    obj->hit_delay = 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(L, idx);
+
+    auto engine = find_engine(bld, create);
+    if (!engine && !silent)
+        luaL_error(L, "no such engine");
+
+    return engine;
+}
+
+static EngineInfo *find_engine(df::coord pos)
+{
+    auto engine = find_engine(coord_engines[pos]);
+
+    if (engine)
+    {
+        auto bld0 = df::building::find(engine->id);
+        auto bld = strict_virtual_cast(bld0);
+        if (!bld)
+            return NULL;
+
+        engine->bld = bld;
+    }
+
+    return engine;
+}
+
+/*
+ * Configuration management
+ */
+
+static void clear_engines()
+{
+    for (auto it = engines.begin(); it != engines.end(); ++it)
+        delete it->second;
+    engines.clear();
+    coord_engines.clear();
+}
+
+static void load_engines()
+{
+    clear_engines();
+
+    auto pworld = Core::getInstance().getWorld();
+    std::vector 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 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 path;
+
+    struct Hit {
+        UnitPath *path;
+        df::coord pos;
+        int dist;
+        float time, lmargin, rmargin;
+    };
+
+    static std::map cache;
+
+    static UnitPath *get(df::unit *unit)
+    {
+        auto &cv = cache[unit];
+        if (!cv) cv = new UnitPath(unit);
+        return cv;
+    };
+
+    UnitPath(df::unit *unit) : unit(unit)
+    {
+        if (unit->flags1.bits.rider)
+        {
+            auto mount = df::unit::find(unit->relations.rider_mount_id);
+
+            if (mount)
+            {
+                path = get(mount)->path;
+                return;
+            }
+        }
+
+        df::coord pos = unit->pos;
+        df::coord dest = unit->path.dest;
+        auto &upath = unit->path.path;
+
+        if (dest.isValid() && !upath.x.empty())
+        {
+            float time = unit->counters.job_counter+0.5f;
+            float speed = Units::computeMovementSpeed(unit)/100.0f;
+
+            for (size_t i = 0; i < upath.size(); i++)
+            {
+                df::coord new_pos = upath[i];
+                if (new_pos == pos)
+                    continue;
+
+                float delay = speed;
+                if (new_pos.x != pos.x && new_pos.y != pos.y)
+                    delay *= 362.0/256.0;
+
+                path[time] = pos;
+                pos = new_pos;
+                time += delay + 1;
+            }
+        }
+
+        path[MAX_TIME] = pos;
+    }
+
+    void get_margin(std::map::iterator &it, float time, float *lmargin, float *rmargin)
+    {
+        auto it2 = it;
+        *lmargin = (it == path.begin()) ? MAX_TIME : time - (--it2)->first;
+        *rmargin = (it->first == MAX_TIME) ? MAX_TIME : it->first - time;
+    }
+
+    df::coord posAtTime(float time, float *lmargin = NULL, float *rmargin = NULL)
+    {
+        CHECK_INVALID_ARGUMENT(time < MAX_TIME);
+
+        auto it = path.upper_bound(time);
+        if (lmargin)
+            get_margin(it, time, lmargin, rmargin);
+        return it->second;
+    }
+
+    bool findHits(EngineInfo *engine, std::vector *hit_points, float bias)
+    {
+        df::coord origin = engine->center;
+
+        Hit info;
+        info.path = this;
+
+        for (auto it = path.begin(); it != path.end(); ++it)
+        {
+            info.pos = it->second;
+            info.dist = point_distance(origin - info.pos);
+            info.time = float(info.dist)*(engine->proj_speed+1) + engine->hit_delay + bias;
+            get_margin(it, info.time, &info.lmargin, &info.rmargin);
+
+            if (info.lmargin > 0 && info.rmargin > 0)
+            {
+                if (engine->onTarget(info.pos) && engine->isInRange(info.dist))
+                    hit_points->push_back(info);
+            }
+        }
+
+        return !hit_points->empty();
+    }
+};
+
+std::map UnitPath::cache;
+
+static void push_margin(lua_State *L, float margin)
+{
+    if (margin == MAX_TIME)
+        lua_pushnil(L);
+    else
+        lua_pushnumber(L, margin);
+}
+
+static int traceUnitPath(lua_State *L)
+{
+    auto unit = Lua::CheckDFObject(L, 1);
+
+    CHECK_NULL_POINTER(unit);
+
+    size_t idx = 1;
+    auto info = UnitPath::get(unit);
+    lua_createtable(L, info->path.size(), 0);
+
+    float last_time = 0.0f;
+    for (auto it = info->path.begin(); it != info->path.end(); ++it)
+    {
+        Lua::Push(L, it->second);
+        if (idx > 1)
+        {
+            lua_pushnumber(L, last_time);
+            lua_setfield(L, -2, "from");
+        }
+        if (idx < info->path.size())
+        {
+            lua_pushnumber(L, it->first);
+            lua_setfield(L, -2, "to");
+        }
+        lua_rawseti(L, -2, idx++);
+        last_time = it->first;
+    }
+
+    return 1;
+}
+
+static int unitPosAtTime(lua_State *L)
+{
+    auto unit = Lua::CheckDFObject(L, 1);
+    float time = luaL_checknumber(L, 2);
+
+    CHECK_NULL_POINTER(unit);
+
+    float lmargin, rmargin;
+    auto info = UnitPath::get(unit);
+
+    Lua::Push(L, info->posAtTime(time, &lmargin, &rmargin));
+    push_margin(L, lmargin);
+    push_margin(L, rmargin);
+    return 3;
+}
+
+static bool canTargetUnit(df::unit *unit)
+{
+    CHECK_NULL_POINTER(unit);
+
+    if (unit->flags1.bits.dead ||
+        unit->flags3.bits.ghostly ||
+        unit->flags1.bits.caged ||
+        unit->flags1.bits.hidden_in_ambush)
+        return false;
+
+    return true;
+}
+
+static void proposeUnitHits(EngineInfo *engine, std::vector *hits, float bias)
+{
+    auto &active = world->units.active;
+
+    for (size_t i = 0; i < active.size(); i++)
+    {
+        auto unit = active[i];
+
+        if (!canTargetUnit(unit))
+            continue;
+
+        UnitPath::get(unit)->findHits(engine, hits, bias);
+    }
+}
+
+static int proposeUnitHits(lua_State *L)
+{
+    auto engine = find_engine(L, 1);
+    float bias = luaL_optnumber(L, 2, 0);
+
+    if (!engine->hasTarget())
+        luaL_error(L, "target not set");
+
+    std::vector hits;
+    proposeUnitHits(engine, &hits, bias);
+
+    lua_createtable(L, hits.size(), 0);
+
+    for (size_t i = 0; i < hits.size(); i++)
+    {
+        auto &hit = hits[i];
+        lua_createtable(L, 0, 6);
+        Lua::SetField(L, hit.path->unit, -1, "unit");
+        Lua::SetField(L, hit.pos, -1, "pos");
+        Lua::SetField(L, hit.dist, -1, "dist");
+        Lua::SetField(L, hit.time, -1, "time");
+        push_margin(L, hit.lmargin);          lua_setfield(L, -2, "lmargin");
+        push_margin(L, hit.rmargin);          lua_setfield(L, -2, "rmargin");
+        lua_rawseti(L, -2, i+1);
+    }
+
+    return 1;
+}
+
+/*
+ * 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 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  &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/jobutils.cpp b/plugins/jobutils.cpp
index 24ad4170e..dbfe26b90 100644
--- a/plugins/jobutils.cpp
+++ b/plugins/jobutils.cpp
@@ -372,7 +372,7 @@ static command_result job_cmd(color_ostream &out, vector  & parameters)
 
         out << "Job item updated." << endl;
 
-        if (item->item_type < 0 && minfo.isValid())
+        if (item->item_type < (df::item_type)0 && minfo.isValid())
             out.printerr("WARNING: Due to a probable bug, creature & plant material subtype\n"
                             "         is ignored unless the item type is also specified.\n");
 
diff --git a/plugins/lua/power-meter.lua b/plugins/lua/power-meter.lua
new file mode 100644
index 000000000..310e51c4e
--- /dev/null
+++ b/plugins/lua/power-meter.lua
@@ -0,0 +1,11 @@
+local _ENV = mkmodule('plugins.power-meter')
+
+--[[
+
+ Native functions:
+
+ * makePowerMeter(plate_info,min_power,max_power,invert)
+
+--]]
+
+return _ENV
\ No newline at end of file
diff --git a/plugins/lua/rename.lua b/plugins/lua/rename.lua
new file mode 100644
index 000000000..0e7128f57
--- /dev/null
+++ b/plugins/lua/rename.lua
@@ -0,0 +1,13 @@
+local _ENV = mkmodule('plugins.rename')
+
+--[[
+
+ Native functions:
+
+ * canRenameBuilding(building)
+ * isRenamingBuilding(building)
+ * renameBuilding(building, name)
+
+--]]
+
+return _ENV
\ No newline at end of file
diff --git a/plugins/lua/siege-engine.lua b/plugins/lua/siege-engine.lua
new file mode 100644
index 000000000..89c47659d
--- /dev/null
+++ b/plugins/lua/siege-engine.lua
@@ -0,0 +1,45 @@
+local _ENV = mkmodule('plugins.siege-engine')
+
+--[[
+
+ Native functions:
+
+ * getTargetArea(building) -> point1, point2
+ * clearTargetArea(building)
+ * setTargetArea(building, point1, point2) -> true/false
+
+--]]
+
+Z_STEP_COUNT = 15
+Z_STEP = 1/31
+
+function findShotHeight(engine, target)
+    local path = { target = target, delta = 0.0 }
+
+    if projPathMetrics(engine, path).goal_step then
+        return path
+    end
+
+    for i = 1,Z_STEP_COUNT do
+        path.delta = i*Z_STEP
+        if projPathMetrics(engine, path).goal_step then
+            return path
+        end
+
+        path.delta = -i*Z_STEP
+        if projPathMetrics(engine, path).goal_step then
+            return path
+        end
+    end
+end
+
+function doAimProjectile(engine, item, target_min, target_max, skill)
+    print(item, df.skill_rating[skill])
+    local targets = proposeUnitHits(engine)
+    if #targets > 0 then
+        local rnd = math.random(#targets)
+        return findShotHeight(engine, targets[rnd].pos)
+    end
+end
+
+return _ENV
\ No newline at end of file
diff --git a/plugins/manipulator.cpp b/plugins/manipulator.cpp
index 3410b69e3..f40969655 100644
--- a/plugins/manipulator.cpp
+++ b/plugins/manipulator.cpp
@@ -39,8 +39,6 @@ using df::global::ui;
 using df::global::gps;
 using df::global::enabler;
 
-DFHACK_PLUGIN("manipulator");
-
 struct SkillLevel
 {
     const char *name;
@@ -254,6 +252,7 @@ struct UnitInfo
 enum altsort_mode {
     ALTSORT_NAME,
     ALTSORT_PROFESSION,
+    ALTSORT_HAPPINESS,
     ALTSORT_MAX
 };
 
@@ -277,6 +276,14 @@ bool sortByProfession (const UnitInfo *d1, const UnitInfo *d2)
         return (d1->profession < d2->profession);
 }
 
+bool sortByHappiness (const UnitInfo *d1, const UnitInfo *d2)
+{
+    if (descending)
+        return (d1->unit->status.happiness > d2->unit->status.happiness);
+    else
+        return (d1->unit->status.happiness < d2->unit->status.happiness);
+}
+
 bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2)
 {
     if (sort_skill != job_skill::NONE)
@@ -312,6 +319,14 @@ bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2)
     return sortByName(d1, d2);
 }
 
+enum display_columns {
+    DISP_COLUMN_HAPPINESS,
+    DISP_COLUMN_NAME,
+    DISP_COLUMN_PROFESSION,
+    DISP_COLUMN_LABORS,
+    DISP_COLUMN_MAX,
+};
+
 class viewscreen_unitlaborsst : public dfhack_viewscreen {
 public:
     void feed(set *events);
@@ -330,10 +345,11 @@ protected:
     vector units;
     altsort_mode altsort;
 
-    int first_row, sel_row;
+    int first_row, sel_row, num_rows;
     int first_column, sel_column;
 
-    int height, name_width, prof_width, labors_width;
+    int col_widths[DISP_COLUMN_MAX];
+    int col_offsets[DISP_COLUMN_MAX];
 
     void calcSize ();
 };
@@ -376,34 +392,52 @@ viewscreen_unitlaborsst::viewscreen_unitlaborsst(vector &src)
 
 void viewscreen_unitlaborsst::calcSize()
 {
-    height = gps->dimy - 10;
-    if (height > units.size())
-        height = units.size();
-
-    name_width = prof_width = labors_width = 0;
-    for (int i = 4; i < gps->dimx; i++)
+    num_rows = gps->dimy - 10;
+    if (num_rows > units.size())
+        num_rows = units.size();
+
+    int num_columns = gps->dimx - DISP_COLUMN_MAX - 1;
+    for (int i = 0; i < DISP_COLUMN_MAX; i++)
+        col_widths[i] = 0;
+    while (num_columns > 0)
     {
-        // 20% for Name, 20% for Profession, 60% for Labors
-        switch ((i - 4) % 5)
+        num_columns--;
+        // need at least 4 digits for happiness
+        if (col_widths[DISP_COLUMN_HAPPINESS] < 4)
+        {
+            col_widths[DISP_COLUMN_HAPPINESS]++;
+            continue;
+        }
+        // of remaining, 20% for Name, 20% for Profession, 60% for Labors
+        switch (num_columns % 5)
         {
         case 0: case 2: case 4:
-            labors_width++;
+            col_widths[DISP_COLUMN_LABORS]++;
             break;
         case 1:
-            name_width++;
+            col_widths[DISP_COLUMN_NAME]++;
             break;
         case 3:
-            prof_width++;
+            col_widths[DISP_COLUMN_PROFESSION]++;
             break;
         }
     }
-    while (labors_width > NUM_COLUMNS)
+
+    while (col_widths[DISP_COLUMN_LABORS] > NUM_COLUMNS)
+    {
+        col_widths[DISP_COLUMN_LABORS]--;
+        if (col_widths[DISP_COLUMN_LABORS] & 1)
+            col_widths[DISP_COLUMN_NAME]++;
+        else
+            col_widths[DISP_COLUMN_PROFESSION]++;
+    }
+
+    for (int i = 0; i < DISP_COLUMN_MAX; i++)
     {
-        if (labors_width & 1)
-            name_width++;
+        if (i == 0)
+            col_offsets[i] = 1;
         else
-            prof_width++;
-        labors_width--;
+            col_offsets[i] = col_offsets[i - 1] + col_widths[i - 1] + 1;
     }
 
     // don't adjust scroll position immediately after the window opened
@@ -411,20 +445,20 @@ void viewscreen_unitlaborsst::calcSize()
         return;
 
     // if the window grows vertically, scroll upward to eliminate blank rows from the bottom
-    if (first_row > units.size() - height)
-        first_row = units.size() - height;
+    if (first_row > units.size() - num_rows)
+        first_row = units.size() - num_rows;
 
     // if it shrinks vertically, scroll downward to keep the cursor visible
-    if (first_row < sel_row - height + 1)
-        first_row = sel_row - height + 1;
+    if (first_row < sel_row - num_rows + 1)
+        first_row = sel_row - num_rows + 1;
 
     // if the window grows horizontally, scroll to the left to eliminate blank columns from the right
-    if (first_column > NUM_COLUMNS - labors_width)
-        first_column = NUM_COLUMNS - labors_width;
+    if (first_column > NUM_COLUMNS - col_widths[DISP_COLUMN_LABORS])
+        first_column = NUM_COLUMNS - col_widths[DISP_COLUMN_LABORS];
 
     // if it shrinks horizontally, scroll to the right to keep the cursor visible
-    if (first_column < sel_column - labors_width + 1)
-        first_column = sel_column - labors_width + 1;
+    if (first_column < sel_column - col_widths[DISP_COLUMN_LABORS] + 1)
+        first_column = sel_column - col_widths[DISP_COLUMN_LABORS] + 1;
 }
 
 void viewscreen_unitlaborsst::feed(set *events)
@@ -455,8 +489,8 @@ void viewscreen_unitlaborsst::feed(set *events)
 
     if (sel_row < first_row)
         first_row = sel_row;
-    if (first_row < sel_row - height + 1)
-        first_row = sel_row - height + 1;
+    if (first_row < sel_row - num_rows + 1)
+        first_row = sel_row - num_rows + 1;
 
     if (events->count(interface_key::CURSOR_LEFT) || events->count(interface_key::CURSOR_UPLEFT) || events->count(interface_key::CURSOR_DOWNLEFT))
         sel_column--;
@@ -491,8 +525,8 @@ void viewscreen_unitlaborsst::feed(set *events)
 
     if (sel_column < first_column)
         first_column = sel_column;
-    if (first_column < sel_column - labors_width + 1)
-        first_column = sel_column - labors_width + 1;
+    if (first_column < sel_column - col_widths[DISP_COLUMN_LABORS] + 1)
+        first_column = sel_column - col_widths[DISP_COLUMN_LABORS] + 1;
 
     UnitInfo *cur = units[sel_row];
     if (events->count(interface_key::SELECT) && (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE))
@@ -558,6 +592,9 @@ void viewscreen_unitlaborsst::feed(set *events)
         case ALTSORT_PROFESSION:
             std::sort(units.begin(), units.end(), sortByProfession);
             break;
+        case ALTSORT_HAPPINESS:
+            std::sort(units.begin(), units.end(), sortByHappiness);
+            break;
         }
     }
     if (events->count(interface_key::CHANGETAB))
@@ -568,6 +605,9 @@ void viewscreen_unitlaborsst::feed(set *events)
             altsort = ALTSORT_PROFESSION;
             break;
         case ALTSORT_PROFESSION:
+            altsort = ALTSORT_HAPPINESS;
+            break;
+        case ALTSORT_HAPPINESS:
             altsort = ALTSORT_NAME;
             break;
         }
@@ -605,9 +645,9 @@ void viewscreen_unitlaborsst::render()
     dfhack_viewscreen::render();
 
     Screen::clear();
-    Screen::drawBorder("  Manage Labors  ");
+    Screen::drawBorder("  Dwarf Manipulator - Manage Labors  ");
 
-    for (int col = 0; col < labors_width; col++)
+    for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++)
     {
         int col_offset = col + first_column;
         if (col_offset >= NUM_COLUMNS)
@@ -622,21 +662,21 @@ void viewscreen_unitlaborsst::render()
             bg = 7;
         }
 
-        Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 1);
-        Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 2);
+        Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 1);
+        Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 2);
         df::profession profession = columns[col_offset].profession;
-        if (profession != profession::NONE)
+        if ((profession != profession::NONE) && (ui->race_id != -1))
         {
             auto graphics = world->raws.creatures.all[ui->race_id]->graphics;
             Screen::paintTile(
                 Screen::Pen(' ', fg, 0,
                     graphics.profession_add_color[creature_graphics_role::DEFAULT][profession],
                     graphics.profession_texpos[creature_graphics_role::DEFAULT][profession]),
-                1 + name_width + 1 + prof_width + 1 + col, 3);
+                col_offsets[DISP_COLUMN_LABORS] + col, 3);
         }
     }
 
-    for (int row = 0; row < height; row++)
+    for (int row = 0; row < num_rows; row++)
     {
         int row_offset = row + first_row;
         if (row_offset >= units.size())
@@ -645,6 +685,26 @@ void viewscreen_unitlaborsst::render()
         UnitInfo *cur = units[row_offset];
         df::unit *unit = cur->unit;
         int8_t fg = 15, bg = 0;
+
+        int happy = cur->unit->status.happiness;
+        string happiness = stl_sprintf("%4i", happy);
+        if (happy == 0)         // miserable
+            fg = 13;    // 5:1
+        else if (happy <= 25)   // very unhappy
+            fg = 12;    // 4:1
+        else if (happy <= 50)   // unhappy
+            fg = 4;     // 4:0
+        else if (happy < 75)    // fine
+            fg = 14;    // 6:1
+        else if (happy < 125)   // quite content
+            fg = 6;     // 6:0
+        else if (happy < 150)   // happy
+            fg = 2;     // 2:0
+        else                    // ecstatic
+            fg = 10;    // 2:1
+        Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_HAPPINESS], 4 + row, happiness);
+
+        fg = 15;
         if (row_offset == sel_row)
         {
             fg = 0;
@@ -652,23 +712,23 @@ void viewscreen_unitlaborsst::render()
         }
 
         string name = cur->name;
-        name.resize(name_width);
-        Screen::paintString(Screen::Pen(' ', fg, bg), 1, 4 + row, name);
+        name.resize(col_widths[DISP_COLUMN_NAME]);
+        Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_NAME], 4 + row, name);
 
         string profession = cur->profession;
-        profession.resize(prof_width);
+        profession.resize(col_widths[DISP_COLUMN_PROFESSION]);
         fg = cur->color;
         bg = 0;
 
-        Screen::paintString(Screen::Pen(' ', fg, bg), 1 + name_width + 1, 4 + row, profession);
+        Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_PROFESSION], 4 + row, profession);
 
         // Print unit's skills and labor assignments
-        for (int col = 0; col < labors_width; col++)
+        for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++)
         {
             int col_offset = col + first_column;
             fg = 15;
             bg = 0;
-            char c = 0xFA;
+            uint8_t c = 0xFA;
             if ((col_offset == sel_column) && (row_offset == sel_row))
                 fg = 9;
             if (columns[col_offset].skill != job_skill::NONE)
@@ -695,7 +755,7 @@ void viewscreen_unitlaborsst::render()
             }
             else
                 bg = 4;
-            Screen::paintTile(Screen::Pen(c, fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 4 + row);
+            Screen::paintTile(Screen::Pen(c, fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 4 + row);
         }
     }
 
@@ -705,17 +765,17 @@ void viewscreen_unitlaborsst::render()
     {
         df::unit *unit = cur->unit;
         int x = 1;
-        Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->transname);
+        Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->transname);
         x += cur->transname.length();
 
         if (cur->transname.length())
         {
-            Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ", ");
+            Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ", ");
             x += 2;
         }
-        Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->profession);
+        Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->profession);
         x += cur->profession.length();
-        Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ": ");
+        Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ": ");
         x += 2;
 
         string str;
@@ -742,12 +802,12 @@ void viewscreen_unitlaborsst::render()
             else
                 str = stl_sprintf("Not %s (0/500)", ENUM_ATTR_STR(job_skill, caption_noun, columns[sel_column].skill));
         }
-        Screen::paintString(Screen::Pen(' ', 9, 0), x, 3 + height + 2, str);
+        Screen::paintString(Screen::Pen(' ', 9, 0), x, 3 + num_rows + 2, str);
 
         canToggle = (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE);
     }
 
-    int x = 1;
+    int x = 2;
     OutputString(10, x, gps->dimy - 3, "Enter"); // SELECT key
     OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle labor, ");
 
@@ -760,7 +820,7 @@ void viewscreen_unitlaborsst::render()
     OutputString(10, x, gps->dimy - 3, "c"); // UNITJOB_ZOOM_CRE key
     OutputString(15, x, gps->dimy - 3, ": Zoom-Cre");
 
-    x = 1;
+    x = 2;
     OutputString(10, x, gps->dimy - 2, "Esc"); // LEAVESCREEN key
     OutputString(15, x, gps->dimy - 2, ": Done, ");
 
@@ -781,6 +841,9 @@ void viewscreen_unitlaborsst::render()
     case ALTSORT_PROFESSION:
         OutputString(15, x, gps->dimy - 2, "Profession");
         break;
+    case ALTSORT_HAPPINESS:
+        OutputString(15, x, gps->dimy - 2, "Happiness");
+        break;
     default:
         OutputString(15, x, gps->dimy - 2, "Unknown");
         break;
@@ -803,23 +866,35 @@ struct unitlist_hook : df::viewscreen_unitlistst
         }
         INTERPOSE_NEXT(feed)(input);
     }
+
+    DEFINE_VMETHOD_INTERPOSE(void, render, ())
+    {
+        INTERPOSE_NEXT(render)();
+
+        if (units[page].size())
+        {
+            int x = 2;
+            OutputString(12, x, gps->dimy - 2, "l"); // UNITVIEW_PRF_PROF key
+            OutputString(15, x, gps->dimy - 2, ": Manage labors (DFHack)");
+        }
+    }
 };
 
 IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, feed);
+IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, render);
+
+DFHACK_PLUGIN("manipulator");
 
 DFhackCExport command_result plugin_init ( color_ostream &out, vector  &commands)
 {
-    if (gps)
-    {
-        if (!INTERPOSE_HOOK(unitlist_hook, feed).apply())
-            out.printerr("Could not interpose viewscreen_unitlistst::feed\n");
-    }
-
+    if (!gps || !INTERPOSE_HOOK(unitlist_hook, feed).apply() || !INTERPOSE_HOOK(unitlist_hook, render).apply())
+        out.printerr("Could not insert Dwarf Manipulator hooks!\n");
     return CR_OK;
 }
 
 DFhackCExport command_result plugin_shutdown ( color_ostream &out )
 {
     INTERPOSE_HOOK(unitlist_hook, feed).remove();
+    INTERPOSE_HOOK(unitlist_hook, render).remove();
     return CR_OK;
 }
diff --git a/plugins/power-meter.cpp b/plugins/power-meter.cpp
new file mode 100644
index 000000000..0466b68e4
--- /dev/null
+++ b/plugins/power-meter.cpp
@@ -0,0 +1,237 @@
+#include "Core.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include "df/graphic.h"
+#include "df/building_trapst.h"
+#include "df/builtin_mats.h"
+#include "df/world.h"
+#include "df/buildings_other_id.h"
+#include "df/machine.h"
+#include "df/machine_info.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("power-meter");
+
+static const uint32_t METER_BIT = 0x80000000U;
+
+static void init_plate_info(df::pressure_plate_info &plate_info)
+{
+    plate_info.water_min = 1;
+    plate_info.water_max = 7;
+    plate_info.flags.whole = METER_BIT;
+    plate_info.flags.bits.water = true;
+    plate_info.flags.bits.resets = true;
+}
+
+/*
+ * Hook for the pressure plate itself. Implements core logic.
+ */
+
+struct trap_hook : df::building_trapst {
+    typedef df::building_trapst interpose_base;
+
+    // Engine detection
+
+    bool is_power_meter()
+    {
+        return trap_type == trap_type::PressurePlate &&
+               (plate_info.flags.whole & METER_BIT) != 0;
+    }
+
+    inline bool is_fully_built()
+    {
+        return getBuildStage() >= getMaxBuildStage();
+    }
+
+    DEFINE_VMETHOD_INTERPOSE(void, getName, (std::string *buf))
+    {
+        if (is_power_meter())
+        {
+            buf->clear();
+            *buf += "Power Meter";
+            return;
+        }
+
+        INTERPOSE_NEXT(getName)(buf);
+    }
+
+    DEFINE_VMETHOD_INTERPOSE(void, updateAction, ())
+    {
+        if (is_power_meter())
+        {
+            auto pdsgn = Maps::getTileDesignation(centerx,centery,z);
+
+            if (pdsgn)
+            {
+                bool active = false;
+                auto &gears = world->buildings.other[buildings_other_id::GEAR_ASSEMBLY];
+
+                for (size_t i = 0; i < gears.size(); i++)
+                {
+                    // Adjacent
+                    auto gear = gears[i];
+                    int deltaxy = abs(centerx - gear->centerx) + abs(centery - gear->centery);
+                    if (gear->z != z || deltaxy != 1)
+                        continue;
+                    // Linked to machine
+                    auto info = gears[i]->getMachineInfo();
+                    if (!info || info->machine_id < 0)
+                        continue;
+                    // an active machine
+                    auto machine = df::machine::find(info->machine_id);
+                    if (!machine || !machine->flags.bits.active)
+                        continue;
+                    // with adequate power?
+                    int power = machine->cur_power - machine->min_power;
+                    if (power < 0 || machine->cur_power <= 0)
+                        continue;
+                    if (power < plate_info.track_min)
+                        continue;
+                    if (power > plate_info.track_max && plate_info.track_max >= 0)
+                        continue;
+
+                    active = true;
+                    break;
+                }
+
+                if (plate_info.flags.bits.citizens)
+                    active = !active;
+
+                // Temporarily set the tile water amount based on power state
+                auto old_dsgn = *pdsgn;
+                pdsgn->bits.liquid_type = tile_liquid::Water;
+                pdsgn->bits.flow_size = (active ? 7 : 0);
+
+                INTERPOSE_NEXT(updateAction)();
+
+                *pdsgn = old_dsgn;
+                return;
+            }
+        }
+
+        INTERPOSE_NEXT(updateAction)();
+    }
+
+    DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (df::building_drawbuffer *db, void *unk))
+    {
+        INTERPOSE_NEXT(drawBuilding)(db, unk);
+
+        if (is_power_meter() && is_fully_built())
+        {
+            db->fore[0][0] = 3;
+        }
+    }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(trap_hook, getName);
+IMPLEMENT_VMETHOD_INTERPOSE(trap_hook, updateAction);
+IMPLEMENT_VMETHOD_INTERPOSE(trap_hook, drawBuilding);
+
+static bool enabled = false;
+
+static void enable_hooks(bool enable)
+{
+    enabled = enable;
+
+    INTERPOSE_HOOK(trap_hook, getName).apply(enable);
+    INTERPOSE_HOOK(trap_hook, updateAction).apply(enable);
+    INTERPOSE_HOOK(trap_hook, drawBuilding).apply(enable);
+}
+
+static bool makePowerMeter(df::pressure_plate_info *info, int min_power, int max_power, bool invert)
+{
+    CHECK_NULL_POINTER(info);
+
+    if (!enabled)
+    {
+        auto pworld = Core::getInstance().getWorld();
+        auto entry = pworld->GetPersistentData("power-meter/enabled", NULL);
+        if (!entry.isValid())
+            return false;
+
+        enable_hooks(true);
+    }
+
+    init_plate_info(*info);
+    info->track_min = min_power;
+    info->track_max = max_power;
+    info->flags.bits.citizens = invert;
+    return true;
+}
+
+DFHACK_PLUGIN_LUA_FUNCTIONS {
+    DFHACK_LUA_FUNCTION(makePowerMeter),
+    DFHACK_LUA_END
+};
+
+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("power-meter/enabled").isValid();
+
+            if (enable)
+            {
+                out.print("Enabling the power meter plugin.\n");
+                enable_hooks(true);
+            }
+        }
+        break;
+    case SC_MAP_UNLOADED:
+        enable_hooks(false);
+        break;
+    default:
+        break;
+    }
+
+    return CR_OK;
+}
+
+DFhackCExport command_result plugin_init ( color_ostream &out, std::vector  &commands)
+{
+    if (Core::getInstance().isMapLoaded())
+        plugin_onstatechange(out, SC_MAP_LOADED);
+
+    return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown ( color_ostream &out )
+{
+    enable_hooks(false);
+    return CR_OK;
+}
diff --git a/plugins/proto/rename.proto b/plugins/proto/rename.proto
index aa1e95f48..810091fc7 100644
--- a/plugins/proto/rename.proto
+++ b/plugins/proto/rename.proto
@@ -17,3 +17,10 @@ message RenameUnitIn {
     optional string nickname = 2;
     optional string profession = 3;
 }
+
+// RPC RenameBuilding : RenameBuildingIn -> EmptyMessage
+message RenameBuildingIn {
+    required int32 building_id = 1;
+
+    optional string name = 2;
+}
diff --git a/plugins/raw/building_steam_engine.txt b/plugins/raw/building_steam_engine.txt
new file mode 100644
index 000000000..48657b0c1
--- /dev/null
+++ b/plugins/raw/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/raw/item_trapcomp_steam_engine.txt b/plugins/raw/item_trapcomp_steam_engine.txt
new file mode 100644
index 000000000..c35f6ef45
--- /dev/null
+++ b/plugins/raw/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:heavy]
+[SIZE:1800]
+[HITS:1]
+[MATERIAL_SIZE:6]
+[METAL]
+[ATTACK:BLUNT:40:200:bash:bashes:NO_SUB:2000]
diff --git a/plugins/raw/reaction_steam_engine.txt b/plugins/raw/reaction_steam_engine.txt
new file mode 100644
index 000000000..175ffdd50
--- /dev/null
+++ b/plugins/raw/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/rename.cpp b/plugins/rename.cpp
index 1871d0f73..99dc6848a 100644
--- a/plugins/rename.cpp
+++ b/plugins/rename.cpp
@@ -3,11 +3,15 @@
 #include "Export.h"
 #include "PluginManager.h"
 
+#include 
+#include 
+
 #include "modules/Gui.h"
 #include "modules/Translation.h"
 #include "modules/Units.h"
+#include "modules/World.h"
 
-#include "DataDefs.h"
+#include 
 #include "df/ui.h"
 #include "df/world.h"
 #include "df/squad.h"
@@ -18,6 +22,11 @@
 #include "df/historical_figure_info.h"
 #include "df/assumed_identity.h"
 #include "df/language_name.h"
+#include "df/building_stockpilest.h"
+#include "df/building_workshopst.h"
+#include "df/building_furnacest.h"
+#include "df/building_trapst.h"
+#include "df/building_siegeenginest.h"
 
 #include "RemoteServer.h"
 #include "rename.pb.h"
@@ -36,6 +45,8 @@ using namespace dfproto;
 using df::global::ui;
 using df::global::world;
 
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event);
+
 static command_result rename(color_ostream &out, vector  & parameters);
 
 DFHACK_PLUGIN("rename");
@@ -51,8 +62,32 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector clear(); \
+                *buf += name; \
+                *buf += " ("; \
+                if (tag) *buf += (const char*)tag; \
+                else { std::string tmp; INTERPOSE_NEXT(getName)(&tmp); *buf += tmp; } \
+                *buf += ")"; \
+                return; \
+            } \
+            else \
+                INTERPOSE_NEXT(getName)(buf); \
+        } \
+    }; \
+    IMPLEMENT_VMETHOD_INTERPOSE(cname##_hook, getName);
+KNOWN_BUILDINGS
+#undef BUILDING
+
+static char getBuildingCode(df::building *bld)
+{
+    CHECK_NULL_POINTER(bld);
+
+#define BUILDING(code, cname, tag) \
+    if (strict_virtual_cast(bld)) return code;
+KNOWN_BUILDINGS
+#undef BUILDING
+
+    return 0;
+}
+
+static bool enable_building_rename(char code, bool enable)
+{
+    switch (code) {
+#define BUILDING(code, cname, tag) \
+    case code: return INTERPOSE_HOOK(cname##_hook, getName).apply(enable);
+KNOWN_BUILDINGS
+#undef BUILDING
+    default:
+        return false;
+    }
+}
+
+static void disable_building_rename()
+{
+#define BUILDING(code, cname, tag) \
+    INTERPOSE_HOOK(cname##_hook, getName).remove();
+KNOWN_BUILDINGS
+#undef BUILDING
+}
+
+static bool is_enabled_building(char code)
+{
+    switch (code) {
+#define BUILDING(code, cname, tag) \
+    case code: return INTERPOSE_HOOK(cname##_hook, getName).is_applied();
+KNOWN_BUILDINGS
+#undef BUILDING
+    default:
+        return false;
+    }
+}
+
+static void init_buildings(bool enable)
+{
+    disable_building_rename();
+
+    if (enable)
+    {
+        auto pworld = Core::getInstance().getWorld();
+        auto entry = pworld->GetPersistentData("rename/building_types");
+
+        if (entry.isValid())
+        {
+            std::string val = entry.val();
+            for (size_t i = 0; i < val.size(); i++)
+                enable_building_rename(val[i], true);
+        }
+    }
+}
+
+static bool canRenameBuilding(df::building *bld)
+{
+    return getBuildingCode(bld) != 0;
+}
+
+static bool isRenamingBuilding(df::building *bld)
+{
+    return is_enabled_building(getBuildingCode(bld));
+}
+
+static bool renameBuilding(df::building *bld, std::string name)
+{
+    char code = getBuildingCode(bld);
+    if (code == 0 && !name.empty())
+        return false;
+
+    if (!name.empty() && !is_enabled_building(code))
+    {
+        auto pworld = Core::getInstance().getWorld();
+        auto entry = pworld->GetPersistentData("rename/building_types", NULL);
+        if (!entry.isValid())
+            return false;
+
+        if (!enable_building_rename(code, true))
+            return false;
+
+        entry.val().push_back(code);
+    }
+
+    bld->name = name;
+    return true;
+}
+
 static df::squad *getSquadByIndex(unsigned idx)
 {
     auto entity = df::historical_entity::find(ui->group_id);
@@ -101,14 +263,37 @@ static command_result RenameUnit(color_ostream &stream, const RenameUnitIn *in)
     return CR_OK;
 }
 
+static command_result RenameBuilding(color_ostream &stream, const RenameBuildingIn *in)
+{
+    auto building = df::building::find(in->building_id());
+    if (!building)
+        return CR_NOT_FOUND;
+
+    if (in->has_name())
+    {
+        if (!renameBuilding(building, in->name()))
+            return CR_FAILURE;
+    }
+
+    return CR_OK;
+}
+
 DFhackCExport RPCService *plugin_rpcconnect(color_ostream &)
 {
     RPCService *svc = new RPCService();
     svc->addFunction("RenameSquad", RenameSquad);
     svc->addFunction("RenameUnit", RenameUnit);
+    svc->addFunction("RenameBuilding", RenameBuilding);
     return svc;
 }
 
+DFHACK_PLUGIN_LUA_FUNCTIONS {
+    DFHACK_LUA_FUNCTION(canRenameBuilding),
+    DFHACK_LUA_FUNCTION(isRenamingBuilding),
+    DFHACK_LUA_FUNCTION(renameBuilding),
+    DFHACK_LUA_END
+};
+
 static command_result rename(color_ostream &out, vector  ¶meters)
 {
     CoreSuspender suspend;
@@ -167,6 +352,20 @@ static command_result rename(color_ostream &out, vector  ¶meters)
 
         unit->custom_profession = parameters[1];
     }
+    else if (cmd == "building")
+    {
+        if (parameters.size() != 2)
+            return CR_WRONG_USAGE;
+
+        if (ui->main.mode != ui_sidebar_mode::QueryBuilding)
+            return CR_WRONG_USAGE;
+
+        if (!renameBuilding(world->selected_building, parameters[1]))
+        {
+            out.printerr("This type of building is not supported.\n");
+            return CR_FAILURE;
+        }
+    }
     else
     {
         if (!parameters.empty() && cmd != "?")
diff --git a/plugins/steam-engine.cpp b/plugins/steam-engine.cpp
new file mode 100644
index 000000000..cacfc6e16
--- /dev/null
+++ b/plugins/steam-engine.cpp
@@ -0,0 +1,1007 @@
+#include "Core.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#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"
+
+/*
+ * This plugin implements a steam engine workshop. It activates
+ * if there are any workshops in the raws with STEAM_ENGINE in
+ * their token, and provides the necessary behavior.
+ *
+ * Construction:
+ *
+ * The workshop needs water as its input, which it takes via a
+ * passable floor tile below it, like usual magma workshops do.
+ * The magma version also needs magma.
+ *
+ * ISSUE: Since this building is a machine, and machine collapse
+ * code cannot be modified, it would collapse over true open space.
+ * As a loophole, down stair provides support to machines, while
+ * being passable, so use them.
+ *
+ * After constructing the building itself, machines can be connected
+ * to the edge tiles that look like gear boxes. Their exact position
+ * is extracted from the workshop raws.
+ *
+ * ISSUE: Like with collapse above, part of the code involved in
+ * machine connection cannot be modified. As a result, the workshop
+ * can only immediately connect to machine components built AFTER it.
+ * This also means that engines cannot be chained without intermediate
+ * short axles that can be built later.
+ *
+ * Operation:
+ *
+ * In order to operate the engine, queue the Stoke Boiler job.
+ * A furnace operator will come, possibly bringing a bar of fuel,
+ * and perform it. As a result, a "boiling water" item will appear
+ * in the 't' view of the workshop.
+ *
+ * Note: The completion of the job will actually consume one unit
+ * of appropriate liquids from below the workshop.
+ *
+ * Every such item gives 100 power, up to a limit of 300 for coal,
+ * and 500 for a magma engine. The building can host twice that
+ * amount of items to provide longer autonomous running. When the
+ * boiler gets filled to capacity, all queued jobs are suspended;
+ * once it drops back to 3+1 or 5+1 items, they are re-enabled.
+ *
+ * While the engine is providing power, steam is being consumed.
+ * The consumption speed includes a fixed 10% waste rate, and
+ * the remaining 90% are applied proportionally to the actual
+ * load in the machine. With the engine at nominal 300 power with
+ * 150 load in the system, it will consume steam for actual
+ * 300*(10% + 90%*150/300) = 165 power.
+ *
+ * Masterpiece mechanism and chain will decrease the mechanical
+ * power drawn by the engine itself from 10 to 5. Masterpiece
+ * barrel decreases waste rate by 4%. Masterpiece piston and pipe
+ * decrease it by further 4%, and also decrease the whole steam
+ * use rate by 10%.
+ *
+ * Explosions:
+ *
+ * The engine must be constructed using barrel, pipe and piston
+ * from fire-safe, or in the magma version magma-safe metals.
+ *
+ * During operation weak parts get gradually worn out, and
+ * eventually the engine explodes. It should also explode if
+ * toppled during operation by a building destroyer, or a
+ * tantruming dwarf.
+ *
+ * Save files:
+ *
+ * It should be safe to load and view fortresses using engines
+ * from a DF version without DFHack installed, except that in such
+ * case the engines won't work. However actually making modifications
+ * to them, or machines they connect to (including by pulling levers),
+ * can easily result in inconsistent state once this plugin is
+ * available again. The effects may be as weird as negative power
+ * being generated.
+ */
+
+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 gear_tiles;
+    df::coord2d hearth_tile;
+    df::coord2d water_tile;
+    df::coord2d magma_tile;
+};
+
+std::vector 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(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(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 piston_qual = get_component_quality(1);
+        float waste = 0.1f - 0.016f * 0.5f * (piston_qual + get_component_quality(2));
+        float efficiency_coeff = 1.0f - 0.02f * piston_qual;
+        // apply rate and waste factor
+        ticks *= (waste + 0.9f*power_rate)*power_level*efficiency_coeff;
+        // 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 *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  &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;
+}
diff --git a/plugins/stonesense b/plugins/stonesense
index 2a62ba5ed..37a823541 160000
--- a/plugins/stonesense
+++ b/plugins/stonesense
@@ -1 +1 @@
-Subproject commit 2a62ba5ed2607f4dbf0473e77502d4e19c19678e
+Subproject commit 37a823541538023b9f3d0d1e8039cf32851de68d
diff --git a/plugins/tiletypes.cpp b/plugins/tiletypes.cpp
index 190bda7cd..6af94f2ee 100644
--- a/plugins/tiletypes.cpp
+++ b/plugins/tiletypes.cpp
@@ -767,7 +767,7 @@ command_result executePaintJob(color_ostream &out)
         }
 
         // Remove liquid from walls, etc
-        if (type != -1 && !DFHack::FlowPassable(type))
+        if (type != (df::tiletype)-1 && !DFHack::FlowPassable(type))
         {
             des.bits.flow_size = 0;
             //des.bits.liquid_type = DFHack::liquid_water;
diff --git a/plugins/tweak.cpp b/plugins/tweak.cpp
index fa99f39e5..bebc346c5 100644
--- a/plugins/tweak.cpp
+++ b/plugins/tweak.cpp
@@ -32,6 +32,8 @@
 #include "df/squad_order_trainst.h"
 #include "df/ui_build_selector.h"
 #include "df/building_trapst.h"
+#include "df/item_actual.h"
+#include "df/contaminant.h"
 
 #include 
 
@@ -85,6 +87,12 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector \n"
+        "    Further improves temperature updates by ensuring that 1 degree of\n"
+        "    item temperature is crossed in no more than specified number of frames\n"
+        "    when updating from the environment temperature. Use 0 to disable.\n"
     ));
     return CR_OK;
 }
@@ -211,9 +219,6 @@ struct patrol_duty_hook : df::squad_order_trainst
 
 IMPLEMENT_VMETHOD_INTERPOSE(patrol_duty_hook, isPatrol);
 
-static const int AREA_MAP_WIDTH = 23;
-static const int MENU_WIDTH = 30;
-
 struct readable_build_plate_hook : df::viewscreen_dwarfmodest
 {
     typedef df::viewscreen_dwarfmodest interpose_base;
@@ -228,10 +233,8 @@ struct readable_build_plate_hook : df::viewscreen_dwarfmodest
             ui_build_selector->building_subtype == trap_type::PressurePlate &&
             ui_build_selector->plate_info.flags.bits.units)
         {
-            auto wsize = Screen::getWindowSize();
-            int x = wsize.x - MENU_WIDTH - 1;
-            if (*ui_menu_width == 1 || *ui_area_map_width == 2)
-                x -= AREA_MAP_WIDTH + 1;
+            auto dims = Gui::getDwarfmodeViewDims();
+            int x = dims.menu_x1;
 
             Screen::Pen pen(' ',COLOR_WHITE);
 
@@ -248,6 +251,98 @@ struct readable_build_plate_hook : df::viewscreen_dwarfmodest
 
 IMPLEMENT_VMETHOD_INTERPOSE(readable_build_plate_hook, render);
 
+struct stable_temp_hook : df::item_actual {
+    typedef df::item_actual interpose_base;
+
+    DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t rate_mult))
+    {
+        if (temperature != temp)
+        {
+            // Bug 6012 is caused by fixed-point precision mismatch jitter
+            // when an item is being pushed by two sources at N and N+1.
+            // This check suppresses it altogether.
+            if (temp == temperature+1 ||
+                (temp == temperature-1 && temperature_fraction == 0))
+                temp = temperature;
+            // When SPEC_HEAT is NONE, the original function seems to not
+            // change the temperature, yet return true, which is silly.
+            else if (getSpecHeat() == 60001)
+                temp = temperature;
+        }
+
+        return INTERPOSE_NEXT(adjustTemperature)(temp, rate_mult);
+    }
+
+    DEFINE_VMETHOD_INTERPOSE(bool, updateContaminants, ())
+    {
+        if (contaminants)
+        {
+            // Force 1-degree difference in contaminant temperature to 0
+            for (size_t i = 0; i < contaminants->size(); i++)
+            {
+                auto obj = (*contaminants)[i];
+
+                if (abs(obj->temperature - temperature) == 1)
+                {
+                    obj->temperature = temperature;
+                    obj->temperature_fraction = temperature_fraction;
+                }
+            }
+        }
+
+        return INTERPOSE_NEXT(updateContaminants)();
+    }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, adjustTemperature);
+IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, updateContaminants);
+
+static int map_temp_mult = -1;
+static int max_heat_ticks = 0;
+
+struct fast_heat_hook : df::item_actual {
+    typedef df::item_actual interpose_base;
+
+    DEFINE_VMETHOD_INTERPOSE(
+        bool, updateTempFromMap,
+        (bool local, bool contained, bool adjust, int32_t rate_mult)
+    ) {
+        int cmult = map_temp_mult;
+        map_temp_mult = rate_mult;
+
+        bool rv = INTERPOSE_NEXT(updateTempFromMap)(local, contained, adjust, rate_mult);
+        map_temp_mult = cmult;
+        return rv;
+    }
+
+    DEFINE_VMETHOD_INTERPOSE(
+        bool, updateTemperature,
+        (uint16_t temp, bool local, bool contained, bool adjust, int32_t rate_mult)
+    ) {
+        // Some items take ages to cross the last degree, so speed them up
+        if (map_temp_mult > 0 && temp != temperature && max_heat_ticks > 0)
+        {
+            int spec = getSpecHeat();
+            if (spec != 60001)
+                rate_mult = std::max(map_temp_mult, spec/max_heat_ticks/abs(temp - temperature));
+        }
+
+        return INTERPOSE_NEXT(updateTemperature)(temp, local, contained, adjust, rate_mult);
+    }
+
+    DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t rate_mult))
+    {
+        if (map_temp_mult > 0)
+            rate_mult = map_temp_mult;
+
+        return INTERPOSE_NEXT(adjustTemperature)(temp, rate_mult);
+    }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(fast_heat_hook, updateTempFromMap);
+IMPLEMENT_VMETHOD_INTERPOSE(fast_heat_hook, updateTemperature);
+IMPLEMENT_VMETHOD_INTERPOSE(fast_heat_hook, adjustTemperature);
+
 static void enable_hook(color_ostream &out, VMethodInterposeLinkBase &hook, vector  ¶meters)
 {
     if (vector_get(parameters, 1) == "disable")
@@ -380,6 +475,22 @@ static command_result tweak(color_ostream &out, vector  ¶meters)
 
         enable_hook(out, INTERPOSE_HOOK(readable_build_plate_hook, render), parameters);
     }
+    else if (cmd == "stable-temp")
+    {
+        enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, adjustTemperature), parameters);
+        enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, updateContaminants), parameters);
+    }
+    else if (cmd == "fast-heat")
+    {
+        if (parameters.size() < 2)
+            return CR_WRONG_USAGE;
+        max_heat_ticks = atoi(parameters[1].c_str());
+        if (max_heat_ticks <= 0)
+            parameters[1] = "disable";
+        enable_hook(out, INTERPOSE_HOOK(fast_heat_hook, updateTempFromMap), parameters);
+        enable_hook(out, INTERPOSE_HOOK(fast_heat_hook, updateTemperature), parameters);
+        enable_hook(out, INTERPOSE_HOOK(fast_heat_hook, adjustTemperature), parameters);
+    }
     else 
         return CR_WRONG_USAGE;
 
diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp
index 639e7c56a..98258682e 100644
--- a/plugins/workflow.cpp
+++ b/plugins/workflow.cpp
@@ -828,7 +828,7 @@ static void compute_custom_job(ProtectedJob *pj, df::job *job)
         using namespace df::enums::reaction_product_item_flags;
 
         VIRTUAL_CAST_VAR(prod, df::reaction_product_itemst, r->products[i]);
-        if (!prod || (prod->item_type < 0 && !prod->flags.is_set(CRAFTS)))
+        if (!prod || (prod->item_type < (df::item_type)0 && !prod->flags.is_set(CRAFTS)))
             continue;
 
         MaterialInfo mat(prod);
diff --git a/plugins/zone.cpp b/plugins/zone.cpp
index fc89fecc1..c496f49b6 100644
--- a/plugins/zone.cpp
+++ b/plugins/zone.cpp
@@ -792,7 +792,7 @@ void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false)
 bool isActivityZone(df::building * building)
 {
     if(    building->getType() == building_type::Civzone
-        && building->getSubtype() == civzone_type::ActivityZone)
+        && building->getSubtype() == (short)civzone_type::ActivityZone)
         return true;
     else
         return false;
@@ -1603,7 +1603,7 @@ void zoneInfo(color_ostream & out, df::building* building, bool verbose)
     if(building->getType()!= building_type::Civzone)
         return;
 
-    if(building->getSubtype() != civzone_type::ActivityZone)
+    if(building->getSubtype() != (short)civzone_type::ActivityZone)
         return;
 
     string name;
diff --git a/scripts/devel/lsmem.lua b/scripts/devel/lsmem.lua
new file mode 100644
index 000000000..75586324d
--- /dev/null
+++ b/scripts/devel/lsmem.lua
@@ -0,0 +1,14 @@
+-- Prints memory ranges of the process.
+
+for _,v in ipairs(dfhack.internal.getMemRanges()) do
+    local access = { '-', '-', '-', 'p' }
+    if v.read then access[1] = 'r' end
+    if v.write then access[2] = 'w' end
+    if v.execute then access[3] = 'x' end
+    if not v.valid then
+        access[4] = '?'
+    elseif v.shared then
+        access[4] = 's'
+    end
+    print(string.format('%08x-%08x %s %s', v.start_addr, v.end_addr, table.concat(access), v.name))
+end
diff --git a/scripts/devel/pop-screen.lua b/scripts/devel/pop-screen.lua
new file mode 100644
index 000000000..f1ee072c5
--- /dev/null
+++ b/scripts/devel/pop-screen.lua
@@ -0,0 +1,3 @@
+-- For killing bugged out gui script screens.
+
+dfhack.screen.dismiss(dfhack.gui.getCurViewscreen())
diff --git a/scripts/fix/stable-temp.lua b/scripts/fix/stable-temp.lua
index d06d0fcce..27a88ef7b 100644
--- a/scripts/fix/stable-temp.lua
+++ b/scripts/fix/stable-temp.lua
@@ -1,5 +1,9 @@
 -- Reset item temperature to the value of their tile.
 
+local args = {...}
+
+local apply = (args[1] == 'apply')
+
 local count = 0
 local types = {}
 
@@ -9,13 +13,16 @@ local function update_temp(item,btemp)
         local tid = item:getType()
         types[tid] = (types[tid] or 0) + 1
     end
-    item.temperature = btemp
-    item.temperature_fraction = 0
 
-    if item.contaminants then
-        for _,c in ipairs(item.contaminants) do
-            c.temperature = btemp
-            c.temperature_fraction = 0
+    if apply then
+        item.temperature = btemp
+        item.temperature_fraction = 0
+
+        if item.contaminants then
+            for _,c in ipairs(item.contaminants) do
+                c.temperature = btemp
+                c.temperature_fraction = 0
+            end
         end
     end
 
@@ -23,7 +30,9 @@ local function update_temp(item,btemp)
         update_temp(sub,btemp)
     end
 
-    item:checkTemperatureDamage()
+    if apply then
+        item:checkTemperatureDamage()
+    end
 end
 
 local last_frame = df.global.world.frame_counter-1
@@ -39,7 +48,11 @@ for _,item in ipairs(df.global.world.items.all) do
     end
 end
 
-print('Items updated: '..count)
+if apply then
+    print('Items updated: '..count)
+else
+    print('Items not in equilibrium: '..count)
+end
 
 local tlist = {}
 for k,_ in pairs(types) do tlist[#tlist+1] = k end
@@ -47,3 +60,7 @@ table.sort(tlist, function(a,b) return types[a] > types[b] end)
 for _,k in ipairs(tlist) do
     print('    '..df.item_type[k]..':', types[k])
 end
+
+if not apply then
+    print("Use 'fix/stable-temp apply' to force-change temperature.")
+end
diff --git a/scripts/gui/liquids.lua b/scripts/gui/liquids.lua
index 869cac908..89f08b7cf 100644
--- a/scripts/gui/liquids.lua
+++ b/scripts/gui/liquids.lua
@@ -3,6 +3,7 @@
 local utils = require 'utils'
 local gui = require 'gui'
 local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
 
 local liquids = require('plugins.liquids')
 
@@ -199,6 +200,42 @@ function LiquidsUI:onRenderBody(dc)
     dc:string("Enter", COLOR_LIGHTGREEN):string(": Paint")
 end
 
+function ensure_blocks(cursor, size, cb)
+    local cx,cy,cz = pos2xyz(cursor)
+    local all = true
+    for x=1,size.x or 1,16 do
+        for y=1,size.y or 1,16 do
+            for z=1,size.z do
+                if not dfhack.maps.getTileBlock(cx+x-1, cy+y-1, cz+z-1) then
+                    all = false
+                end
+            end
+        end
+    end
+    if all then
+        cb()
+        return
+    end
+    dlg.showYesNoPrompt(
+        'Instantiate Blocks',
+        'Not all map blocks are allocated - instantiate?\n\nWarning: new untested feature.',
+        COLOR_YELLOW,
+        function()
+            for x=1,size.x or 1,16 do
+                for y=1,size.y or 1,16 do
+                    for z=1,size.z do
+                        dfhack.maps.ensureTileBlock(cx+x-1, cy+y-1, cz+z-1)
+                    end
+                end
+            end
+            cb()
+        end,
+        function()
+            cb()
+        end
+    )
+end
+
 function LiquidsUI:onInput(keys)
     local paint = self.paint:get()
     local liquid = paint.liquid
@@ -239,13 +276,15 @@ function LiquidsUI:onInput(keys)
         else
             guidm.clearSelection()
         end
-        liquids.paint(
+        local cb = curry(
+            liquids.paint,
             cursor,
             self.brush:get().tag, self.paint:get().tag,
             self.amount, size,
             self.set:get().tag, self.flow:get().tag,
             self.permaflow:get().tag
         )
+        ensure_blocks(cursor, size, cb)
     elseif self:propagateMoveKeys(keys) then
         return
     elseif keys.D_LOOK_ARENA_WATER then
diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua
index 6b4b4042b..c14bfcbe9 100644
--- a/scripts/gui/mechanisms.lua
+++ b/scripts/gui/mechanisms.lua
@@ -122,7 +122,7 @@ function MechanismList:onInput(keys)
     end
 end
 
-if dfhack.gui.getCurFocus() ~= 'dwarfmode/QueryBuilding/Some' then
+if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then
     qerror("This script requires the main dwarfmode view in 'q' mode")
 end
 
diff --git a/scripts/gui/power-meter.lua b/scripts/gui/power-meter.lua
new file mode 100644
index 000000000..8baf43e7e
--- /dev/null
+++ b/scripts/gui/power-meter.lua
@@ -0,0 +1,116 @@
+-- Interface front-end for power-meter plugin.
+
+local utils = require 'utils'
+local gui = require 'gui'
+local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
+
+local plugin = require('plugins.power-meter')
+local bselector = df.global.ui_build_selector
+
+PowerMeter = defclass(PowerMeter, guidm.MenuOverlay)
+
+PowerMeter.focus_path = 'power-meter'
+
+function PowerMeter:init()
+    self:init_fields{
+        min_power = 0, max_power = -1, invert = false,
+    }
+    guidm.MenuOverlay.init(self)
+    return self
+end
+
+function PowerMeter:onShow()
+    guidm.MenuOverlay.onShow(self)
+
+    -- Send an event to update the errors
+    bselector.plate_info.flags.whole = 0
+    self:sendInputToParent('BUILDING_TRIGGER_ENABLE_WATER')
+end
+
+function PowerMeter:onRenderBody(dc)
+    dc:fill(0,0,dc.width-1,13,gui.CLEAR_PEN)
+    dc:seek(1,1):pen(COLOR_WHITE)
+    dc:string("Power Meter"):newline():newline(1)
+    dc:string("Placement"):newline():newline(1)
+
+    dc:string("Excess power range:")
+
+    dc:newline(3):string("as", COLOR_LIGHTGREEN)
+    dc:string(": Min ")
+    if self.min_power <= 0 then
+        dc:string("(any)")
+    else
+        dc:string(''..self.min_power)
+    end
+
+    dc:newline(3):string("zx", COLOR_LIGHTGREEN)
+    dc:string(": Max ")
+    if self.max_power < 0 then
+        dc:string("(any)")
+    else
+        dc:string(''..self.max_power)
+    end
+    dc:newline():newline(1)
+
+    dc:string("i",COLOR_LIGHTGREEN):string(": ")
+    if self.invert then
+        dc:string("Inverted")
+    else
+        dc:string("Not inverted")
+    end
+end
+
+function PowerMeter:onInput(keys)
+    if keys.CUSTOM_I then
+        self.invert = not self.invert
+    elseif keys.BUILDING_TRIGGER_MIN_WATER_UP then
+        self.min_power = self.min_power + 10
+    elseif keys.BUILDING_TRIGGER_MIN_WATER_DOWN then
+        self.min_power = math.max(0, self.min_power - 10)
+    elseif keys.BUILDING_TRIGGER_MAX_WATER_UP then
+        if self.max_power < 0 then
+            self.max_power = 0
+        else
+            self.max_power = self.max_power + 10
+        end
+    elseif keys.BUILDING_TRIGGER_MAX_WATER_DOWN then
+        self.max_power = math.max(-1, self.max_power - 10)
+    elseif keys.LEAVESCREEN then
+        self:dismiss()
+        self:sendInputToParent('LEAVESCREEN')
+    elseif keys.SELECT then
+        if #bselector.errors == 0 then
+            if not plugin.makePowerMeter(
+                bselector.plate_info,
+                self.min_power, self.max_power, self.invert
+            )
+            then
+                dlg.showMessage(
+                    'Power Meter',
+                    'Could not initialize.', COLOR_LIGHTRED
+                )
+
+                self:dismiss()
+                self:sendInputToParent('LEAVESCREEN')
+                return
+            end
+
+            self:sendInputToParent('SELECT')
+            if bselector.stage ~= 1 then
+                self:dismiss()
+            end
+        end
+    elseif self:propagateMoveKeys(keys) then
+        return
+    end
+end
+
+if dfhack.gui.getCurFocus() ~= 'dwarfmode/Build/Position/Trap'
+or bselector.building_subtype ~= df.trap_type.PressurePlate
+then
+    qerror("This script requires the main dwarfmode view in build pressure plate mode")
+end
+
+local list = mkinstance(PowerMeter):init()
+list:show()
diff --git a/scripts/gui/rename.lua b/scripts/gui/rename.lua
new file mode 100644
index 000000000..a457a0bfd
--- /dev/null
+++ b/scripts/gui/rename.lua
@@ -0,0 +1,63 @@
+-- Rename various objects via gui.
+
+local gui = require 'gui'
+local dlg = require 'gui.dialogs'
+local plugin = require 'plugins.rename'
+
+local mode = ...
+local focus = dfhack.gui.getCurFocus()
+
+local function verify_mode(expected)
+    if mode ~= nil and mode ~= expected then
+        qerror('Invalid UI state for mode '..mode)
+    end
+end
+
+if string.match(focus, '^dwarfmode/QueryBuilding/Some') then
+    verify_mode('building')
+
+    local building = df.global.world.selected_building
+    if plugin.canRenameBuilding(building) then
+        dlg.showInputPrompt(
+            'Rename Building',
+            'Enter a new name for the building:', COLOR_GREEN,
+            building.name,
+            curry(plugin.renameBuilding, building)
+        )
+    else
+        dlg.showMessage(
+            'Rename Building',
+            'Cannot rename this type of building.', COLOR_LIGHTRED
+        )
+    end
+elseif dfhack.gui.getSelectedUnit(true) then
+    local unit = dfhack.gui.getSelectedUnit(true)
+
+    if mode == 'unit-profession' then
+        dlg.showInputPrompt(
+            'Rename Unit',
+            'Enter a new profession for the unit:', COLOR_GREEN,
+            unit.custom_profession,
+            function(newval)
+                unit.custom_profession = newval
+            end
+        )
+    else
+        verify_mode('unit')
+
+        local vname = dfhack.units.getVisibleName(unit)
+        local vnick = ''
+        if vname and vname.has_name then
+            vnick = vname.nickname
+        end
+
+        dlg.showInputPrompt(
+            'Rename Unit',
+            'Enter a new nickname for the unit:', COLOR_GREEN,
+            vnick,
+            curry(dfhack.units.setNickname, unit)
+        )
+    end
+elseif mode then
+    verify_mode(nil)
+end
diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua
new file mode 100644
index 000000000..47043cbb1
--- /dev/null
+++ b/scripts/gui/siege-engine.lua
@@ -0,0 +1,490 @@
+-- Front-end for the siege engine plugin.
+
+local utils = require 'utils'
+local gui = require 'gui'
+local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
+
+local plugin = require 'plugins.siege-engine'
+local wmap = df.global.world.map
+
+local LEGENDARY = df.skill_rating.Legendary
+
+-- Globals kept between script calls
+last_target_min = last_target_min or nil
+last_target_max = last_target_max or nil
+
+local item_choices = {
+    { caption = 'boulders (default)', item_type = df.item_type.BOULDER },
+    { caption = 'blocks', item_type = df.item_type.BLOCKS },
+    { caption = 'weapons', item_type = df.item_type.WEAPON },
+    { caption = 'trap components', item_type = df.item_type.TRAPCOMP },
+    { caption = 'bins', item_type = df.item_type.BIN },
+    { caption = 'barrels', item_type = df.item_type.BARREL },
+    { caption = 'anything', item_type = -1 },
+}
+
+local item_choice_idx = {}
+for i,v in ipairs(item_choices) do
+    item_choice_idx[v.item_type] = i
+end
+
+SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay)
+
+SiegeEngine.focus_path = 'siege-engine'
+
+function SiegeEngine:init(building)
+    self:init_fields{
+        building = building,
+        center = utils.getBuildingCenter(building),
+        selected_pile = 1,
+    }
+    guidm.MenuOverlay.init(self)
+    self.mode_main = {
+        render = self:callback 'onRenderBody_main',
+        input = self:callback 'onInput_main',
+    }
+    self.mode_aim = {
+        render = self:callback 'onRenderBody_aim',
+        input = self:callback 'onInput_aim',
+    }
+    self.mode_pile = {
+        render = self:callback 'onRenderBody_pile',
+        input = self:callback 'onInput_pile',
+    }
+    return self
+end
+
+function SiegeEngine:onShow()
+    guidm.MenuOverlay.onShow(self)
+
+    self.old_cursor = guidm.getCursorPos()
+    self.old_viewport = self:getViewport()
+
+    self.mode = self.mode_main
+    self:showCursor(false)
+end
+
+function SiegeEngine:onDestroy()
+    if self.save_profile then
+        plugin.saveWorkshopProfile(self.building)
+    end
+    if not self.no_select_building then
+        self:selectBuilding(self.building, self.old_cursor, self.old_viewport, 10)
+    end
+end
+
+function SiegeEngine:showCursor(enable)
+    local cursor = guidm.getCursorPos()
+    if cursor and not enable then
+        self.cursor = cursor
+        self.target_select_first = nil
+        guidm.clearCursorPos()
+    elseif not cursor and enable then
+        local view = self:getViewport()
+        cursor = self.cursor
+        if not cursor or not view:isVisible(cursor) then
+            cursor = view:getCenter()
+        end
+        self.cursor = nil
+        guidm.setCursorPos(cursor)
+    end
+end
+
+function SiegeEngine:centerViewOn(pos)
+    local cursor = guidm.getCursorPos()
+    if cursor then
+        guidm.setCursorPos(pos)
+    else
+        self.cursor = pos
+    end
+    self:getViewport():centerOn(pos):set()
+end
+
+function SiegeEngine:zoomToTarget()
+    local target_min, target_max = plugin.getTargetArea(self.building)
+    if target_min then
+        local cx = math.floor((target_min.x + target_max.x)/2)
+        local cy = math.floor((target_min.y + target_max.y)/2)
+        local cz = math.floor((target_min.z + target_max.z)/2)
+        local pos = plugin.adjustToTarget(self.building, xyz2pos(cx,cy,cz))
+        self:centerViewOn(pos)
+    end
+end
+
+function paint_target_grid(dc, view, origin, p1, p2)
+    local r1, sz, r2 = guidm.getSelectionRange(p1, p2)
+
+    if view.z < r1.z or view.z > r2.z then
+        return
+    end
+
+    local p1 = view:tileToScreen(r1)
+    local p2 = view:tileToScreen(r2)
+    local org = view:tileToScreen(origin)
+    dc:pen{ fg = COLOR_CYAN, bg = COLOR_CYAN, ch = '+', bold = true }
+
+    -- Frame
+    dc:fill(p1.x,p1.y,p1.x,p2.y)
+    dc:fill(p1.x,p1.y,p2.x,p1.y)
+    dc:fill(p2.x,p1.y,p2.x,p2.y)
+    dc:fill(p1.x,p2.y,p2.x,p2.y)
+
+    -- Grid
+    local gxmin = org.x+10*math.ceil((p1.x-org.x)/10)
+    local gxmax = org.x+10*math.floor((p2.x-org.x)/10)
+    local gymin = org.y+10*math.ceil((p1.y-org.y)/10)
+    local gymax = org.y+10*math.floor((p2.y-org.y)/10)
+    for x = gxmin,gxmax,10 do
+        for y = gymin,gymax,10 do
+            dc:fill(p1.x,y,p2.x,y)
+            dc:fill(x,p1.y,x,p2.y)
+        end
+    end
+end
+
+function SiegeEngine:renderTargetView(target_min, target_max)
+    local view = self:getViewport()
+    local map = self.df_layout.map
+    local map_dc = gui.Painter.new(map)
+
+    plugin.paintAimScreen(
+        self.building, view:getPos(),
+        xy2pos(map.x1, map.y1), view:getSize()
+    )
+
+    if target_min and math.floor(dfhack.getTickCount()/500) % 2 == 0 then
+        paint_target_grid(map_dc, view, self.center, target_min, target_max)
+    end
+
+    local cursor = guidm.getCursorPos()
+    if cursor then
+        local cx, cy, cz = pos2xyz(view:tileToScreen(cursor))
+        if cz == 0 then
+            map_dc:seek(cx,cy):char('X', COLOR_YELLOW)
+        end
+    end
+end
+
+function SiegeEngine:scrollPiles(delta)
+    local links = plugin.getStockpileLinks(self.building)
+    if links then
+        self.selected_pile = 1+(self.selected_pile+delta-1) % #links
+        return links[self.selected_pile]
+    end
+end
+
+function SiegeEngine:renderStockpiles(dc, links, nlines)
+    local idx = (self.selected_pile-1) % #links
+    local page = math.floor(idx/nlines)
+    for i = page*nlines,math.min(#links,(page+1)*nlines)-1 do
+        local color = COLOR_BROWN
+        if i == idx then
+            color = COLOR_YELLOW
+        end
+        dc:newline(2):string(utils.getBuildingName(links[i+1]), color)
+    end
+end
+
+function SiegeEngine:onRenderBody_main(dc)
+    dc:newline(1):pen(COLOR_WHITE):string("Target: ")
+
+    local target_min, target_max = plugin.getTargetArea(self.building)
+    if target_min then
+        dc:string(
+            (target_max.x-target_min.x+1).."x"..
+            (target_max.y-target_min.y+1).."x"..
+            (target_max.z-target_min.z+1).." Rect"
+        )
+    else
+        dc:string("None (default)")
+    end
+
+    dc:newline(3):string("r",COLOR_LIGHTGREEN):string(": Rectangle")
+    if last_target_min then
+        dc:string(", "):string("p",COLOR_LIGHTGREEN):string(": Paste")
+    end
+    dc:newline(3)
+    if target_min then
+        dc:string("x",COLOR_LIGHTGREEN):string(": Clear, ")
+        dc:string("z",COLOR_LIGHTGREEN):string(": Zoom")
+    end
+
+    dc:newline():newline(1)
+    if self.building.type == df.siegeengine_type.Ballista then
+        dc:string("Uses ballista arrows")
+    else
+        local item = plugin.getAmmoItem(self.building)
+        dc:string("u",COLOR_LIGHTGREEN):string(": Use ")
+        if item_choice_idx[item] then
+            dc:string(item_choices[item_choice_idx[item]].caption)
+        else
+            dc:string(df.item_type[item])
+        end
+    end
+
+    dc:newline():newline(1)
+    dc:string("t",COLOR_LIGHTGREEN):string(": Take from stockpile"):newline(3)
+    local links = plugin.getStockpileLinks(self.building)
+    local bottom = dc.height - 5
+    if links then
+        dc:string("d",COLOR_LIGHTGREEN):string(": Delete, ")
+        dc:string("o",COLOR_LIGHTGREEN):string(": Zoom"):newline()
+        self:renderStockpiles(dc, links, bottom-2-dc:localY())
+        dc:newline():newline()
+    end
+
+    local prof = self.building:getWorkshopProfile() or {}
+    dc:seek(1,math.max(dc:localY(),19)):string('ghjk',COLOR_LIGHTGREEN)dc:string(': ')
+    dc:string(df.skill_rating.attrs[prof.min_level or 0].caption):string('-')
+    dc:string(df.skill_rating.attrs[math.min(LEGENDARY,prof.max_level or 3000)].caption)
+    dc:newline():newline()
+
+    if self.target_select_first then
+        self:renderTargetView(self.target_select_first, guidm.getCursorPos())
+    else
+        self:renderTargetView(target_min, target_max)
+    end
+end
+
+function SiegeEngine:setTargetArea(p1, p2)
+    self.target_select_first = nil
+
+    if not plugin.setTargetArea(self.building, p1, p2) then
+        dlg.showMessage(
+            'Set Target Area',
+            'Could not set the target area', COLOR_LIGHTRED
+        )
+    else
+        last_target_min = p1
+        last_target_max = p2
+    end
+end
+
+function SiegeEngine:setAmmoItem(choice)
+    if self.building.type == df.siegeengine_type.Ballista then
+        return
+    end
+
+    if not plugin.setAmmoItem(self.building, choice.item_type) then
+        dlg.showMessage(
+            'Set Ammo Item',
+            'Could not set the ammo item', COLOR_LIGHTRED
+        )
+    end
+end
+
+function SiegeEngine:onInput_main(keys)
+    if keys.CUSTOM_R then
+        self:showCursor(true)
+        self.target_select_first = nil
+        self.mode = self.mode_aim
+    elseif keys.CUSTOM_P and last_target_min then
+        self:setTargetArea(last_target_min, last_target_max)
+    elseif keys.CUSTOM_U then
+        local item = plugin.getAmmoItem(self.building)
+        local idx = 1 + (item_choice_idx[item] or 0) % #item_choices
+        self:setAmmoItem(item_choices[idx])
+    elseif keys.CUSTOM_Z then
+        self:zoomToTarget()
+    elseif keys.CUSTOM_X then
+        plugin.clearTargetArea(self.building)
+    elseif keys.SECONDSCROLL_UP then
+        self:scrollPiles(-1)
+    elseif keys.SECONDSCROLL_DOWN then
+        self:scrollPiles(1)
+    elseif keys.CUSTOM_D then
+        local pile = self:scrollPiles(0)
+        if pile then
+            plugin.removeStockpileLink(self.building, pile)
+        end
+    elseif keys.CUSTOM_O then
+        local pile = self:scrollPiles(0)
+        if pile then
+            self:centerViewOn(utils.getBuildingCenter(pile))
+        end
+    elseif keys.CUSTOM_T then
+        self:showCursor(true)
+        self.mode = self.mode_pile
+        self:sendInputToParent('CURSOR_DOWN_Z')
+        self:sendInputToParent('CURSOR_UP_Z')
+    elseif keys.CUSTOM_G then
+        local prof = plugin.saveWorkshopProfile(self.building)
+        prof.min_level = math.max(0, prof.min_level-1)
+        plugin.saveWorkshopProfile(self.building)
+    elseif keys.CUSTOM_H then
+        local prof = plugin.saveWorkshopProfile(self.building)
+        prof.min_level = math.min(LEGENDARY, prof.min_level+1)
+        plugin.saveWorkshopProfile(self.building)
+    elseif keys.CUSTOM_J then
+        local prof = plugin.saveWorkshopProfile(self.building)
+        prof.max_level = math.max(0, math.min(LEGENDARY,prof.max_level)-1)
+        plugin.saveWorkshopProfile(self.building)
+    elseif keys.CUSTOM_K then
+        local prof = plugin.saveWorkshopProfile(self.building)
+        prof.max_level = math.min(LEGENDARY, prof.max_level+1)
+        if prof.max_level >= LEGENDARY then prof.max_level = 3000 end
+        plugin.saveWorkshopProfile(self.building)
+    elseif self:simulateViewScroll(keys) then
+        self.cursor = nil
+    else
+        return false
+    end
+    return true
+end
+
+local status_table = {
+    ok = { pen = COLOR_GREEN, msg = "Target accessible" },
+    out_of_range = { pen = COLOR_CYAN, msg = "Target out of range" },
+    blocked = { pen = COLOR_RED, msg = "Target obstructed" },
+    semi_blocked = { pen = COLOR_BROWN, msg = "Partially obstructed" },
+}
+
+function SiegeEngine:onRenderBody_aim(dc)
+    local cursor = guidm.getCursorPos()
+    local first = self.target_select_first
+
+    dc:newline(1):string('Select target rectangle'):newline()
+
+    local info = status_table[plugin.getTileStatus(self.building, cursor)]
+    if info then
+        dc:newline(2):string(info.msg, info.pen)
+    else
+        dc:newline(2):string('ERROR', COLOR_RED)
+    end
+
+    dc:newline():newline(1):string("Enter",COLOR_LIGHTGREEN)
+    if first then
+        dc:string(": Finish rectangle")
+    else
+        dc:string(": Start rectangle")
+    end
+    dc:newline()
+
+    local target_min, target_max = plugin.getTargetArea(self.building)
+    if target_min then
+        dc:newline(1):string("z",COLOR_LIGHTGREEN):string(": Zoom to current target")
+    end
+
+    if first then
+        self:renderTargetView(first, cursor)
+    else
+        local target_min, target_max = plugin.getTargetArea(self.building)
+        self:renderTargetView(target_min, target_max)
+    end
+end
+
+function SiegeEngine:onInput_aim(keys)
+    if keys.SELECT then
+        local cursor = guidm.getCursorPos()
+        if self.target_select_first then
+            self:setTargetArea(self.target_select_first, cursor)
+
+            self.mode = self.mode_main
+            self:showCursor(false)
+        else
+            self.target_select_first = cursor
+        end
+    elseif keys.CUSTOM_Z then
+        self:zoomToTarget()
+    elseif keys.LEAVESCREEN then
+        self.mode = self.mode_main
+        self:showCursor(false)
+    elseif self:simulateCursorMovement(keys) then
+        self.cursor = nil
+    else
+        return false
+    end
+    return true
+end
+
+function SiegeEngine:onRenderBody_pile(dc)
+    dc:newline(1):string('Select pile to take from'):newline():newline(2)
+
+    local sel = df.global.world.selected_building
+
+    if df.building_stockpilest:is_instance(sel) then
+        dc:string(utils.getBuildingName(sel), COLOR_GREEN):newline():newline(1)
+
+        if plugin.isLinkedToPile(self.building, sel) then
+            dc:string("Already taking from here"):newline():newline(2)
+            dc:string("d", COLOR_LIGHTGREEN):string(": Delete link")
+        else
+            dc:string("Enter",COLOR_LIGHTGREEN):string(": Take from this pile")
+        end
+    elseif sel then
+        dc:string(utils.getBuildingName(sel), COLOR_DARKGREY)
+        dc:newline():newline(1)
+        dc:string("Not a stockpile",COLOR_LIGHTRED)
+    else
+        dc:string("No building selected", COLOR_DARKGREY)
+    end
+end
+
+function SiegeEngine:onInput_pile(keys)
+    if keys.SELECT then
+        local sel = df.global.world.selected_building
+        if df.building_stockpilest:is_instance(sel)
+        and not plugin.isLinkedToPile(self.building, sel) then
+            plugin.addStockpileLink(self.building, sel)
+
+            df.global.world.selected_building = self.building
+            self.mode = self.mode_main
+            self:showCursor(false)
+        end
+    elseif keys.CUSTOM_D then
+        local sel = df.global.world.selected_building
+        if df.building_stockpilest:is_instance(sel) then
+            plugin.removeStockpileLink(self.building, sel)
+        end
+    elseif keys.LEAVESCREEN then
+        df.global.world.selected_building = self.building
+        self.mode = self.mode_main
+        self:showCursor(false)
+    elseif self:propagateMoveKeys(keys) then
+        --
+    else
+        return false
+    end
+    return true
+end
+
+function SiegeEngine:onRenderBody(dc)
+    dc:clear()
+    dc:seek(1,1):pen(COLOR_WHITE):string(utils.getBuildingName(self.building)):newline()
+
+    self.mode.render(dc)
+
+    dc:seek(1, math.max(dc:localY(), 21)):pen(COLOR_WHITE)
+    dc:string("ESC", COLOR_LIGHTGREEN):string(": Back, ")
+    dc:string("c", COLOR_LIGHTGREEN):string(": Recenter")
+end
+
+function SiegeEngine:onInput(keys)
+    if self.mode.input(keys) then
+        --
+    elseif keys.CUSTOM_C then
+        self:centerViewOn(self.center)
+    elseif keys.LEAVESCREEN then
+        self:dismiss()
+    elseif keys.LEAVESCREEN_ALL then
+        self:dismiss()
+        self.no_select_building = true
+        guidm.clearCursorPos()
+        df.global.ui.main.mode = df.ui_sidebar_mode.Default
+        df.global.world.selected_building = nil
+    end
+end
+
+if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/SiegeEngine') then
+    qerror("This script requires a siege engine selected in 'q' mode")
+end
+
+local building = df.global.world.selected_building
+
+if not df.building_siegeenginest:is_instance(building) then
+    qerror("A siege engine must be selected")
+end
+
+local list = mkinstance(SiegeEngine):init(df.global.world.selected_building)
+list:show()
diff --git a/scripts/setfps.lua b/scripts/setfps.lua
new file mode 100644
index 000000000..690f82702
--- /dev/null
+++ b/scripts/setfps.lua
@@ -0,0 +1,10 @@
+-- Set the FPS cap at runtime.
+
+local cap = ...
+local capnum = tonumber(cap)
+
+if not capnum or capnum < 1 then
+    qerror('Invalid FPS cap value: '..cap)
+end
+
+df.global.enabler.fps = capnum