-
+
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 b5c0f335d..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!
=============================
@@ -1632,16 +1662,18 @@ removebadthoughts
This script remove negative thoughts from your dwarves. Very useful against
tantrum spirals.
-With a selected unit in 'v' mode, will clear this unit's mind, otherwise
-clear all your fort's units minds.
+The script can target a single creature, when used with the ``him`` argument,
+or the whole fort population, with ``all``.
+
+To show every bad thought present without actually removing them, run the
+script with the ``-n`` or ``--dry-run`` argument. This can give a quick
+hint on what bothers your dwarves the most.
Individual dwarf happiness may not increase right after this command is run,
but in the short term your dwarves will get much more joyful.
-The thoughts are set to be very old, and the game will remove them soon when
-you unpause.
-With the optional ``-v`` parameter, the script will dump the negative thoughts
-it removed.
+Internals: the thoughts are set to be very old, so that the game remove them
+quickly after you unpause.
slayrace
@@ -1650,7 +1682,7 @@ Kills any unit of a given race.
With no argument, lists the available races.
-With the special argument 'him', targets only the selected creature.
+With the special argument ``him``, targets only the selected creature.
Any non-dead non-caged unit of the specified race gets its ``blood_count``
set to 0, which means immediate death at the next game tick. For creatures
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