-
+
 This plugin makes reactions with names starting with SPATTER_ADD_
 produce contaminants on the items instead of improvements. The produced
 contaminants are immune to being washed away by water or destroyed by
diff --git a/Readme.rst b/Readme.rst
index 7ce15015a..39d44d975 100644
--- a/Readme.rst
+++ b/Readme.rst
@@ -88,6 +88,36 @@ Interactive commands like 'liquids' cannot be used as hotkeys.
 
 Most of the commands come from plugins. Those reside in 'hack/plugins/'.
 
+Patched binaries
+================
+
+On linux and OSX, users of patched binaries may have to find the relevant
+section in symbols.xml, and add a new line with the checksum of their
+executable::
+
+    
+
+In order to find the correct value of the hash, look into stderr.log;
+DFHack prints an error there if it does not recognize the hash.
+
+DFHack includes a small stand-alone utility for applying and removing
+binary patches from the game executable. Use it from the regular operating
+system console:
+
+ * ``binpatch check "Dwarf Fortress.exe" patch.dif``
+
+   Checks and prints if the patch is currently applied.
+
+ * ``binpatch apply "Dwarf Fortress.exe" patch.dif``
+
+   Applies the patch, unless it is already applied or in conflict.
+
+ * ``binpatch remove "Dwarf Fortress.exe" patch.dif``
+
+   Removes the patch, unless it is already removed.
+
+The patches are expected to be encoded in text format used by IDA.
+
 =============================
 Something doesn't work, help!
 =============================
diff --git a/depends/md5/md5wrapper.cpp b/depends/md5/md5wrapper.cpp
index e12b65780..d9f857c5d 100644
--- a/depends/md5/md5wrapper.cpp
+++ b/depends/md5/md5wrapper.cpp
@@ -36,16 +36,14 @@
  * internal hash function, calling
  * the basic methods from md5.h
  */
-std::string md5wrapper::hashit(std::string text)
+std::string md5wrapper::hashit(unsigned char *data, size_t length)
 {
 	MD5Context ctx;
 
 	//init md5
 	MD5Init(&ctx);
 	//update with our string
-	MD5Update(&ctx,
-		 (unsigned char*)text.c_str(),
-		 text.length());
+	MD5Update(&ctx, data, length);
 
 	//create the hash
 	unsigned char buff[16] = "";
@@ -95,10 +93,9 @@ md5wrapper::~md5wrapper()
  */
 std::string md5wrapper::getHashFromString(std::string text)
 {
-	return this->hashit(text);
+	return this->hashit((unsigned char*)text.data(), text.length());
 }
 
-
 /*
  * creates a MD5 hash from
  * a file specified in "filename" and
diff --git a/depends/md5/md5wrapper.h b/depends/md5/md5wrapper.h
index 1a41192a1..0b534b61d 100644
--- a/depends/md5/md5wrapper.h
+++ b/depends/md5/md5wrapper.h
@@ -31,7 +31,7 @@ class md5wrapper
 		 * internal hash function, calling
 		 * the basic methods from md5.h
 		 */
-		std::string hashit(std::string text);
+		std::string hashit(unsigned char *data, size_t length);
 
 		/*
 		 * converts the numeric giets to
@@ -52,6 +52,10 @@ class md5wrapper
 		 */
 		std::string getHashFromString(std::string text);
 
+		std::string getHashFromBytes(const unsigned char *data, size_t size) {
+			return hashit(const_cast(data),size);
+		}
+
 		/*
 		 * creates a MD5 hash from
 		 * a file specified in "filename" and
diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt
index 536f4d34d..6f33d5c8a 100644
--- a/library/CMakeLists.txt
+++ b/library/CMakeLists.txt
@@ -250,6 +250,9 @@ ADD_DEPENDENCIES(dfhack-client dfhack)
 
 ADD_EXECUTABLE(dfhack-run dfhack-run.cpp)
 
+ADD_EXECUTABLE(binpatch binpatch.cpp)
+TARGET_LINK_LIBRARIES(binpatch dfhack-md5)
+
 IF(BUILD_EGGY)
     SET_TARGET_PROPERTIES(dfhack PROPERTIES OUTPUT_NAME "egg" )
 else()
@@ -329,7 +332,7 @@ install(FILES xml/symbols.xml
 install(FILES ../dfhack.init-example
         DESTINATION ${DFHACK_BINARY_DESTINATION})
 
-install(TARGETS dfhack-run dfhack-client
+install(TARGETS dfhack-run dfhack-client binpatch
         LIBRARY DESTINATION ${DFHACK_LIBRARY_DESTINATION}
         RUNTIME DESTINATION ${DFHACK_LIBRARY_DESTINATION})
 
diff --git a/library/Core.cpp b/library/Core.cpp
index 1015194ad..a8000070f 100644
--- a/library/Core.cpp
+++ b/library/Core.cpp
@@ -627,8 +627,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
         }
         else if(first == "fpause")
         {
-            World * w = getWorld();
-            w->SetPauseState(true);
+            World::SetPauseState(true);
             con.print("The game was forced to pause!\n");
         }
         else if(first == "cls")
@@ -821,6 +820,8 @@ std::string Core::getHackPath()
 #endif
 }
 
+void init_screen_module(Core *);
+
 bool Core::Init()
 {
     if(started)
@@ -866,6 +867,7 @@ bool Core::Init()
 
     // Init global object pointers
     df::global::InitGlobals();
+    init_screen_module(this);
 
     cerr << "Initializing Console.\n";
     // init the console.
@@ -1122,7 +1124,7 @@ void Core::doUpdate(color_ostream &out, bool first_update)
         last_world_data_ptr = new_wdata;
         last_local_map_ptr = new_mapdata;
 
-        getWorld()->ClearPersistentCache();
+        World::ClearPersistentCache();
 
         // and if the world is going away, we report the map change first
         if(had_map)
@@ -1140,7 +1142,7 @@ void Core::doUpdate(color_ostream &out, bool first_update)
 
         if (isMapLoaded() != had_map)
         {
-            getWorld()->ClearPersistentCache();
+            World::ClearPersistentCache();
             onStateChange(out, new_mapdata ? SC_MAP_LOADED : SC_MAP_UNLOADED);
         }
     }
@@ -1678,7 +1680,6 @@ TYPE * Core::get##TYPE() \
     return s_mods.p##TYPE;\
 }
 
-MODULE_GETTER(World);
 MODULE_GETTER(Materials);
 MODULE_GETTER(Notes);
 MODULE_GETTER(Graphic);
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 352e21ccf..0df96f066 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -258,7 +258,7 @@ static PersistentDataItem persistent_by_struct(lua_State *state, int idx)
     int id = lua_tointeger(state, -1);
     lua_pop(state, 1);
 
-    PersistentDataItem ref = Core::getInstance().getWorld()->GetPersistentData(id);
+    PersistentDataItem ref = World::GetPersistentData(id);
 
     if (ref.isValid())
     {
@@ -323,7 +323,7 @@ static PersistentDataItem get_persistent(lua_State *state)
     {
         const char *str = luaL_checkstring(state, 1);
 
-        return Core::getInstance().getWorld()->GetPersistentData(str);
+        return World::GetPersistentData(str);
     }
 }
 
@@ -342,7 +342,7 @@ static int dfhack_persistent_delete(lua_State *state)
 
     auto ref = get_persistent(state);
 
-    bool ok = Core::getInstance().getWorld()->DeletePersistentData(ref);
+    bool ok = World::DeletePersistentData(ref);
 
     lua_pushboolean(state, ok);
     return 1;
@@ -356,7 +356,7 @@ static int dfhack_persistent_get_all(lua_State *state)
     bool prefix = (lua_gettop(state)>=2 ? lua_toboolean(state,2) : false);
 
     std::vector data;
-    Core::getInstance().getWorld()->GetPersistentData(&data, str, prefix);
+    World::GetPersistentData(&data, str, prefix);
 
     if (data.empty())
     {
@@ -396,7 +396,7 @@ static int dfhack_persistent_save(lua_State *state)
 
     if (add)
     {
-        ref = Core::getInstance().getWorld()->AddPersistentData(str);
+        ref = World::AddPersistentData(str);
         added = true;
     }
     else if (lua_getmetatable(state, 1))
@@ -409,13 +409,13 @@ static int dfhack_persistent_save(lua_State *state)
     }
     else
     {
-        ref = Core::getInstance().getWorld()->GetPersistentData(str);
+        ref = World::GetPersistentData(str);
     }
 
     // Auto-add if not found
     if (!ref.isValid())
     {
-        ref = Core::getInstance().getWorld()->AddPersistentData(str);
+        ref = World::AddPersistentData(str);
         if (!ref.isValid())
             luaL_error(state, "cannot create persistent entry");
         added = true;
@@ -1146,6 +1146,7 @@ static const LuaWrapper::FunctionReg dfhack_screen_module[] = {
     WRAPM(Screen, inGraphicsMode),
     WRAPM(Screen, clear),
     WRAPM(Screen, invalidate),
+    WRAPM(Screen, getKeyDisplay),
     { NULL, NULL }
 };
 
diff --git a/library/binpatch.cpp b/library/binpatch.cpp
new file mode 100644
index 000000000..815ac5b92
--- /dev/null
+++ b/library/binpatch.cpp
@@ -0,0 +1,314 @@
+/*
+https://github.com/peterix/dfhack
+Copyright (c) 2011 Petr Mrázek 
+
+A thread-safe logging console with a line editor for windows.
+
+Based on linenoise win32 port,
+copyright 2010, Jon Griffiths .
+All rights reserved.
+Based on linenoise, copyright 2010, Salvatore Sanfilippo .
+The original linenoise can be found at: http://github.com/antirez/linenoise
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice,
+    this list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+  * Neither the name of Redis nor the names of its contributors may be used
+    to endorse or promote products derived from this software without
+    specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+
+using std::cout;
+using std::cerr;
+using std::endl;
+
+typedef unsigned char patch_byte;
+
+struct BinaryPatch {
+    struct Byte {
+        unsigned offset;
+        patch_byte old_val, new_val;
+    };
+    enum State {
+        Conflict = 0,
+        Unapplied = 1,
+        Applied = 2,
+        Partial = 3
+    };
+
+    std::vector entries;
+
+    bool loadDIF(std::string name);
+    State checkState(const patch_byte *ptr, size_t len);
+
+    void apply(patch_byte *ptr, size_t len, bool newv);
+};
+
+inline bool is_hex(char c)
+{
+    return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
+}
+
+bool BinaryPatch::loadDIF(std::string name)
+{
+    entries.clear();
+
+    std::ifstream infile(name);
+    if(infile.bad())
+    {
+        cerr << "Cannot open file: " << name << endl;
+        return false;
+    }
+
+    std::string s;
+    while(std::getline(infile, s))
+    {
+        // Parse lines that begin with "[0-9a-f]+:"
+        size_t idx = s.find(':');
+        if (idx == std::string::npos || idx == 0 || idx > 8)
+            continue;
+
+        bool ok = true;
+        for (size_t i = 0; i < idx; i++)
+            if (!is_hex(s[i]))
+                ok = false;
+        if (!ok)
+            continue;
+
+        unsigned off, oval, nval;
+        int nchar = 0;
+        int cnt = sscanf(s.c_str(), "%x: %x %x%n", &off, &oval, &nval, &nchar);
+
+        if (cnt < 3)
+        {
+            cerr << "Could not parse: " << s << endl;
+            return false;
+        }
+
+        for (size_t i = nchar; i < s.size(); i++)
+        {
+            if (!isspace(s[i]))
+            {
+                cerr << "Garbage at end of line: " << s << endl;
+                return false;
+            }
+        }
+
+        if (oval >= 256 || nval >= 256)
+        {
+            cerr << "Invalid byte values: " << s << endl;
+            return false;
+        }
+
+        Byte bv = { off, patch_byte(oval), patch_byte(nval) };
+        entries.push_back(bv);
+    }
+
+    if (entries.empty())
+    {
+        cerr << "No lines recognized." << endl;
+        return false;
+    }
+
+    return true;
+}
+
+BinaryPatch::State BinaryPatch::checkState(const patch_byte *ptr, size_t len)
+{
+    int state = 0;
+
+    for (size_t i = 0; i < entries.size(); i++)
+    {
+        Byte &bv = entries[i];
+
+        if (bv.offset >= len)
+        {
+            cerr << "Offset out of range: 0x" << std::hex << bv.offset << std::dec << endl;
+            return Conflict;
+        }
+
+        patch_byte cv = ptr[bv.offset];
+        if (bv.old_val == cv)
+            state |= Unapplied;
+        else if (bv.new_val == cv)
+            state |= Applied;
+        else
+        {
+            cerr << std::hex << bv.offset << ": " << bv.old_val << " " << bv.new_val
+                 << ", but currently " << cv << std::dec << endl;
+            return Conflict;
+        }
+    }
+
+    return State(state);
+}
+
+void BinaryPatch::apply(patch_byte *ptr, size_t len, bool newv)
+{
+    for (size_t i = 0; i < entries.size(); i++)
+    {
+        Byte &bv = entries[i];
+        assert (bv.offset < len);
+
+        ptr[bv.offset] = (newv ? bv.new_val : bv.old_val);
+    }
+}
+
+bool load_file(std::vector *pvec, std::string fname)
+{
+    FILE *f = fopen(fname.c_str(), "rb");
+    if (!f)
+    {
+        cerr << "Cannot open file: " << fname << endl;
+        return false;
+    }
+
+    fseek(f, 0, SEEK_END);
+    pvec->resize(ftell(f));
+    fseek(f, 0, SEEK_SET);
+    size_t cnt = fread(pvec->data(), 1, pvec->size(), f);
+    fclose(f);
+
+    return cnt == pvec->size();
+}
+
+bool save_file(const std::vector &pvec, std::string fname)
+{
+    FILE *f = fopen(fname.c_str(), "wb");
+    if (!f)
+    {
+        cerr << "Cannot open file: " << fname << endl;
+        return false;
+    }
+
+    size_t cnt = fwrite(pvec.data(), 1, pvec.size(), f);
+    fclose(f);
+
+    return cnt == pvec.size();
+}
+
+std::string compute_hash(const std::vector &pvec)
+{
+    md5wrapper md5;
+    return md5.getHashFromBytes(pvec.data(), pvec.size());
+}
+
+int main (int argc, char *argv[])
+{
+    if (argc <= 3)
+    {
+        cerr << "Usage: binpatch check|apply|remove  " << endl;
+        return 2;
+    }
+
+    std::string cmd = argv[1];
+
+    if (cmd != "check" && cmd != "apply" && cmd != "remove")
+    {
+        cerr << "Invalid command: " << cmd << endl;
+        return 2;
+    }
+
+    std::string exe_file = argv[2];
+    std::vector bindata;
+    if (!load_file(&bindata, exe_file))
+        return 2;
+
+    BinaryPatch patch;
+    if (!patch.loadDIF(argv[3]))
+        return 2;
+
+    BinaryPatch::State state = patch.checkState(bindata.data(), bindata.size());
+    if (state == BinaryPatch::Conflict)
+        return 1;
+
+    if (cmd == "check")
+    {
+        switch (state)
+        {
+        case BinaryPatch::Unapplied:
+            cout << "Currently not applied." << endl;
+            break;
+        case BinaryPatch::Applied:
+            cout << "Currently applied." << endl;
+            break;
+        case BinaryPatch::Partial:
+            cout << "Currently partially applied." << endl;
+            break;
+        default:
+            break;
+        }
+
+        return 0;
+    }
+    else if (cmd == "apply")
+    {
+        if (state == BinaryPatch::Applied)
+        {
+            cout << "Already applied." << endl;
+            return 0;
+        }
+
+        patch.apply(bindata.data(), bindata.size(), true);
+    }
+    else if (cmd == "remove")
+    {
+        if (state == BinaryPatch::Unapplied)
+        {
+            cout << "Already removed." << endl;
+            return 0;
+        }
+
+        patch.apply(bindata.data(), bindata.size(), false);
+    }
+
+    if (!save_file(bindata, exe_file + ".bak"))
+    {
+        cerr << "Could not create backup." << endl;
+        return 1;
+    }
+
+    if (!save_file(bindata, exe_file))
+        return 1;
+
+    cout << "Patched " << patch.entries.size()
+         << " bytes, new hash: " << compute_hash(bindata) << endl;
+    return 0;
+}
diff --git a/library/include/Core.h b/library/include/Core.h
index d2d7080da..b3db50c74 100644
--- a/library/include/Core.h
+++ b/library/include/Core.h
@@ -54,7 +54,6 @@ namespace DFHack
 {
     class Process;
     class Module;
-    class World;
     class Materials;
     class Notes;
     struct VersionInfo;
@@ -120,8 +119,6 @@ namespace DFHack
         /// Is everything OK?
         bool isValid(void) { return !errorstate; }
 
-        /// get the world module
-        World * getWorld();
         /// get the materials module
         Materials * getMaterials();
         /// get the notes module
@@ -205,7 +202,6 @@ namespace DFHack
         // Module storage
         struct
         {
-            World * pWorld;
             Materials * pMaterials;
             Notes * pNotes;
             Graphic * pGraphic;
diff --git a/library/include/df/custom/coord_path.methods.inc b/library/include/df/custom/coord_path.methods.inc
index 9acebb82a..5421796e3 100644
--- a/library/include/df/custom/coord_path.methods.inc
+++ b/library/include/df/custom/coord_path.methods.inc
@@ -1,5 +1,12 @@
+bool empty() const { return x.empty(); }
 unsigned size() const { return x.size(); }
 
+void clear() {
+    x.clear();
+    y.clear();
+    z.clear();
+}
+
 coord operator[] (unsigned idx) const {
     if (idx >= x.size())
         return coord();
diff --git a/library/include/modules/MapCache.h b/library/include/modules/MapCache.h
index ac083075f..c1c478bc6 100644
--- a/library/include/modules/MapCache.h
+++ b/library/include/modules/MapCache.h
@@ -47,14 +47,6 @@ namespace MapExtras
 
 class DFHACK_EXPORT MapCache;
 
-template inline R index_tile(T &v, df::coord2d p) {
-    return v[p.x&15][p.y&15];
-}
-
-inline bool is_valid_tile_coord(df::coord2d p) {
-    return (p.x & ~15) == 0 && (p.y & ~15) == 0;
-}
-
 class Block;
 
 class BlockInfo
diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h
index 3150acccf..632e8ec13 100644
--- a/library/include/modules/Maps.h
+++ b/library/include/modules/Maps.h
@@ -151,6 +151,21 @@ typedef uint8_t biome_indices40d [9];
  */
 typedef uint16_t t_temperatures [16][16];
 
+/**
+ * Index a tile array by a 2D coordinate, clipping it to mod 16
+ */
+template inline R index_tile(T &v, df::coord2d p) {
+    return v[p.x&15][p.y&15];
+}
+
+/**
+ * Check if a 2D coordinate is in the 0-15 range.
+ */
+inline bool is_valid_tile_coord(df::coord2d p) {
+    return (p.x & ~15) == 0 && (p.y & ~15) == 0;
+}
+
+
 /**
  * The Maps module
  * \ingroup grp_modules
diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h
index a2e64a515..ccd7f2f8d 100644
--- a/library/include/modules/Screen.h
+++ b/library/include/modules/Screen.h
@@ -128,6 +128,9 @@ namespace DFHack
         DFHACK_EXPORT bool show(df::viewscreen *screen, df::viewscreen *before = NULL);
         DFHACK_EXPORT void dismiss(df::viewscreen *screen, bool to_first = false);
         DFHACK_EXPORT bool isDismissed(df::viewscreen *screen);
+
+        /// Retrieve the string representation of the bound key.
+        DFHACK_EXPORT std::string getKeyDisplay(df::interface_key key);
     }
 
     class DFHACK_EXPORT dfhack_viewscreen : public df::viewscreen {
diff --git a/library/include/modules/World.h b/library/include/modules/World.h
index 1cd57e2f2..f4c31dcf3 100644
--- a/library/include/modules/World.h
+++ b/library/include/modules/World.h
@@ -55,8 +55,6 @@ namespace DFHack
     class DFContextShared;
 
     class DFHACK_EXPORT PersistentDataItem {
-        friend class World;
-
         int id;
         std::string key_value;
 
@@ -65,13 +63,17 @@ namespace DFHack
     public:
         static const int NumInts = 7;
 
-        bool isValid() { return id != 0; }
-        int entry_id() { return -id; }
+        bool isValid() const { return id != 0; }
+        int entry_id() const { return -id; }
+
+        int raw_id() const { return id; }
 
-        const std::string &key() { return key_value; }
+        const std::string &key() const { return key_value; }
 
         std::string &val() { return *str_value; }
+        const std::string &val() const { return *str_value; }
         int &ival(int i) { return int_values[i]; }
+        int ival(int i) const { return int_values[i]; }
 
         PersistentDataItem() : id(0), str_value(0), int_values(0) {}
         PersistentDataItem(int id, const std::string &key, std::string *sv, int *iv)
@@ -83,54 +85,42 @@ namespace DFHack
      * \ingroup grp_modules
      * \ingroup grp_world
      */
-    class DFHACK_EXPORT World : public Module
+    namespace World
     {
-        public:
-        World();
-        ~World();
-        bool Start();
-        bool Finish();
-
         ///true if paused, false if not
-        bool ReadPauseState();
+        DFHACK_EXPORT bool ReadPauseState();
         ///true if paused, false if not
-        void SetPauseState(bool paused);
-
-        uint32_t ReadCurrentTick();
-        uint32_t ReadCurrentYear();
-        uint32_t ReadCurrentMonth();
-        uint32_t ReadCurrentDay();
-        uint8_t ReadCurrentWeather();
-        void SetCurrentWeather(uint8_t weather);
-        bool ReadGameMode(t_gamemodes& rd);
-        bool WriteGameMode(const t_gamemodes & wr); // this is very dangerous
-        std::string ReadWorldFolder();
+        DFHACK_EXPORT void SetPauseState(bool paused);
+
+        DFHACK_EXPORT uint32_t ReadCurrentTick();
+        DFHACK_EXPORT uint32_t ReadCurrentYear();
+        DFHACK_EXPORT uint32_t ReadCurrentMonth();
+        DFHACK_EXPORT uint32_t ReadCurrentDay();
+        DFHACK_EXPORT uint8_t ReadCurrentWeather();
+        DFHACK_EXPORT void SetCurrentWeather(uint8_t weather);
+        DFHACK_EXPORT bool ReadGameMode(t_gamemodes& rd);
+        DFHACK_EXPORT bool WriteGameMode(const t_gamemodes & wr); // this is very dangerous
+        DFHACK_EXPORT std::string ReadWorldFolder();
 
         // Store data in fake historical figure names.
         // This ensures that the values are stored in save games.
-        PersistentDataItem AddPersistentData(const std::string &key);
-        PersistentDataItem GetPersistentData(const std::string &key);
-        PersistentDataItem GetPersistentData(int entry_id);
+        DFHACK_EXPORT PersistentDataItem AddPersistentData(const std::string &key);
+        DFHACK_EXPORT PersistentDataItem GetPersistentData(const std::string &key);
+        DFHACK_EXPORT PersistentDataItem GetPersistentData(int entry_id);
         // Calls GetPersistentData(key); if not found, adds and sets added to true.
         // The result can still be not isValid() e.g. if the world is not loaded.
-        PersistentDataItem GetPersistentData(const std::string &key, bool *added);
+        DFHACK_EXPORT PersistentDataItem GetPersistentData(const std::string &key, bool *added);
         // Lists all items with the given key.
         // If prefix is true, search for keys starting with key+"/".
         // GetPersistentData(&vec,"",true) returns all items.
         // Items have alphabetic order by key; same key ordering is undefined.
-        void GetPersistentData(std::vector *vec,
-                               const std::string &key, bool prefix = false);
+        DFHACK_EXPORT void GetPersistentData(std::vector *vec,
+                                             const std::string &key, bool prefix = false);
         // Deletes the item; returns true if success.
-        bool DeletePersistentData(const PersistentDataItem &item);
+        DFHACK_EXPORT bool DeletePersistentData(const PersistentDataItem &item);
 
-        void ClearPersistentCache();
-
-    private:
-        struct Private;
-        Private *d;
-
-        bool BuildPersistentCache();
-    };
+        DFHACK_EXPORT void ClearPersistentCache();
+    }
 }
 #endif
 
diff --git a/library/lua/gui.lua b/library/lua/gui.lua
index 6eaa98606..a5e4f7503 100644
--- a/library/lua/gui.lua
+++ b/library/lua/gui.lua
@@ -93,6 +93,14 @@ function Painter.new(rect, pen)
     return Painter{ rect = rect, pen = pen }
 end
 
+function Painter.new_xy(x1,y1,x2,y2,pen)
+    return Painter{ rect = mkdims_xy(x1,y1,x2,y2), pen = pen }
+end
+
+function Painter.new_wh(x,y,w,h,pen)
+    return Painter{ rect = mkdims_wh(x,y,w,h), pen = pen }
+end
+
 function Painter:isValidPos()
     return self.x >= self.clip_x1 and self.x <= self.clip_x2
        and self.y >= self.clip_y1 and self.y <= self.clip_y2
@@ -210,6 +218,16 @@ function Painter:string(text,pen,...)
     return self:advance(#text, nil)
 end
 
+function Painter:key(code,pen,bg,...)
+    if type(code) == 'string' then
+        code = df.interface_key[code]
+    end
+    return self:string(
+        dscreen.getKeyDisplay(code),
+        pen or COLOR_LIGHTGREEN, bg or self.cur_pen.bg, ...
+    )
+end
+
 ------------------------
 -- Base screen object --
 ------------------------
@@ -292,6 +310,7 @@ function Screen:onResize(w,h)
 end
 
 function Screen:updateLayout()
+    self:invoke_after('postUpdateLayout')
 end
 
 ------------------------
@@ -381,7 +400,7 @@ function FramedScreen:updateFrameSize()
     self.frame_opaque = (gw == 0 and gh == 0)
 end
 
-function FramedScreen:updateLayout()
+function FramedScreen:postUpdateLayout()
     self:updateFrameSize()
 end
 
diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua
index b1a96a558..553bb3a4b 100644
--- a/library/lua/gui/dialogs.lua
+++ b/library/lua/gui/dialogs.lua
@@ -51,10 +51,9 @@ function MessageBox:onRenderBody(dc)
     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')
+        local fr = self.frame_rect
+        local dc2 = gui.Painter.new_xy(fr.x1+1,fr.y2+1,fr.x2-8,fr.y2+1)
+        dc2:key('LEAVESCREEN'):string('/'):key('MENU_CONFIRM')
     end
 end
 
@@ -171,10 +170,10 @@ ListBox = defclass(ListBox, MessageBox)
 ListBox.focus_path = 'ListBox'
 
 ListBox.ATTRS{
-    selection = 0,
+    selection = 1,
     choices = {},
     select_pen = DEFAULT_NIL,
-    on_input = DEFAULT_NIL
+    on_select = DEFAULT_NIL
 }
 
 function InputBox:preinit(info)
@@ -182,84 +181,112 @@ function InputBox:preinit(info)
 end
 
 function ListBox:init(info)
-    self.page_top = 0
+    self.page_top = 1
+end
+
+local function choice_text(entry)
+    if type(entry)=="table" then
+        return entry.caption or entry[1]
+    else
+        return entry
+    end
 end
 
 function ListBox:getWantedFrameSize()
     local mw, mh = ListBox.super.getWantedFrameSize(self)
-    return mw, mh+#self.choices
+    for _,v in ipairs(self.choices) do
+        local text = choice_text(v)
+        mw = math.max(mw, #text+2)
+    end
+    return mw, mh+#self.choices+1
 end
 
-function ListBox:onRenderBody(dc)
-    ListBox.super.onRenderBody(self, dc)
-
-    dc:newline(1)
+function ListBox:postUpdateLayout()
+    self.page_size = self.frame_rect.height - #self.text - 3
+    self:moveCursor(0)
+end
 
-    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]
+function ListBox:moveCursor(delta)
+    local page = math.max(1, self.page_size)
+    local cnt = #self.choices
+    local off = self.selection+delta-1
+    local ds = math.abs(delta)
+
+    if ds > 1 then
+        if off >= cnt+ds-1 then
+            off = 0
+        else
+            off = math.min(cnt-1, off)
         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)
+        if off <= -ds then
+            off = cnt-1
+        else
+            off = math.max(0, off)
         end
     end
+
+    self.selection = 1 + off % cnt
+    self.page_top = 1 + page * math.floor((self.selection-1) / page)
 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
+function ListBox:onRenderBody(dc)
+    ListBox.super.onRenderBody(self, dc)
+
+    dc:newline(1):pen(self.select_pen or COLOR_CYAN)
+
+    local choices = self.choices
+    local iend = math.min(#choices, self.page_top+self.page_size-1)
+
+    for i = self.page_top,iend do
+        local text = choice_text(choices[i])
+        if text then
+            dc.cur_pen.bold = (i == self.selection);
+            dc:string(text)
+        else
+            dc:string('?ERROR?', COLOR_LIGHTRED)
         end
+        dc:newline(1)
     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)
+        if self.on_select then
+            self.on_select(self.selection, choice)
         end
 
-        if choice and choice[2] then
-            choice[2](choice,self.selection) -- maybe reverse the arguments?
+        if choice then
+            local callback = choice.on_select or choice[2]
+            if callback then
+                callback(choice, self.selection)
+            end
         end
     elseif keys.LEAVESCREEN then
         self:dismiss()
         if self.on_cancel then
             self.on_cancel()
         end
-    elseif keys.CURSOR_UP then
+    elseif keys.STANDARDSCROLL_UP then
         self:moveCursor(-1)
-    elseif keys.CURSOR_DOWN then
+    elseif keys.STANDARDSCROLL_DOWN then
         self:moveCursor(1)
-    elseif keys.CURSOR_UP_FAST then
-        self:moveCursor(-10)
-    elseif keys.CURSOR_DOWN_FAST then
-        self:moveCursor(10)
+    elseif keys.STANDARDSCROLL_PAGEUP then
+        self:moveCursor(-self.page_size)
+    elseif keys.STANDARDSCROLL_PAGEDOWN then
+        self:moveCursor(self.page_size)
     end
 end
 
-function showListPrompt(title, text, tcolor, choices, on_input, on_cancel, min_width)
+function showListPrompt(title, text, tcolor, choices, on_select, on_cancel, min_width)
     ListBox{
         frame_title = title,
         text = text,
         text_pen = tcolor,
         choices = choices,
-        on_input = on_input,
+        on_select = on_select,
         on_cancel = on_cancel,
         frame_width = min_width,
     }:show()
diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua
index ba3cfbe6c..125e71220 100644
--- a/library/lua/gui/dwarfmode.lua
+++ b/library/lua/gui/dwarfmode.lua
@@ -253,7 +253,7 @@ end
 
 DwarfOverlay = defclass(DwarfOverlay, gui.Screen)
 
-function DwarfOverlay:updateLayout()
+function DwarfOverlay:postUpdateLayout()
     self.df_layout = getPanelLayout()
 end
 
@@ -352,8 +352,7 @@ end
 
 MenuOverlay = defclass(MenuOverlay, DwarfOverlay)
 
-function MenuOverlay:updateLayout()
-    MenuOverlay.super.updateLayout(self)
+function MenuOverlay:postUpdateLayout()
     self.frame_rect = self.df_layout.menu
 end
 
diff --git a/library/lua/gui/script.lua b/library/lua/gui/script.lua
new file mode 100644
index 000000000..021a4fa52
--- /dev/null
+++ b/library/lua/gui/script.lua
@@ -0,0 +1,151 @@
+-- Support for scripted interaction sequences via coroutines.
+
+local _ENV = mkmodule('gui.script')
+
+local dlg = require('gui.dialogs')
+
+--[[
+  Example:
+
+  start(function()
+    sleep(100, 'frames')
+    print(showYesNoPrompt('test', 'print true?'))
+  end)
+]]
+
+-- Table of running background scripts.
+if not scripts then
+    scripts = {}
+    setmetatable(scripts, { __mode = 'k' })
+end
+
+local function do_resume(inst, ...)
+    inst.gen = inst.gen + 1
+    return (dfhack.saferesume(inst.coro, ...))
+end
+
+-- Starts a new background script by calling the function.
+function start(fn,...)
+    local coro = coroutine.create(fn)
+    local inst = {
+        coro = coro,
+        gen = 0,
+    }
+    scripts[coro] = inst
+    return do_resume(inst, ...)
+end
+
+-- Checks if called from a background script
+function in_script()
+    return scripts[coroutine.running()] ~= nil
+end
+
+local function getinst()
+    local inst = scripts[coroutine.running()]
+    if not inst then
+        error('Not in a gui script coroutine.')
+    end
+    return inst
+end
+
+local function invoke_resume(inst,gen,quiet,...)
+    local state = coroutine.status(inst.coro)
+    if state ~= 'suspended' then
+        if state ~= 'dead' then
+            dfhack.printerr(debug.traceback('resuming a non-waiting continuation'))
+        end
+    elseif inst.gen > gen then
+        if not quiet then
+            dfhack.printerr(debug.traceback('resuming an expired continuation'))
+        end
+    else
+        do_resume(inst, ...)
+    end
+end
+
+-- Returns a callback that resumes the script from wait with given return values
+function mkresume(...)
+    local inst = getinst()
+    return curry(invoke_resume, inst, inst.gen, false, ...)
+end
+
+-- Like mkresume, but does not complain if already resumed from this wait
+function qresume(...)
+    local inst = getinst()
+    return curry(invoke_resume, inst, inst.gen, true, ...)
+end
+
+-- Wait until a mkresume callback is called, then return its arguments.
+-- Once it returns, all mkresume callbacks created before are invalidated.
+function wait()
+    getinst() -- check that the context is right
+    return coroutine.yield()
+end
+
+-- Wraps dfhack.timeout for coroutines.
+function sleep(time, quantity)
+    if dfhack.timeout(time, quantity, mkresume()) then
+        wait()
+        return true
+    else
+        return false
+    end
+end
+
+-- Some dialog wrappers:
+
+function showMessage(title, text, tcolor)
+    dlg.MessageBox{
+        frame_title = title,
+        text = text,
+        text_pen = tcolor,
+        on_close = qresume(nil)
+    }:show()
+
+    return wait()
+end
+
+function showYesNoPrompt(title, text, tcolor)
+    dlg.MessageBox{
+        frame_title = title,
+        text = text,
+        text_pen = tcolor,
+        on_accept = mkresume(true),
+        on_cancel = mkresume(false),
+        on_close = qresume(nil)
+    }:show()
+
+    return wait()
+end
+
+function showInputPrompt(title, text, tcolor, input, min_width)
+    dlg.InputBox{
+        frame_title = title,
+        text = text,
+        text_pen = tcolor,
+        input = input,
+        frame_width = min_width,
+        on_input = mkresume(true),
+        on_cancel = mkresume(false),
+        on_close = qresume(nil)
+    }:show()
+
+    return wait()
+end
+
+function showListPrompt(title, text, tcolor, choices, min_width)
+    dlg.ListBox{
+        frame_title = title,
+        text = text,
+        text_pen = tcolor,
+        choices = choices,
+        frame_width = min_width,
+        on_select = mkresume(true),
+        on_cancel = mkresume(false),
+        on_close = qresume(nil)
+    }:show()
+
+    return wait()
+end
+
+return _ENV
diff --git a/library/lua/memscan.lua b/library/lua/memscan.lua
index 970f821c2..6764a0d58 100644
--- a/library/lua/memscan.lua
+++ b/library/lua/memscan.lua
@@ -358,7 +358,7 @@ end
 
 -- Interactive search utility
 
-function DiffSearcher:find_interactive(prompt,data_type,condition_cb)
+function DiffSearcher:find_interactive(prompt,data_type,condition_cb,iter_limit)
     enum = enum or {}
 
     -- Loop for restarting search from scratch
@@ -374,6 +374,11 @@ function DiffSearcher:find_interactive(prompt,data_type,condition_cb)
         while true do
             print('')
 
+            if iter_limit and ccursor >= iter_limit then
+                dfhack.printerr('  Iteration limit reached without a solution.')
+                break
+            end
+
             local ok, value, delta = condition_cb(ccursor)
 
             ccursor = ccursor + 1
diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp
index 5ef4ce829..482b950ba 100644
--- a/library/modules/Maps.cpp
+++ b/library/modules/Maps.cpp
@@ -454,7 +454,7 @@ df::coord2d Maps::getBlockTileBiomeRgn(df::map_block *block, df::coord2d pos)
     if (!block || !world->world_data)
         return df::coord2d();
 
-    auto des = MapExtras::index_tile(block->designation,pos);
+    auto des = index_tile(block->designation,pos);
     unsigned idx = des.bits.biome;
     if (idx < 9)
     {
@@ -529,8 +529,8 @@ bool Maps::canWalkBetween(df::coord pos1, df::coord pos2)
     if (!block1 || !block2)
         return false;
 
-    auto tile1 = MapExtras::index_tile(block1->walkable, pos1);
-    auto tile2 = MapExtras::index_tile(block2->walkable, pos2);
+    auto tile1 = index_tile(block1->walkable, pos1);
+    auto tile2 = index_tile(block2->walkable, pos2);
 
     return tile1 && tile1 == tile2;
 }
diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp
index 0b9279d2d..29f718266 100644
--- a/library/modules/Screen.cpp
+++ b/library/modules/Screen.cpp
@@ -28,6 +28,7 @@ distribution.
 #include 
 #include 
 #include