-
+
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