-
+
Any files with the .lua extension placed into hack/scripts/*
are automatically used by the DFHack core as commands. The
matching command name consists of the name of the file sans
diff --git a/dfhack.init-example b/dfhack.init-example
index c9408e375..5af527099 100644
--- a/dfhack.init-example
+++ b/dfhack.init-example
@@ -16,6 +16,10 @@ keybinding add Ctrl-K autodump-destroy-item
# quicksave, only in main dwarfmode screen and menu page
keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
+# gui/rename script
+keybinding add Ctrl-Shift-N gui/rename
+keybinding add Ctrl-Shift-P "gui/rename unit-profession"
+
##############################
# Generic adv mode bindings #
##############################
@@ -48,3 +52,25 @@ keybinding add Ctrl-M@dwarfmode/QueryBuilding/Some gui/mechanisms
# browse rooms of same owner
keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list.work
+
+# interface for the liquids plugin
+keybinding add Alt-L@dwarfmode/LookAround gui/liquids
+
+# machine power sensitive pressure plate construction
+keybinding add Ctrl-Shift-M@dwarfmode/Build/Position/Trap gui/power-meter
+
+###################
+# UI logic tweaks #
+###################
+
+# stabilize the cursor of dwarfmode when switching menus
+tweak stable-cursor
+
+# stop military from considering training as 'patrol duty'
+tweak patrol-duty
+
+# display creature weight in build plate menu as ??K, instead of (???df: Max
+tweak readable-build-plate
+
+# improve FPS by squashing endless item temperature update loops
+tweak stable-temp
diff --git a/library/Core.cpp b/library/Core.cpp
index 6a0dea7c2..735359a7f 100644
--- a/library/Core.cpp
+++ b/library/Core.cpp
@@ -219,28 +219,30 @@ static std::string getScriptHelp(std::string path, std::string helpprefix)
return "No help available.";
}
-static std::map listScripts(PluginManager *plug_mgr, std::string path)
+static void listScripts(PluginManager *plug_mgr, std::map &pset, std::string path, bool all, std::string prefix = "")
{
std::vector files;
getdir(path, files);
- std::map pset;
for (size_t i = 0; i < files.size(); i++)
{
if (hasEnding(files[i], ".lua"))
{
std::string help = getScriptHelp(path + files[i], "-- ");
- pset[files[i].substr(0, files[i].size()-4)] = help;
+ pset[prefix + files[i].substr(0, files[i].size()-4)] = help;
}
else if (plug_mgr->eval_ruby && hasEnding(files[i], ".rb"))
{
std::string help = getScriptHelp(path + files[i], "# ");
- pset[files[i].substr(0, files[i].size()-3)] = help;
+ pset[prefix + files[i].substr(0, files[i].size()-3)] = help;
+ }
+ else if (all && !files[i].empty() && files[i][0] != '.')
+ {
+ listScripts(plug_mgr, pset, path+files[i]+"/", all, prefix+files[i]+"/");
}
}
- return pset;
}
static bool fileExists(std::string path)
@@ -335,7 +337,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
con.print("Basic commands:\n"
" help|?|man - This text.\n"
" help COMMAND - Usage help for the given command.\n"
- " ls|dir [PLUGIN] - List available commands. Optionally for single plugin.\n"
+ " ls|dir [-a] [PLUGIN] - List available commands. Optionally for single plugin.\n"
" cls - Clear the console.\n"
" fpause - Force DF to pause.\n"
" die - Force DF to close immediately\n"
@@ -469,6 +471,12 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
}
else if(first == "ls" || first == "dir")
{
+ bool all = false;
+ if (parts.size() && parts[0] == "-a")
+ {
+ all = true;
+ vector_erase_at(parts, 0);
+ }
if(parts.size())
{
string & plugname = parts[0];
@@ -491,7 +499,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
con.print(
"builtin:\n"
" help|?|man - This text or help specific to a plugin.\n"
- " ls [PLUGIN] - List available commands. Optionally for single plugin.\n"
+ " ls [-a] [PLUGIN] - List available commands. Optionally for single plugin.\n"
" cls - Clear the console.\n"
" fpause - Force DF to pause.\n"
" die - Force DF to close immediately\n"
@@ -523,7 +531,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
con.print(" %-22s- %s\n",(*iter).name.c_str(), (*iter).description.c_str());
con.reset_color();
}
- auto scripts = listScripts(plug_mgr, getHackPath() + "scripts/");
+ std::map scripts;
+ listScripts(plug_mgr, scripts, getHackPath() + "scripts/", all);
if (!scripts.empty())
{
con.print("\nscripts:\n");
diff --git a/library/DataDefs.cpp b/library/DataDefs.cpp
index d6604cdb3..fa2aacf78 100644
--- a/library/DataDefs.cpp
+++ b/library/DataDefs.cpp
@@ -218,8 +218,10 @@ virtual_identity::virtual_identity(size_t size, TAllocateFn alloc,
virtual_identity::~virtual_identity()
{
// Remove interpose entries, so that they don't try accessing this object later
- for (int i = interpose_list.size()-1; i >= 0; i--)
- interpose_list[i]->remove();
+ for (auto it = interpose_list.begin(); it != interpose_list.end(); ++it)
+ if (it->second)
+ it->second->on_host_delete(this);
+ interpose_list.clear();
}
/* Vtable name to identity lookup. */
@@ -372,7 +374,7 @@ void DFHack::bitfieldToString(std::vector *pvec, const void *p,
unsigned size, const bitfield_item_info *items)
{
for (unsigned i = 0; i < size; i++) {
- int value = getBitfieldField(p, i, std::min(1,items[i].size));
+ int value = getBitfieldField(p, i, std::max(1,items[i].size));
if (value) {
std::string name = format_key(items[i].name, i);
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 00d4c517d..d39a945dd 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -77,6 +77,9 @@ distribution.
#include "df/job_material_category.h"
#include "df/burrow.h"
#include "df/building_civzonest.h"
+#include "df/region_map_entry.h"
+#include "df/flow_info.h"
+#include "df/unit_misc_trait.h"
#include
#include
@@ -726,6 +729,7 @@ static std::string getOSType()
}
static std::string getDFVersion() { return Core::getInstance().vinfo->getVersion(); }
+static uint32_t getTickCount() { return Core::getInstance().p->getTickCount(); }
static std::string getDFPath() { return Core::getInstance().p->getPath(); }
static std::string getHackPath() { return Core::getInstance().getHackPath(); }
@@ -737,6 +741,7 @@ static const LuaWrapper::FunctionReg dfhack_module[] = {
WRAP(getOSType),
WRAP(getDFVersion),
WRAP(getDFPath),
+ WRAP(getTickCount),
WRAP(getHackPath),
WRAP(isWorldLoaded),
WRAP(isMapLoaded),
@@ -755,7 +760,9 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
WRAPM(Gui, getSelectedUnit),
WRAPM(Gui, getSelectedItem),
WRAPM(Gui, showAnnouncement),
+ WRAPM(Gui, showZoomAnnouncement),
WRAPM(Gui, showPopupAnnouncement),
+ WRAPM(Gui, showAutoAnnouncement),
{ NULL, NULL }
};
@@ -807,12 +814,20 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getVisibleName),
WRAPM(Units, getIdentity),
WRAPM(Units, getNemesis),
+ WRAPM(Units, isCrazed),
+ WRAPM(Units, isOpposedToLife),
+ WRAPM(Units, hasExtravision),
+ WRAPM(Units, isBloodsucker),
+ WRAPM(Units, isMischievous),
+ WRAPM(Units, getMiscTrait),
WRAPM(Units, isDead),
WRAPM(Units, isAlive),
WRAPM(Units, isSane),
WRAPM(Units, isDwarf),
WRAPM(Units, isCitizen),
WRAPM(Units, getAge),
+ WRAPM(Units, getEffectiveSkill),
+ WRAPM(Units, computeMovementSpeed),
WRAPM(Units, getProfessionName),
WRAPM(Units, getCasteProfessionName),
WRAPM(Units, getProfessionColor),
@@ -911,9 +926,17 @@ static const LuaWrapper::FunctionReg dfhack_maps_module[] = {
WRAPM(Maps, getGlobalInitFeature),
WRAPM(Maps, getLocalInitFeature),
WRAPM(Maps, canWalkBetween),
+ WRAPM(Maps, spawnFlow),
{ NULL, NULL }
};
+static int maps_isValidTilePos(lua_State *L)
+{
+ auto pos = CheckCoordXYZ(L, 1, true);
+ lua_pushboolean(L, Maps::isValidTilePos(pos));
+ return 1;
+}
+
static int maps_getTileBlock(lua_State *L)
{
auto pos = CheckCoordXYZ(L, 1, true);
@@ -921,6 +944,13 @@ static int maps_getTileBlock(lua_State *L)
return 1;
}
+static int maps_ensureTileBlock(lua_State *L)
+{
+ auto pos = CheckCoordXYZ(L, 1, true);
+ Lua::PushDFObject(L, Maps::ensureTileBlock(pos));
+ return 1;
+}
+
static int maps_getRegionBiome(lua_State *L)
{
auto pos = CheckCoordXY(L, 1, true);
@@ -931,12 +961,13 @@ static int maps_getRegionBiome(lua_State *L)
static int maps_getTileBiomeRgn(lua_State *L)
{
auto pos = CheckCoordXYZ(L, 1, true);
- Lua::PushPosXY(L, Maps::getTileBiomeRgn(pos));
- return 1;
+ return Lua::PushPosXY(L, Maps::getTileBiomeRgn(pos));
}
static const luaL_Reg dfhack_maps_funcs[] = {
+ { "isValidTilePos", maps_isValidTilePos },
{ "getTileBlock", maps_getTileBlock },
+ { "ensureTileBlock", maps_ensureTileBlock },
{ "getRegionBiome", maps_getRegionBiome },
{ "getTileBiomeRgn", maps_getTileBiomeRgn },
{ NULL, NULL }
@@ -1132,6 +1163,45 @@ static int screen_paintTile(lua_State *L)
return 1;
}
+static int screen_readTile(lua_State *L)
+{
+ int x = luaL_checkint(L, 1);
+ int y = luaL_checkint(L, 2);
+ Pen pen = Screen::readTile(x, y);
+
+ if (!pen.valid())
+ {
+ lua_pushnil(L);
+ }
+ else
+ {
+ lua_newtable(L);
+ lua_pushinteger(L, pen.ch); lua_setfield(L, -2, "ch");
+ lua_pushinteger(L, pen.fg); lua_setfield(L, -2, "fg");
+ lua_pushinteger(L, pen.bg); lua_setfield(L, -2, "bg");
+ lua_pushboolean(L, pen.bold); lua_setfield(L, -2, "bold");
+
+ if (pen.tile)
+ {
+ lua_pushinteger(L, pen.tile); lua_setfield(L, -2, "tile");
+
+ switch (pen.tile_mode) {
+ case Pen::CharColor:
+ lua_pushboolean(L, true); lua_setfield(L, -2, "tile_color");
+ break;
+ case Pen::TileColor:
+ lua_pushinteger(L, pen.tile_fg); lua_setfield(L, -2, "tile_fg");
+ lua_pushinteger(L, pen.tile_bg); lua_setfield(L, -2, "tile_bg");
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ return 1;
+}
+
static int screen_paintString(lua_State *L)
{
Pen pen;
@@ -1236,6 +1306,7 @@ static const luaL_Reg dfhack_screen_funcs[] = {
{ "getMousePos", screen_getMousePos },
{ "getWindowSize", screen_getWindowSize },
{ "paintTile", screen_paintTile },
+ { "readTile", screen_readTile },
{ "paintString", screen_paintString },
{ "fillRect", screen_fillRect },
{ "findGraphicsTile", screen_findGraphicsTile },
diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp
index 7c2c8f8d6..a283d215c 100644
--- a/library/LuaTools.cpp
+++ b/library/LuaTools.cpp
@@ -1211,6 +1211,39 @@ static int dfhack_open_plugin(lua_State *L)
return 0;
}
+static int dfhack_curry_wrap(lua_State *L)
+{
+ int nargs = lua_gettop(L);
+ int ncurry = lua_tointeger(L, lua_upvalueindex(1));
+ int scount = nargs + ncurry;
+
+ luaL_checkstack(L, ncurry, "stack overflow in curry");
+
+ // Insert values in O(N+M) by first shifting the existing data
+ lua_settop(L, scount);
+ for (int i = 0; i < nargs; i++)
+ lua_copy(L, nargs-i, scount-i);
+ for (int i = 1; i <= ncurry; i++)
+ lua_copy(L, lua_upvalueindex(i+1), i);
+
+ lua_callk(L, scount-1, LUA_MULTRET, 0, lua_gettop);
+
+ return lua_gettop(L);
+}
+
+static int dfhack_curry(lua_State *L)
+{
+ luaL_checkany(L, 1);
+ if (lua_isnil(L, 1))
+ luaL_argerror(L, 1, "nil function in curry");
+ if (lua_gettop(L) == 1)
+ return 1;
+ lua_pushinteger(L, lua_gettop(L));
+ lua_insert(L, 1);
+ lua_pushcclosure(L, dfhack_curry_wrap, lua_gettop(L));
+ return 1;
+}
+
bool Lua::IsCoreContext(lua_State *state)
{
// This uses a private field of the lua state to
@@ -1234,6 +1267,7 @@ static const luaL_Reg dfhack_funcs[] = {
{ "call_with_finalizer", dfhack_call_with_finalizer },
{ "with_suspend", lua_dfhack_with_suspend },
{ "open_plugin", dfhack_open_plugin },
+ { "curry", dfhack_curry },
{ NULL, NULL }
};
@@ -1546,6 +1580,9 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state)
lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_BASE_G_TOKEN);
lua_setfield(state, -2, "BASE_G");
+ lua_pushstring(state, DFHACK_VERSION);
+ lua_setfield(state, -2, "VERSION");
+
lua_pushboolean(state, IsCoreContext(state));
lua_setfield(state, -2, "is_core_context");
diff --git a/library/LuaTypes.cpp b/library/LuaTypes.cpp
index e71977960..9f2689fa9 100644
--- a/library/LuaTypes.cpp
+++ b/library/LuaTypes.cpp
@@ -894,7 +894,7 @@ static int meta_bitfield_len(lua_State *state)
static void read_bitfield(lua_State *state, uint8_t *ptr, bitfield_identity *id, int idx)
{
- int size = id->getBits()[idx].size;
+ int size = std::max(1, id->getBits()[idx].size);
int value = getBitfieldField(ptr, idx, size);
if (size <= 1)
@@ -951,7 +951,7 @@ static int meta_bitfield_newindex(lua_State *state)
}
int idx = check_container_index(state, id->getNumBits(), 2, iidx, "write");
- int size = id->getBits()[idx].size;
+ int size = std::max(1, id->getBits()[idx].size);
if (lua_isboolean(state, 3) || lua_isnil(state, 3))
setBitfieldField(ptr, idx, size, lua_toboolean(state, 3));
diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp
index d8b9ff27d..ceb644e60 100644
--- a/library/PluginManager.cpp
+++ b/library/PluginManager.cpp
@@ -186,14 +186,17 @@ Plugin::~Plugin()
bool Plugin::load(color_ostream &con)
{
- RefAutolock lock(access);
- if(state == PS_BROKEN)
- {
- return false;
- }
- else if(state == PS_LOADED)
{
- return true;
+ RefAutolock lock(access);
+ if(state == PS_LOADED)
+ {
+ return true;
+ }
+ else if(state != PS_UNLOADED)
+ {
+ return false;
+ }
+ state = PS_LOADING;
}
// enter suspend
CoreSuspender suspend;
@@ -202,6 +205,7 @@ bool Plugin::load(color_ostream &con)
if(!plug)
{
con.printerr("Can't load plugin %s\n", filename.c_str());
+ RefAutolock lock(access);
state = PS_BROKEN;
return false;
}
@@ -211,6 +215,7 @@ bool Plugin::load(color_ostream &con)
{
con.printerr("Plugin %s has no name or version.\n", filename.c_str());
ClosePlugin(plug);
+ RefAutolock lock(access);
state = PS_BROKEN;
return false;
}
@@ -219,9 +224,11 @@ bool Plugin::load(color_ostream &con)
con.printerr("Plugin %s was not built for this version of DFHack.\n"
"Plugin: %s, DFHack: %s\n", *plug_name, *plug_version, DFHACK_VERSION);
ClosePlugin(plug);
+ RefAutolock lock(access);
state = PS_BROKEN;
return false;
}
+ RefAutolock lock(access);
plugin_init = (command_result (*)(color_ostream &, std::vector &)) LookupPlugin(plug, "plugin_init");
if(!plugin_init)
{
@@ -273,8 +280,11 @@ bool Plugin::unload(color_ostream &con)
}
// wait for all calls to finish
access->wait();
+ state = PS_UNLOADING;
+ access->unlock();
// enter suspend
CoreSuspender suspend;
+ access->lock();
// notify plugin about shutdown, if it has a shutdown function
command_result cr = CR_OK;
if(plugin_shutdown)
diff --git a/library/Process-darwin.cpp b/library/Process-darwin.cpp
index 5a97d9e00..3893cfc5f 100644
--- a/library/Process-darwin.cpp
+++ b/library/Process-darwin.cpp
@@ -27,6 +27,7 @@ distribution.
#include
#include
#include
+#include
#include
@@ -262,6 +263,13 @@ bool Process::getThreadIDs(vector & threads )
return true;
}
+uint32_t Process::getTickCount()
+{
+ struct timeval tp;
+ gettimeofday(&tp, NULL);
+ return (tp.tv_sec * 1000) + (tp.tv_usec / 1000);
+}
+
string Process::getPath()
{
char path[1024];
diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp
index fe8647845..4a66470f9 100644
--- a/library/Process-linux.cpp
+++ b/library/Process-linux.cpp
@@ -27,6 +27,7 @@ distribution.
#include
#include
#include
+#include
#include
#include
@@ -192,6 +193,13 @@ bool Process::getThreadIDs(vector & threads )
return true;
}
+uint32_t Process::getTickCount()
+{
+ struct timeval tp;
+ gettimeofday(&tp, NULL);
+ return (tp.tv_sec * 1000) + (tp.tv_usec / 1000);
+}
+
string Process::getPath()
{
const char * cwd_name = "/proc/self/cwd";
diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp
index 7eb6ff5f7..db58c4d33 100644
--- a/library/Process-windows.cpp
+++ b/library/Process-windows.cpp
@@ -410,6 +410,11 @@ string Process::doReadClassName (void * vptr)
return raw;
}
+uint32_t Process::getTickCount()
+{
+ return GetTickCount();
+}
+
string Process::getPath()
{
HMODULE hmod;
diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp
index 3725ccba7..583ef5184 100644
--- a/library/VTableInterpose.cpp
+++ b/library/VTableInterpose.cpp
@@ -48,6 +48,26 @@ struct MSVC_MPTR {
intptr_t this_shift;
};
+static uint32_t *follow_jmp(void *ptr)
+{
+ uint8_t *p = (uint8_t*)ptr;
+
+ for (;;)
+ {
+ switch (*p)
+ {
+ case 0xE9:
+ p += 5 + *(int32_t*)(p+1);
+ break;
+ case 0xEB:
+ p += 2 + *(int8_t*)(p+1);
+ break;
+ default:
+ return (uint32_t*)p;
+ }
+ }
+}
+
bool DFHack::is_vmethod_pointer_(void *pptr)
{
auto pobj = (MSVC_MPTR*)pptr;
@@ -55,7 +75,7 @@ bool DFHack::is_vmethod_pointer_(void *pptr)
// MSVC implements pointers to vmethods via thunks.
// This expects that they all follow a very specific pattern.
- auto pval = (unsigned*)pobj->method;
+ auto pval = follow_jmp(pobj->method);
switch (pval[0]) {
case 0x20FF018BU: // mov eax, [ecx]; jmp [eax]
case 0x60FF018BU: // mov eax, [ecx]; jmp [eax+0x??]
@@ -71,7 +91,7 @@ int DFHack::vmethod_pointer_to_idx_(void *pptr)
auto pobj = (MSVC_MPTR*)pptr;
if (!pobj->method || pobj->this_shift != 0) return -1;
- auto pval = (unsigned*)pobj->method;
+ auto pval = follow_jmp(pobj->method);
switch (pval[0]) {
case 0x20FF018BU: // mov eax, [ecx]; jmp [eax]
return 0;
@@ -154,6 +174,73 @@ bool virtual_identity::set_vmethod_ptr(int idx, void *ptr)
return Core::getInstance().p->patchMemory(&vtable[idx], &ptr, sizeof(void*));
}
+/*
+ VMethod interposing data structures.
+
+ In order to properly support adding and removing hooks,
+ it is necessary to track them. This is what this class
+ is for. The task is further complicated by propagating
+ hooks to child classes that use exactly the same original
+ vmethod implementation.
+
+ Every applied link contains in the saved_chain field a
+ pointer to the next vmethod body that should be called
+ by the hook the link represents. This is the actual
+ control flow structure that needs to be maintained.
+
+ There also are connections between link objects themselves,
+ which constitute the bookkeeping for doing that. Finally,
+ every link is associated with a fixed virtual_identity host,
+ which represents the point in the class hierarchy where
+ the hook is applied.
+
+ When there are no subclasses (i.e. only one host), the
+ structures look like this:
+
+ +--------------+ +------------+
+ | link1 |-next------->| link2 |-next=NULL
+ |s_c: original |<-------prev-|s_c: $link1 |<--+
+ +--------------+ +------------+ |
+ |
+ host->interpose_list[vmethod_idx] ------+
+ vtable: $link2
+
+ The original vtable entry is stored in the saved_chain of the
+ first link. The interpose_list map points to the last one.
+ The hooks are called in order: link2 -> link1 -> original.
+
+ When there are subclasses that use the same vmethod, but don't
+ hook it, the topmost link gets a set of the child_hosts, and
+ the hosts have the link added to their interpose_list:
+
+ +--------------+ +----------------+
+ | link0 @host0 |<--+-interpose_list-| host1 |
+ | |-child_hosts-+----->| vtable: $link |
+ +--------------+ | | +----------------+
+ | |
+ | | +----------------+
+ +-interpose_list-| host2 |
+ +----->| vtable: $link |
+ +----------------+
+
+ When a child defines its own hook, the child_hosts link is
+ severed and replaced with a child_next pointer to the new
+ hook. The hook still points back the chain with prev.
+ All child links to subclasses of host2 are migrated from
+ link1 to link2.
+
+ +--------------+-next=NULL +--------------+-next=NULL
+ | link1 @host1 |-child_next------->| link2 @host2 |-child_*--->subclasses
+ | |<-------------prev-|s_c: $link1 |
+ +--------------+<-------+ +--------------+<-------+
+ | |
+ +--------------+ | +--------------+ |
+ | host1 |-i_list-+ | host2 |-i_list-+
+ |vtable: $link1| |vtable: $link2|
+ +--------------+ +--------------+
+
+ */
+
void VMethodInterposeLinkBase::set_chain(void *chain)
{
saved_chain = chain;
@@ -162,7 +249,7 @@ void VMethodInterposeLinkBase::set_chain(void *chain)
VMethodInterposeLinkBase::VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr)
: host(host), vmethod_idx(vmethod_idx), interpose_method(interpose_method), chain_mptr(chain_mptr),
- saved_chain(NULL), next(NULL), prev(NULL)
+ applied(false), saved_chain(NULL), next(NULL), prev(NULL)
{
if (vmethod_idx < 0 || interpose_method == NULL)
{
@@ -179,8 +266,83 @@ VMethodInterposeLinkBase::~VMethodInterposeLinkBase()
remove();
}
-bool VMethodInterposeLinkBase::apply()
+VMethodInterposeLinkBase *VMethodInterposeLinkBase::get_first_interpose(virtual_identity *id)
+{
+ auto item = id->interpose_list[vmethod_idx];
+ if (!item)
+ return NULL;
+
+ if (item->host != id)
+ return NULL;
+ while (item->prev && item->prev->host == id)
+ item = item->prev;
+
+ return item;
+}
+
+void VMethodInterposeLinkBase::find_child_hosts(virtual_identity *cur, void *vmptr)
{
+ auto &children = cur->getChildren();
+
+ for (size_t i = 0; i < children.size(); i++)
+ {
+ auto child = static_cast(children[i]);
+ auto base = get_first_interpose(child);
+
+ if (base)
+ {
+ assert(base->prev == NULL);
+
+ if (base->saved_chain != vmptr)
+ continue;
+
+ child_next.insert(base);
+ }
+ else
+ {
+ void *cptr = child->get_vmethod_ptr(vmethod_idx);
+ if (cptr != vmptr)
+ continue;
+
+ child_hosts.insert(child);
+ find_child_hosts(child, vmptr);
+ }
+ }
+}
+
+void VMethodInterposeLinkBase::on_host_delete(virtual_identity *from)
+{
+ if (from == host)
+ {
+ // When in own host, fully delete
+ remove();
+ }
+ else
+ {
+ // Otherwise, drop the link to that child:
+ assert(child_hosts.count(from) != 0 &&
+ from->interpose_list[vmethod_idx] == this);
+
+ // Find and restore the original vmethod ptr
+ auto last = this;
+ while (last->prev) last = last->prev;
+
+ from->set_vmethod_ptr(vmethod_idx, last->saved_chain);
+
+ // Unlink the chains
+ child_hosts.erase(from);
+ from->interpose_list[vmethod_idx] = NULL;
+ }
+}
+
+bool VMethodInterposeLinkBase::apply(bool enable)
+{
+ if (!enable)
+ {
+ remove();
+ return true;
+ }
+
if (is_applied())
return true;
if (!host->vtable_ptr)
@@ -188,33 +350,73 @@ bool VMethodInterposeLinkBase::apply()
// Retrieve the current vtable entry
void *old_ptr = host->get_vmethod_ptr(vmethod_idx);
- assert(old_ptr != NULL);
-
- // Check if there are other interpose entries for the same slot
- VMethodInterposeLinkBase *old_link = NULL;
+ VMethodInterposeLinkBase *old_link = host->interpose_list[vmethod_idx];
- for (int i = host->interpose_list.size()-1; i >= 0; i--)
- {
- if (host->interpose_list[i]->vmethod_idx != vmethod_idx)
- continue;
-
- old_link = host->interpose_list[i];
- assert(old_link->next == NULL && old_ptr == old_link->interpose_method);
- break;
- }
+ assert(old_ptr != NULL && (!old_link || old_link->interpose_method == old_ptr));
// Apply the new method ptr
+ set_chain(old_ptr);
+
if (!host->set_vmethod_ptr(vmethod_idx, interpose_method))
+ {
+ set_chain(NULL);
return false;
+ }
- set_chain(old_ptr);
- host->interpose_list.push_back(this);
+ // Push the current link into the home host
+ applied = true;
+ host->interpose_list[vmethod_idx] = this;
+ prev = old_link;
- // Link into the chain if any
- if (old_link)
+ child_hosts.clear();
+ child_next.clear();
+
+ if (old_link && old_link->host == host)
{
+ // If the old link is home, just push into the plain chain
+ assert(old_link->next == NULL);
old_link->next = this;
- prev = old_link;
+
+ // Child links belong to the topmost local entry
+ child_hosts.swap(old_link->child_hosts);
+ child_next.swap(old_link->child_next);
+ }
+ else
+ {
+ // If creating a new local chain, find children with same vmethod
+ find_child_hosts(host, old_ptr);
+
+ if (old_link)
+ {
+ // Enter the child chain set
+ assert(old_link->child_hosts.count(host));
+ old_link->child_hosts.erase(host);
+ old_link->child_next.insert(this);
+
+ // Subtract our own children from the parent's sets
+ for (auto it = child_next.begin(); it != child_next.end(); ++it)
+ old_link->child_next.erase(*it);
+ for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it)
+ old_link->child_hosts.erase(*it);
+ }
+ }
+
+ // Chain subclass hooks
+ for (auto it = child_next.begin(); it != child_next.end(); ++it)
+ {
+ auto nlink = *it;
+ assert(nlink->saved_chain == old_ptr && nlink->prev == old_link);
+ nlink->set_chain(interpose_method);
+ nlink->prev = this;
+ }
+
+ // Chain passive subclass hosts
+ for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it)
+ {
+ auto nhost = *it;
+ assert(nhost->interpose_list[vmethod_idx] == old_link);
+ nhost->set_vmethod_ptr(vmethod_idx, interpose_method);
+ nhost->interpose_list[vmethod_idx] = this;
}
return true;
@@ -225,25 +427,57 @@ void VMethodInterposeLinkBase::remove()
if (!is_applied())
return;
- // Remove from the list in the identity
- for (int i = host->interpose_list.size()-1; i >= 0; i--)
- if (host->interpose_list[i] == this)
- vector_erase_at(host->interpose_list, i);
-
- // Remove from the chain
+ // Remove the link from prev to this
if (prev)
- prev->next = next;
+ {
+ if (prev->host == host)
+ prev->next = next;
+ else
+ {
+ prev->child_next.erase(this);
+
+ if (next)
+ prev->child_next.insert(next);
+ }
+ }
if (next)
{
next->set_chain(saved_chain);
next->prev = prev;
+
+ assert(child_next.empty() && child_hosts.empty());
}
else
{
+ // Remove from the list in the identity and vtable
+ host->interpose_list[vmethod_idx] = prev;
host->set_vmethod_ptr(vmethod_idx, saved_chain);
+
+ for (auto it = child_next.begin(); it != child_next.end(); ++it)
+ {
+ auto nlink = *it;
+ assert(nlink->saved_chain == interpose_method && nlink->prev == this);
+ nlink->set_chain(saved_chain);
+ nlink->prev = prev;
+ if (prev)
+ prev->child_next.insert(nlink);
+ }
+
+ for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it)
+ {
+ auto nhost = *it;
+ assert(nhost->interpose_list[vmethod_idx] == this);
+ nhost->interpose_list[vmethod_idx] = prev;
+ nhost->set_vmethod_ptr(vmethod_idx, saved_chain);
+ if (prev)
+ prev->child_hosts.insert(nhost);
+ }
}
+ applied = false;
prev = next = NULL;
+ child_next.clear();
+ child_hosts.clear();
set_chain(NULL);
}
diff --git a/library/include/BitArray.h b/library/include/BitArray.h
index fd9bd98fc..ff68ea1d1 100644
--- a/library/include/BitArray.h
+++ b/library/include/BitArray.h
@@ -64,7 +64,7 @@ namespace DFHack
if (newsize == size)
return;
uint8_t* mem = (uint8_t *) realloc(bits, newsize);
- if(!mem)
+ if(!mem && newsize != 0)
throw std::bad_alloc();
bits = mem;
if (newsize > size)
@@ -207,7 +207,7 @@ namespace DFHack
else
{
T* mem = (T*) realloc(m_data, sizeof(T)*new_size);
- if(!mem)
+ if(!mem && new_size != 0)
throw std::bad_alloc();
m_data = mem;
}
diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h
index 6b3aeb3e6..61d5dec41 100644
--- a/library/include/DataDefs.h
+++ b/library/include/DataDefs.h
@@ -303,7 +303,7 @@ namespace DFHack
void *vtable_ptr;
friend class VMethodInterposeLinkBase;
- std::vector interpose_list;
+ std::map interpose_list;
protected:
virtual void doInit(Core *core);
diff --git a/library/include/DataIdentity.h b/library/include/DataIdentity.h
index 0f5fd9e7c..21dc68d1a 100644
--- a/library/include/DataIdentity.h
+++ b/library/include/DataIdentity.h
@@ -390,7 +390,7 @@ namespace df
}
virtual bool resize(void *ptr, int size) {
- ((container*)ptr)->resize(size);
+ ((container*)ptr)->resize(size*8);
return true;
}
diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h
index 0e5f618e2..a226018a6 100644
--- a/library/include/MemAccess.h
+++ b/library/include/MemAccess.h
@@ -281,6 +281,9 @@ namespace DFHack
/// get the DF Process FilePath
std::string getPath();
+ /// millisecond tick count, exactly as DF uses
+ uint32_t getTickCount();
+
/// modify permisions of memory range
bool setPermisions(const t_memrange & range,const t_memrange &trgrange);
diff --git a/library/include/PluginManager.h b/library/include/PluginManager.h
index 25b05ad40..38f0e2e50 100644
--- a/library/include/PluginManager.h
+++ b/library/include/PluginManager.h
@@ -128,7 +128,9 @@ namespace DFHack
{
PS_UNLOADED,
PS_LOADED,
- PS_BROKEN
+ PS_BROKEN,
+ PS_LOADING,
+ PS_UNLOADING
};
friend class PluginManager;
friend class RPCService;
diff --git a/library/include/VTableInterpose.h b/library/include/VTableInterpose.h
index bb7a37ce8..7ba6b67aa 100644
--- a/library/include/VTableInterpose.h
+++ b/library/include/VTableInterpose.h
@@ -134,22 +134,32 @@ namespace DFHack
1) Allow multiple hooks into the same vmethod
2) Auto-remove hooks when a plugin is unloaded.
*/
+ friend class virtual_identity;
virtual_identity *host; // Class with the vtable
int vmethod_idx;
void *interpose_method; // Pointer to the code of the interposing method
void *chain_mptr; // Pointer to the chain field below
+ bool applied;
void *saved_chain; // Previous pointer to the code
VMethodInterposeLinkBase *next, *prev; // Other hooks for the same method
+ // inherited vtable members
+ std::set child_hosts;
+ std::set child_next;
+
void set_chain(void *chain);
+ void on_host_delete(virtual_identity *host);
+
+ VMethodInterposeLinkBase *get_first_interpose(virtual_identity *id);
+ void find_child_hosts(virtual_identity *cur, void *vmptr);
public:
VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr);
~VMethodInterposeLinkBase();
- bool is_applied() { return saved_chain != NULL; }
- bool apply();
+ bool is_applied() { return applied; }
+ bool apply(bool enable = true);
void remove();
};
diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h
index 273d84cee..97e8bd422 100644
--- a/library/include/modules/Gui.h
+++ b/library/include/modules/Gui.h
@@ -32,6 +32,7 @@ distribution.
#include "DataDefs.h"
#include "df/init.h"
#include "df/ui.h"
+#include "df/announcement_type.h"
namespace df {
struct viewscreen;
@@ -92,11 +93,32 @@ namespace DFHack
// Show a plain announcement, or a titan-style popup message
DFHACK_EXPORT void showAnnouncement(std::string message, int color = 7, bool bright = true);
+ DFHACK_EXPORT void showZoomAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true);
DFHACK_EXPORT void showPopupAnnouncement(std::string message, int color = 7, bool bright = true);
+ // Show an announcement with effects determined by announcements.txt
+ DFHACK_EXPORT void showAutoAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true);
+
/*
* Cursor and window coords
*/
+ DFHACK_EXPORT df::coord getViewportPos();
+ DFHACK_EXPORT df::coord getCursorPos();
+
+ static const int AREA_MAP_WIDTH = 23;
+ static const int MENU_WIDTH = 30;
+
+ struct DwarfmodeDims {
+ int map_x1, map_x2, menu_x1, menu_x2, area_x1, area_x2;
+ int y1, y2;
+ bool menu_on, area_on, menu_forced;
+ };
+
+ DFHACK_EXPORT DwarfmodeDims getDwarfmodeViewDims();
+
+ DFHACK_EXPORT void resetDwarfmodeView(bool pause = false);
+ DFHACK_EXPORT bool revealInDwarfmodeMap(df::coord pos, bool center = false);
+
DFHACK_EXPORT bool getViewCoords (int32_t &x, int32_t &y, int32_t &z);
DFHACK_EXPORT bool setViewCoords (const int32_t x, const int32_t y, const int32_t z);
diff --git a/library/include/modules/MapCache.h b/library/include/modules/MapCache.h
index 109a20a41..262e70bbf 100644
--- a/library/include/modules/MapCache.h
+++ b/library/include/modules/MapCache.h
@@ -253,6 +253,8 @@ public:
bool is_valid() { return valid; }
df::map_block *getRaw() { return block; }
+ bool Allocate();
+
MapCache *getParent() { return parent; }
private:
@@ -262,6 +264,8 @@ private:
MapCache *parent;
df::map_block *block;
+ void init();
+
int biomeIndexAt(df::coord2d p);
bool valid;
@@ -347,6 +351,12 @@ class DFHACK_EXPORT MapCache
return BlockAt(df::coord(coord.x>>4,coord.y>>4,coord.z));
}
+ bool ensureBlockAt(df::coord coord)
+ {
+ Block *b = BlockAtTile(coord);
+ return b ? b->Allocate() : false;
+ }
+
df::tiletype baseTiletypeAt (DFCoord tilecoord)
{
Block *b = BlockAtTile(tilecoord);
diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h
index e63eef733..869b21580 100644
--- a/library/include/modules/Maps.h
+++ b/library/include/modules/Maps.h
@@ -50,6 +50,7 @@ distribution.
#include "df/tile_dig_designation.h"
#include "df/tile_traffic.h"
#include "df/feature_init.h"
+#include "df/flow_type.h"
/**
* \defgroup grp_maps Maps module and its types
@@ -232,14 +233,19 @@ extern DFHACK_EXPORT void getSize(uint32_t& x, uint32_t& y, uint32_t& z);
/// get the position of the map on world map
extern DFHACK_EXPORT void getPosition(int32_t& x, int32_t& y, int32_t& z);
+extern DFHACK_EXPORT bool isValidTilePos(int32_t x, int32_t y, int32_t z);
+inline bool isValidTilePos(df::coord pos) { return isValidTilePos(pos.x, pos.y, pos.z); }
+
/**
* Get the map block or NULL if block is not valid
*/
extern DFHACK_EXPORT df::map_block * getBlock (int32_t blockx, int32_t blocky, int32_t blockz);
extern DFHACK_EXPORT df::map_block * getTileBlock (int32_t x, int32_t y, int32_t z);
+extern DFHACK_EXPORT df::map_block * ensureTileBlock (int32_t x, int32_t y, int32_t z);
inline df::map_block * getBlock (df::coord pos) { return getBlock(pos.x, pos.y, pos.z); }
inline df::map_block * getTileBlock (df::coord pos) { return getTileBlock(pos.x, pos.y, pos.z); }
+inline df::map_block * ensureTileBlock (df::coord pos) { return ensureTileBlock(pos.x, pos.y, pos.z); }
extern DFHACK_EXPORT df::tiletype *getTileType(int32_t x, int32_t y, int32_t z);
extern DFHACK_EXPORT df::tile_designation *getTileDesignation(int32_t x, int32_t y, int32_t z);
@@ -258,7 +264,7 @@ inline df::tile_occupancy *getTileOccupancy(df::coord pos) {
/**
* Returns biome info about the specified world region.
*/
-DFHACK_EXPORT df::world_data::T_region_map *getRegionBiome(df::coord2d rgn_pos);
+DFHACK_EXPORT df::region_map_entry *getRegionBiome(df::coord2d rgn_pos);
/**
* Returns biome world region coordinates for the given tile within given block.
@@ -272,6 +278,8 @@ inline df::coord2d getTileBiomeRgn(df::coord pos) {
// Enables per-frame updates for liquid flow and/or temperature.
DFHACK_EXPORT void enableBlockUpdates(df::map_block *blk, bool flow = false, bool temperature = false);
+DFHACK_EXPORT df::flow_info *spawnFlow(df::coord pos, df::flow_type type, int mat_type = 0, int mat_index = -1, int density = 100);
+
/// sorts the block event vector into multiple vectors by type
/// mineral veins, what's under ice, blood smears and mud
extern DFHACK_EXPORT bool SortBlockEvents(df::map_block *block,
diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h
index 492e1eecc..4f47205f2 100644
--- a/library/include/modules/Screen.h
+++ b/library/include/modules/Screen.h
@@ -65,6 +65,9 @@ namespace DFHack
} tile_mode;
int8_t tile_fg, tile_bg;
+ bool valid() const { return tile >= 0; }
+ bool empty() const { return ch == 0 && tile == 0; }
+
Pen(char ch = 0, int8_t fg = 7, int8_t bg = 0, int tile = 0, bool color_tile = false)
: ch(ch), fg(fg&7), bg(bg), bold(!!(fg&8)),
tile(tile), tile_mode(color_tile ? CharColor : AsIs), tile_fg(0), tile_bg(0)
@@ -92,6 +95,9 @@ namespace DFHack
/// Paint one screen tile with the given pen
DFHACK_EXPORT bool paintTile(const Pen &pen, int x, int y);
+ /// Retrieves one screen tile from the buffer
+ DFHACK_EXPORT Pen readTile(int x, int y);
+
/// Paint a string onto the screen. Ignores ch and tile of pen.
DFHACK_EXPORT bool paintString(const Pen &pen, int x, int y, const std::string &text);
diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h
index 9003dc3af..65f0b58a0 100644
--- a/library/include/modules/Units.h
+++ b/library/include/modules/Units.h
@@ -32,6 +32,10 @@ distribution.
#include "modules/Items.h"
#include "DataDefs.h"
#include "df/unit.h"
+#include "df/misc_trait_type.h"
+#include "df/physical_attribute_type.h"
+#include "df/mental_attribute_type.h"
+#include "df/job_skill.h"
namespace df
{
@@ -41,6 +45,7 @@ namespace df
struct historical_entity;
struct entity_position_assignment;
struct entity_position;
+ struct unit_misc_trait;
}
/**
@@ -208,6 +213,18 @@ DFHACK_EXPORT df::language_name *getVisibleName(df::unit *unit);
DFHACK_EXPORT df::assumed_identity *getIdentity(df::unit *unit);
DFHACK_EXPORT df::nemesis_record *getNemesis(df::unit *unit);
+DFHACK_EXPORT bool isHidingCurse(df::unit *unit);
+DFHACK_EXPORT int getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr);
+DFHACK_EXPORT int getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr);
+
+DFHACK_EXPORT bool isCrazed(df::unit *unit);
+DFHACK_EXPORT bool isOpposedToLife(df::unit *unit);
+DFHACK_EXPORT bool hasExtravision(df::unit *unit);
+DFHACK_EXPORT bool isBloodsucker(df::unit *unit);
+DFHACK_EXPORT bool isMischievous(df::unit *unit);
+
+DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create = false);
+
DFHACK_EXPORT bool isDead(df::unit *unit);
DFHACK_EXPORT bool isAlive(df::unit *unit);
DFHACK_EXPORT bool isSane(df::unit *unit);
@@ -216,6 +233,9 @@ DFHACK_EXPORT bool isDwarf(df::unit *unit);
DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false);
+DFHACK_EXPORT int getEffectiveSkill(df::unit *unit, df::job_skill skill_id);
+DFHACK_EXPORT int computeMovementSpeed(df::unit *unit);
+
struct NoblePosition {
df::historical_entity *entity;
df::entity_position_assignment *assignment;
diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua
index 2cbd019a6..baf0d42e0 100644
--- a/library/lua/dfhack.lua
+++ b/library/lua/dfhack.lua
@@ -46,6 +46,7 @@ end
-- Error handling
safecall = dfhack.safecall
+curry = dfhack.curry
function dfhack.pcall(f, ...)
return xpcall(f, dfhack.onerror, ...)
@@ -83,7 +84,7 @@ function mkmodule(module,env)
error("Not a table in package.loaded["..module.."]")
end
end
- local plugname = string.match(module,'^plugins%.(%w+)$')
+ local plugname = string.match(module,'^plugins%.([%w%-]+)$')
if plugname then
dfhack.open_plugin(pkg,plugname)
end
@@ -118,7 +119,12 @@ function defclass(class,parent)
if parent then
setmetatable(class, parent)
else
- rawset_default(class, { init_fields = rawset_default })
+ rawset_default(class, {
+ init_fields = rawset_default,
+ callback = function(self, name, ...)
+ return dfhack.curry(self[name], self, ...)
+ end
+ })
end
return class
end
@@ -163,6 +169,23 @@ function xyz2pos(x,y,z)
end
end
+function pos2xy(pos)
+ if pos then
+ local x = pos.x
+ if x and x ~= -30000 then
+ return x, pos.y
+ end
+ end
+end
+
+function xy2pos(x,y)
+ if x then
+ return {x=x,y=y}
+ else
+ return {x=-30000,y=-30000}
+ end
+end
+
function safe_index(obj,idx,...)
if obj == nil or idx == nil then
return nil
diff --git a/library/lua/gui.lua b/library/lua/gui.lua
index ac032166f..f9b6ab6d2 100644
--- a/library/lua/gui.lua
+++ b/library/lua/gui.lua
@@ -18,7 +18,7 @@ function simulateInput(screen,...)
error('Invalid keycode: '..arg)
end
end
- if type(arg) == 'number' then
+ if type(kv) == 'number' then
keys[#keys+1] = kv
end
end
@@ -94,6 +94,9 @@ function Painter:isValidPos()
end
function Painter:viewport(x,y,w,h)
+ if type(x) == 'table' then
+ x,y,w,h = x.x1, x.y1, x.width, x.height
+ end
local x1,y1 = self.x1+x, self.y1+y
local x2,y2 = x1+w-1, y1+h-1
local vp = {
@@ -159,10 +162,10 @@ function Painter:fill(x1,y1,x2,y2,pen,bg,bold)
if type(x1) == 'table' then
x1, y1, x2, y2, pen, bg, bold = x1.x1, x1.y1, x1.x2, x1.y2, y1, x2, y2
end
- x1 = math.max(x1,self.clip_x1)
- y1 = math.max(y1,self.clip_y1)
- x2 = math.min(x2,self.clip_x2)
- y2 = math.min(y2,self.clip_y2)
+ x1 = math.max(x1+self.x1,self.clip_x1)
+ y1 = math.max(y1+self.y1,self.clip_y1)
+ x2 = math.min(x2+self.x1,self.clip_x2)
+ y2 = math.min(y2+self.y1,self.clip_y2)
dscreen.fillRect(to_pen(self.cur_pen,pen,bg,bold),x1,y1,x2,y2)
return self
end
@@ -277,6 +280,9 @@ end
function Screen:onDismiss()
end
+function Screen:onDestroy()
+end
+
function Screen:onResize(w,h)
self:updateLayout()
end
@@ -350,11 +356,16 @@ local function hint_coord(gap,hint)
end
end
+function FramedScreen:getWantedFrameSize()
+ return self.frame_width, self.frame_height
+end
+
function FramedScreen:updateFrameSize()
local sw, sh = dscreen.getWindowSize()
local iw, ih = sw-2, sh-2
- local width = math.min(self.frame_width or iw, iw)
- local height = math.min(self.frame_height or ih, ih)
+ local fw, fh = self:getWantedFrameSize()
+ local width = math.min(fw or iw, iw)
+ local height = math.min(fh or ih, ih)
local gw, gh = iw-width, ih-height
local x1, y1 = hint_coord(gw,self.frame_xhint), hint_coord(gh,self.frame_yhint)
self.frame_rect = mkdims_wh(x1+1,y1+1,width,height)
diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua
new file mode 100644
index 000000000..eb883465f
--- /dev/null
+++ b/library/lua/gui/dialogs.lua
@@ -0,0 +1,271 @@
+-- Some simple dialog screens
+
+local _ENV = mkmodule('gui.dialogs')
+
+local gui = require('gui')
+local utils = require('utils')
+
+local dscreen = dfhack.screen
+
+MessageBox = defclass(MessageBox, gui.FramedScreen)
+
+MessageBox.focus_path = 'MessageBox'
+MessageBox.frame_style = gui.GREY_LINE_FRAME
+
+function MessageBox:init(info)
+ info = info or {}
+ self:init_fields{
+ text = info.text or {},
+ frame_title = info.title,
+ frame_width = info.frame_width,
+ on_accept = info.on_accept,
+ on_cancel = info.on_cancel,
+ on_close = info.on_close,
+ text_pen = info.text_pen
+ }
+ if type(self.text) == 'string' then
+ self.text = utils.split_string(self.text, "\n")
+ end
+ gui.FramedScreen.init(self, info)
+ return self
+end
+
+function MessageBox:getWantedFrameSize()
+ local text = self.text
+ local w = #(self.frame_title or '') + 4
+ w = math.max(w, 20)
+ w = math.max(self.frame_width or w, w)
+ for _, l in ipairs(text) do
+ w = math.max(w, #l)
+ end
+ local h = #text+1
+ if h > 1 then
+ h = h+1
+ end
+ return w+2, #text+2
+end
+
+function MessageBox:onRenderBody(dc)
+ if #self.text > 0 then
+ dc:newline(1):pen(self.text_pen or COLOR_GREY)
+ for _, l in ipairs(self.text or {}) do
+ dc:string(l):newline(1)
+ end
+ end
+
+ if self.on_accept then
+ local x,y = self.frame_rect.x1+1, self.frame_rect.y2+1
+ dscreen.paintString({fg=COLOR_LIGHTGREEN},x,y,'ESC')
+ dscreen.paintString({fg=COLOR_GREY},x+3,y,'/')
+ dscreen.paintString({fg=COLOR_LIGHTGREEN},x+4,y,'y')
+ end
+end
+
+function MessageBox:onDestroy()
+ if self.on_close then
+ self.on_close()
+ end
+end
+
+function MessageBox:onInput(keys)
+ if keys.MENU_CONFIRM then
+ self:dismiss()
+ if self.on_accept then
+ self.on_accept()
+ end
+ elseif keys.LEAVESCREEN or (keys.SELECT and not self.on_accept) then
+ self:dismiss()
+ if self.on_cancel then
+ self.on_cancel()
+ end
+ end
+end
+
+function showMessage(title, text, tcolor, on_close)
+ mkinstance(MessageBox):init{
+ text = text,
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ on_close = on_close
+ }:show()
+end
+
+function showYesNoPrompt(title, text, tcolor, on_accept, on_cancel)
+ mkinstance(MessageBox):init{
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ on_accept = on_accept,
+ on_cancel = on_cancel,
+ }:show()
+end
+
+InputBox = defclass(InputBox, MessageBox)
+
+InputBox.focus_path = 'InputBox'
+
+function InputBox:init(info)
+ info = info or {}
+ self:init_fields{
+ input = info.input or '',
+ input_pen = info.input_pen,
+ on_input = info.on_input,
+ }
+ MessageBox.init(self, info)
+ self.on_accept = nil
+ return self
+end
+
+function InputBox:getWantedFrameSize()
+ local mw, mh = MessageBox.getWantedFrameSize(self)
+ return mw, mh+2
+end
+
+function InputBox:onRenderBody(dc)
+ MessageBox.onRenderBody(self, dc)
+
+ dc:newline(1)
+ dc:pen(self.input_pen or COLOR_LIGHTCYAN)
+ dc:fill(1,dc:localY(),dc.width-2,dc:localY())
+
+ local cursor = '_'
+ if math.floor(dfhack.getTickCount()/300) % 2 == 0 then
+ cursor = ' '
+ end
+ local txt = self.input .. cursor
+ if #txt > dc.width-2 then
+ txt = string.char(27)..string.sub(txt, #txt-dc.width+4)
+ end
+ dc:string(txt)
+end
+
+function InputBox:onInput(keys)
+ if keys.SELECT then
+ self:dismiss()
+ if self.on_input then
+ self.on_input(self.input)
+ end
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+ if self.on_cancel then
+ self.on_cancel()
+ end
+ elseif keys._STRING then
+ if keys._STRING == 0 then
+ self.input = string.sub(self.input, 1, #self.input-1)
+ else
+ self.input = self.input .. string.char(keys._STRING)
+ end
+ end
+end
+
+function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width)
+ mkinstance(InputBox):init{
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ input = input,
+ on_input = on_input,
+ on_cancel = on_cancel,
+ frame_width = min_width,
+ }:show()
+end
+
+ListBox = defclass(ListBox, MessageBox)
+
+ListBox.focus_path = 'ListBox'
+
+function ListBox:init(info)
+ info = info or {}
+ self:init_fields{
+ selection = info.selection or 0,
+ choices = info.choices or {},
+ select_pen = info.select_pen,
+ on_input = info.on_input,
+ page_top = 0
+ }
+ MessageBox.init(self, info)
+ self.on_accept = nil
+ return self
+end
+
+function ListBox:getWantedFrameSize()
+ local mw, mh = MessageBox.getWantedFrameSize(self)
+ return mw, mh+#self.choices
+end
+
+function ListBox:onRenderBody(dc)
+ MessageBox.onRenderBody(self, dc)
+
+ dc:newline(1)
+
+ if self.selection>dc.height-3 then
+ self.page_top=self.selection-(dc.height-3)
+ elseif self.selection0 then
+ self.page_top=self.selection-1
+ end
+ for i,entry in ipairs(self.choices) do
+ if type(entry)=="table" then
+ entry=entry[1]
+ end
+ if i>self.page_top then
+ if i == self.selection then
+ dc:pen(self.select_pen or COLOR_LIGHTCYAN)
+ else
+ dc:pen(self.text_pen or COLOR_GREY)
+ end
+ dc:string(entry)
+ dc:newline(1)
+ end
+ end
+end
+function ListBox:moveCursor(delta)
+ local newsel=self.selection+delta
+ if #self.choices ~=0 then
+ if newsel<1 or newsel>#self.choices then
+ newsel=newsel % #self.choices
+ end
+ end
+ self.selection=newsel
+end
+function ListBox:onInput(keys)
+ if keys.SELECT then
+ self:dismiss()
+ local choice=self.choices[self.selection]
+ if self.on_input then
+ self.on_input(self.selection,choice)
+ end
+
+ if choice and choice[2] then
+ choice[2](choice,self.selection) -- maybe reverse the arguments?
+ end
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+ if self.on_cancel then
+ self.on_cancel()
+ end
+ elseif keys.CURSOR_UP then
+ self:moveCursor(-1)
+ elseif keys.CURSOR_DOWN then
+ self:moveCursor(1)
+ elseif keys.CURSOR_UP_FAST then
+ self:moveCursor(-10)
+ elseif keys.CURSOR_DOWN_FAST then
+ self:moveCursor(10)
+ end
+end
+
+function showListPrompt(title, text, tcolor, choices, on_input, on_cancel, min_width)
+ mkinstance(ListBox):init{
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ choices = choices,
+ on_input = on_input,
+ on_cancel = on_cancel,
+ frame_width = min_width,
+ }:show()
+end
+
+return _ENV
diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua
index c1a8bcb95..661e15591 100644
--- a/library/lua/gui/dwarfmode.lua
+++ b/library/lua/gui/dwarfmode.lua
@@ -6,6 +6,9 @@ local gui = require('gui')
local utils = require('utils')
local dscreen = dfhack.screen
+
+local g_cursor = df.global.cursor
+local g_sel_rect = df.global.selection_rect
local world_map = df.global.world.map
AREA_MAP_WIDTH = 23
@@ -43,8 +46,8 @@ function getPanelLayout()
end
function getCursorPos()
- if df.global.cursor.x ~= -30000 then
- return copyall(df.global.cursor)
+ if g_cursor.x ~= -30000 then
+ return copyall(g_cursor)
end
end
@@ -56,6 +59,51 @@ function clearCursorPos()
df.global.cursor = xyz2pos(nil)
end
+function getSelection()
+ local p1, p2
+ if g_sel_rect.start_x ~= -30000 then
+ p1 = xyz2pos(g_sel_rect.start_x, g_sel_rect.start_y, g_sel_rect.start_z)
+ end
+ if g_sel_rect.end_x ~= -30000 then
+ p2 = xyz2pos(g_sel_rect.end_x, g_sel_rect.end_y, g_sel_rect.end_z)
+ end
+ return p1, p2
+end
+
+function setSelectionStart(pos)
+ g_sel_rect.start_x = pos.x
+ g_sel_rect.start_y = pos.y
+ g_sel_rect.start_z = pos.z
+end
+
+function setSelectionEnd(pos)
+ g_sel_rect.end_x = pos.x
+ g_sel_rect.end_y = pos.y
+ g_sel_rect.end_z = pos.z
+end
+
+function clearSelection()
+ g_sel_rect.start_x = -30000
+ g_sel_rect.start_y = -30000
+ g_sel_rect.start_z = -30000
+ g_sel_rect.end_x = -30000
+ g_sel_rect.end_y = -30000
+ g_sel_rect.end_z = -30000
+end
+
+function getSelectionRange(p1, p2)
+ local r1 = xyz2pos(
+ math.min(p1.x, p2.x), math.min(p1.y, p2.y), math.min(p1.z, p2.z)
+ )
+ local r2 = xyz2pos(
+ math.max(p1.x, p2.x), math.max(p1.y, p2.y), math.max(p1.z, p2.z)
+ )
+ local sz = xyz2pos(
+ r2.x - r1.x + 1, r2.y - r1.y + 1, r2.z - r1.z + 1
+ )
+ return r1, sz, r2
+end
+
Viewport = defclass(Viewport)
function Viewport.make(map,x,y,z)
@@ -88,6 +136,14 @@ function Viewport:set()
return vp
end
+function Viewport:getPos()
+ return xyz2pos(self.x1, self.y1, self.z)
+end
+
+function Viewport:getSize()
+ return xy2pos(self.width, self.height)
+end
+
function Viewport:clip(x,y,z)
return self:make(
math.max(0, math.min(x or self.x1, world_map.x_count-self.width)),
@@ -111,6 +167,18 @@ function Viewport:isVisible(target,gap)
return self:isVisibleXY(target,gap) and target.z == self.z
end
+function Viewport:tileToScreen(coord)
+ return xyz2pos(coord.x - self.x1, coord.y - self.y1, coord.z - self.z)
+end
+
+function Viewport:getCenter()
+ return xyz2pos(
+ math.floor((self.x2+self.x1)/2),
+ math.floor((self.y2+self.y1)/2),
+ self.z
+ )
+end
+
function Viewport:centerOn(target)
return self:clip(
target.x - math.floor(self.width/2),
@@ -159,16 +227,24 @@ MOVEMENT_KEYS = {
CURSOR_UP_Z_AUX = { 0, 0, 1 }, CURSOR_DOWN_Z_AUX = { 0, 0, -1 },
}
-function Viewport:scrollByKey(key)
+local function get_movement_delta(key, delta, big_step)
local info = MOVEMENT_KEYS[key]
if info then
- local delta = 10
- if info[4] then delta = 20 end
+ if info[4] then
+ delta = big_step
+ end
+ return delta*info[1], delta*info[2], info[3]
+ end
+end
+
+function Viewport:scrollByKey(key)
+ local dx, dy, dz = get_movement_delta(key, 10, 20)
+ if dx then
return self:clip(
- self.x1 + delta*info[1],
- self.y1 + delta*info[2],
- self.z + info[3]
+ self.x1 + dx,
+ self.y1 + dy,
+ self.z + dz
)
else
return self
@@ -189,16 +265,23 @@ function DwarfOverlay:getViewport(old_vp)
end
end
-function DwarfOverlay:moveCursorTo(cursor,viewport)
+function DwarfOverlay:moveCursorTo(cursor,viewport,gap)
setCursorPos(cursor)
- self:getViewport(viewport):reveal(cursor, 5, 0, 10):set()
+ self:zoomViewportTo(cursor,viewport,gap)
end
-function DwarfOverlay:selectBuilding(building,cursor,viewport)
+function DwarfOverlay:zoomViewportTo(target, viewport, gap)
+ if gap and self:getViewport():isVisible(target, gap) then
+ return
+ end
+ self:getViewport(viewport):reveal(target, 5, 0, 10):set()
+end
+
+function DwarfOverlay:selectBuilding(building,cursor,viewport,gap)
cursor = cursor or utils.getBuildingCenter(building)
df.global.world.selected_building = building
- self:moveCursorTo(cursor, viewport)
+ self:moveCursorTo(cursor, viewport, gap)
end
function DwarfOverlay:propagateMoveKeys(keys)
@@ -234,6 +317,31 @@ function DwarfOverlay:simulateViewScroll(keys, anchor, no_clip_cursor)
end
end
+function DwarfOverlay:simulateCursorMovement(keys, anchor)
+ local layout = self.df_layout
+ local cursor = getCursorPos()
+ local cx, cy, cz = pos2xyz(cursor)
+
+ if anchor and keys.A_MOVE_SAME_SQUARE then
+ setCursorPos(anchor)
+ self:getViewport():centerOn(anchor):set()
+ return 'A_MOVE_SAME_SQUARE'
+ end
+
+ for code,_ in pairs(MOVEMENT_KEYS) do
+ if keys[code] then
+ local dx, dy, dz = get_movement_delta(code, 1, 10)
+ local ncur = xyz2pos(cx+dx, cy+dy, cz+dz)
+
+ if dfhack.maps.isValidTilePos(ncur) then
+ setCursorPos(ncur)
+ self:getViewport():reveal(ncur,4,10,6,true):set()
+ return code
+ end
+ end
+ end
+end
+
function DwarfOverlay:onAboutToShow(below)
local screen = dfhack.gui.getCurViewscreen()
if below then screen = below.parent end
diff --git a/library/lua/utils.lua b/library/lua/utils.lua
index 19a4e6f6a..9fa473ed8 100644
--- a/library/lua/utils.lua
+++ b/library/lua/utils.lua
@@ -381,6 +381,19 @@ function getBuildingCenter(building)
return xyz2pos(building.centerx, building.centery, building.z)
end
+function split_string(self, delimiter)
+ local result = { }
+ local from = 1
+ local delim_from, delim_to = string.find( self, delimiter, from )
+ while delim_from do
+ table.insert( result, string.sub( self, from , delim_from-1 ) )
+ from = delim_to + 1
+ delim_from, delim_to = string.find( self, delimiter, from )
+ end
+ table.insert( result, string.sub( self, from ) )
+ return result
+end
+
-- Ask a yes-no question
function prompt_yes_no(msg,default)
local prompt = msg
diff --git a/library/modules/Buildings.cpp b/library/modules/Buildings.cpp
index d1aed8979..9e78edd3e 100644
--- a/library/modules/Buildings.cpp
+++ b/library/modules/Buildings.cpp
@@ -324,7 +324,7 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in
{
auto obj = (df::building_trapst*)bld;
if (obj->trap_type == trap_type::PressurePlate)
- obj->unk_cc = 500;
+ obj->ready_timeout = 500;
break;
}
default:
diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp
index 91a17e998..91df14eaf 100644
--- a/library/modules/Gui.cpp
+++ b/library/modules/Gui.cpp
@@ -43,6 +43,7 @@ using namespace DFHack;
#include "modules/Job.h"
#include "modules/Screen.h"
+#include "modules/Maps.h"
#include "DataDefs.h"
#include "df/world.h"
@@ -81,6 +82,8 @@ using namespace DFHack;
#include "df/graphic.h"
#include "df/layer_object_listst.h"
#include "df/assign_trade_status.h"
+#include "df/announcement_flags.h"
+#include "df/announcements.h"
using namespace df::enums;
using df::global::gview;
@@ -88,6 +91,9 @@ using df::global::init;
using df::global::gps;
using df::global::ui;
using df::global::world;
+using df::global::selection_rect;
+using df::global::ui_menu_width;
+using df::global::ui_area_map_width;
static df::layer_object_listst *getLayerList(df::viewscreen_layerst *layer, int idx)
{
@@ -167,10 +173,9 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
else if (id == &df::building_trapst::_identity)
{
auto trap = (df::building_trapst*)selected;
- if (trap->trap_type == trap_type::Lever) {
- focus += "/Lever";
+ focus += "/" + enum_item_key(trap->trap_type);
+ if (trap->trap_type == trap_type::Lever)
jobs = true;
- }
}
else if (ui_building_in_assign && *ui_building_in_assign &&
ui_building_assign_type && ui_building_assign_units &&
@@ -183,6 +188,8 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
focus += unit ? "/Unit" : "/None";
}
}
+ else
+ focus += "/" + enum_item_key(selected->getType());
if (jobs)
{
@@ -205,7 +212,14 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
if (ui_build_selector->building_type < 0)
focus += "/Type";
else if (ui_build_selector->stage != 2)
- focus += "/Position";
+ {
+ if (ui_build_selector->stage != 1)
+ focus += "/NoMaterials";
+ else
+ focus += "/Position";
+
+ focus += "/" + enum_item_key(ui_build_selector->building_type);
+ }
else
{
focus += "/Material";
@@ -921,8 +935,9 @@ df::item *Gui::getSelectedItem(color_ostream &out, bool quiet)
//
-void Gui::showAnnouncement(std::string message, int color, bool bright)
-{
+static void doShowAnnouncement(
+ df::announcement_type type, df::coord pos, std::string message, int color, bool bright
+) {
using df::global::world;
using df::global::cur_year;
using df::global::cur_year_tick;
@@ -948,6 +963,9 @@ void Gui::showAnnouncement(std::string message, int color, bool bright)
{
df::report *new_rep = new df::report();
+ new_rep->type = type;
+ new_rep->pos = pos;
+
new_rep->color = color;
new_rep->bright = bright;
new_rep->year = year;
@@ -969,7 +987,17 @@ void Gui::showAnnouncement(std::string message, int color, bool bright)
world->status.announcements.push_back(new_rep);
world->status.display_timer = 2000;
}
+}
+void Gui::showAnnouncement(std::string message, int color, bool bright)
+{
+ doShowAnnouncement(df::announcement_type(0), df::coord(), message, color, bright);
+}
+
+void Gui::showZoomAnnouncement(
+ df::announcement_type type, df::coord pos, std::string message, int color, bool bright
+) {
+ doShowAnnouncement(type, pos, message, color, bright);
}
void Gui::showPopupAnnouncement(std::string message, int color, bool bright)
@@ -983,6 +1011,29 @@ void Gui::showPopupAnnouncement(std::string message, int color, bool bright)
world->status.popups.push_back(popup);
}
+void Gui::showAutoAnnouncement(
+ df::announcement_type type, df::coord pos, std::string message, int color, bool bright
+) {
+ using df::global::announcements;
+
+ df::announcement_flags flags;
+ if (is_valid_enum_item(type) && announcements)
+ flags = announcements->flags[type];
+
+ doShowAnnouncement(type, pos, message, color, bright);
+
+ if (flags.bits.DO_MEGA || flags.bits.PAUSE || flags.bits.RECENTER)
+ {
+ resetDwarfmodeView(flags.bits.DO_MEGA || flags.bits.PAUSE);
+
+ if (flags.bits.RECENTER && pos.isValid())
+ revealInDwarfmodeMap(pos, true);
+ }
+
+ if (flags.bits.DO_MEGA)
+ showPopupAnnouncement(message, color, bright);
+}
+
df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed)
{
df::viewscreen * ws = &gview->view;
@@ -998,6 +1049,127 @@ df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed)
return ws;
}
+df::coord Gui::getViewportPos()
+{
+ if (!df::global::window_x || !df::global::window_y || !df::global::window_z)
+ return df::coord(0,0,0);
+
+ return df::coord(*df::global::window_x, *df::global::window_y, *df::global::window_z);
+}
+
+df::coord Gui::getCursorPos()
+{
+ using df::global::cursor;
+ if (!cursor)
+ return df::coord();
+
+ return df::coord(cursor->x, cursor->y, cursor->z);
+}
+
+Gui::DwarfmodeDims Gui::getDwarfmodeViewDims()
+{
+ DwarfmodeDims dims;
+
+ auto ws = Screen::getWindowSize();
+ dims.y1 = 1;
+ dims.y2 = ws.y-2;
+ dims.map_x1 = 1;
+ dims.map_x2 = ws.x-2;
+ dims.area_x1 = dims.area_x2 = dims.menu_x1 = dims.menu_x2 = -1;
+ dims.menu_forced = false;
+
+ int menu_pos = (ui_menu_width ? *ui_menu_width : 2);
+ int area_pos = (ui_area_map_width ? *ui_area_map_width : 3);
+
+ if (ui && ui->main.mode && menu_pos >= area_pos)
+ {
+ dims.menu_forced = true;
+ menu_pos = area_pos-1;
+ }
+
+ dims.area_on = (area_pos < 3);
+ dims.menu_on = (menu_pos < area_pos);
+
+ if (dims.menu_on)
+ {
+ dims.menu_x2 = ws.x - 2;
+ dims.menu_x1 = dims.menu_x2 - Gui::MENU_WIDTH + 1;
+ if (menu_pos == 1)
+ dims.menu_x1 -= Gui::AREA_MAP_WIDTH + 1;
+ dims.map_x2 = dims.menu_x1 - 2;
+ }
+ if (dims.area_on)
+ {
+ dims.area_x2 = ws.x-2;
+ dims.area_x1 = dims.area_x2 - Gui::AREA_MAP_WIDTH + 1;
+ if (dims.menu_on)
+ dims.menu_x2 = dims.area_x1 - 2;
+ else
+ dims.map_x2 = dims.area_x1 - 2;
+ }
+
+ return dims;
+}
+
+void Gui::resetDwarfmodeView(bool pause)
+{
+ using df::global::cursor;
+
+ if (ui)
+ {
+ ui->follow_unit = -1;
+ ui->follow_item = -1;
+ ui->main.mode = ui_sidebar_mode::Default;
+ }
+
+ if (selection_rect)
+ {
+ selection_rect->start_x = -30000;
+ selection_rect->end_x = -30000;
+ }
+
+ if (cursor)
+ cursor->x = cursor->y = cursor->z = -30000;
+
+ if (pause && df::global::pause_state)
+ *df::global::pause_state = true;
+}
+
+bool Gui::revealInDwarfmodeMap(df::coord pos, bool center)
+{
+ using df::global::window_x;
+ using df::global::window_y;
+ using df::global::window_z;
+
+ if (!window_x || !window_y || !window_z || !world)
+ return false;
+ if (!Maps::isValidTilePos(pos))
+ return false;
+
+ auto dims = getDwarfmodeViewDims();
+ int w = dims.map_x2 - dims.map_x1 + 1;
+ int h = dims.y2 - dims.y1 + 1;
+
+ *window_z = pos.z;
+
+ if (center)
+ {
+ *window_x = pos.x - w/2;
+ *window_y = pos.y - h/2;
+ }
+ else
+ {
+ while (*window_x + w < pos.x+5) *window_x += 10;
+ while (*window_y + h < pos.y+5) *window_y += 10;
+ while (*window_x + 5 > pos.x) *window_x -= 10;
+ while (*window_y + 5 > pos.y) *window_y -= 10;
+ }
+
+ *window_x = std::max(0, std::min(*window_x, world->map.x_count-w));
+ *window_y = std::max(0, std::min(*window_y, world->map.y_count-h));
+ return true;
+}
+
bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z)
{
x = *df::global::window_x;
diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp
index 3ab156d77..d0401164a 100644
--- a/library/modules/Maps.cpp
+++ b/library/modules/Maps.cpp
@@ -57,6 +57,8 @@ using namespace std;
#include "df/builtin_mats.h"
#include "df/block_square_event_grassst.h"
#include "df/z_level_flags.h"
+#include "df/region_map_entry.h"
+#include "df/flow_info.h"
using namespace DFHack;
using namespace df::enums;
@@ -137,17 +139,57 @@ df::map_block *Maps::getBlock (int32_t blockx, int32_t blocky, int32_t blockz)
return world->map.block_index[blockx][blocky][blockz];
}
-df::map_block *Maps::getTileBlock (int32_t x, int32_t y, int32_t z)
+bool Maps::isValidTilePos(int32_t x, int32_t y, int32_t z)
{
if (!IsValid())
- return NULL;
+ return false;
if ((x < 0) || (y < 0) || (z < 0))
- return NULL;
+ return false;
if ((x >= world->map.x_count) || (y >= world->map.y_count) || (z >= world->map.z_count))
+ return false;
+ return true;
+}
+
+df::map_block *Maps::getTileBlock (int32_t x, int32_t y, int32_t z)
+{
+ if (!isValidTilePos(x,y,z))
return NULL;
return world->map.block_index[x >> 4][y >> 4][z];
}
+df::map_block *Maps::ensureTileBlock (int32_t x, int32_t y, int32_t z)
+{
+ if (!isValidTilePos(x,y,z))
+ return NULL;
+
+ auto column = world->map.block_index[x >> 4][y >> 4];
+ auto &slot = column[z];
+ if (slot)
+ return slot;
+
+ // Find another block below
+ int z2 = z;
+ while (z2 >= 0 && !column[z2]) z2--;
+ if (z2 < 0)
+ return NULL;
+
+ slot = new df::map_block();
+ slot->region_pos = column[z2]->region_pos;
+ slot->map_pos = column[z2]->map_pos;
+ slot->map_pos.z = z;
+
+ // Assume sky
+ df::tile_designation dsgn(0);
+ dsgn.bits.light = true;
+ dsgn.bits.outside = true;
+
+ for (int tx = 0; tx < 16; tx++)
+ for (int ty = 0; ty < 16; ty++)
+ slot->designation[tx][ty] = dsgn;
+
+ return slot;
+}
+
df::tiletype *Maps::getTileType(int32_t x, int32_t y, int32_t z)
{
df::map_block *block = getTileBlock(x,y,z);
@@ -166,7 +208,7 @@ df::tile_occupancy *Maps::getTileOccupancy(int32_t x, int32_t y, int32_t z)
return block ? &block->occupancy[x&15][y&15] : NULL;
}
-df::world_data::T_region_map *Maps::getRegionBiome(df::coord2d rgn_pos)
+df::region_map_entry *Maps::getRegionBiome(df::coord2d rgn_pos)
{
auto data = world->world_data;
if (!data)
@@ -203,6 +245,26 @@ void Maps::enableBlockUpdates(df::map_block *blk, bool flow, bool temperature)
}
}
+df::flow_info *Maps::spawnFlow(df::coord pos, df::flow_type type, int mat_type, int mat_index, int density)
+{
+ using df::global::flows;
+
+ auto block = getTileBlock(pos);
+ if (!flows || !block)
+ return NULL;
+
+ auto flow = new df::flow_info();
+ flow->type = type;
+ flow->mat_type = mat_type;
+ flow->mat_index = mat_index;
+ flow->density = std::min(100, density);
+ flow->pos = pos;
+
+ block->flows.push_back(flow);
+ flows->push_back(flow);
+ return flow;
+}
+
df::feature_init *Maps::getGlobalInitFeature(int32_t index)
{
auto data = world->world_data;
@@ -484,8 +546,14 @@ MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent)
valid = false;
bcoord = _bcoord;
block = Maps::getBlock(bcoord);
- item_counts = NULL;
tags = NULL;
+
+ init();
+}
+
+void MapExtras::Block::init()
+{
+ item_counts = NULL;
tiles = NULL;
basemats = NULL;
@@ -508,6 +576,23 @@ MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent)
}
}
+bool MapExtras::Block::Allocate()
+{
+ if (block)
+ return true;
+
+ block = Maps::ensureTileBlock(bcoord.x*16, bcoord.y*16, bcoord.z);
+ if (!block)
+ return false;
+
+ delete item_counts;
+ delete tiles;
+ delete basemats;
+ init();
+
+ return true;
+}
+
MapExtras::Block::~Block()
{
delete[] item_counts;
diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp
index c2377f2ca..9f258fe02 100644
--- a/library/modules/Screen.cpp
+++ b/library/modules/Screen.cpp
@@ -100,7 +100,7 @@ static void doSetTile(const Pen &pen, int index)
bool Screen::paintTile(const Pen &pen, int x, int y)
{
- if (!gps) return false;
+ if (!gps || !pen.valid()) return false;
int dimx = gps->dimx, dimy = gps->dimy;
if (x < 0 || x >= dimx || y < 0 || y >= dimy) return false;
@@ -109,6 +109,41 @@ bool Screen::paintTile(const Pen &pen, int x, int y)
return true;
}
+Pen Screen::readTile(int x, int y)
+{
+ if (!gps) return Pen(0,0,0,-1);
+
+ int dimx = gps->dimx, dimy = gps->dimy;
+ if (x < 0 || x >= dimx || y < 0 || y >= dimy)
+ return Pen(0,0,0,-1);
+
+ int index = x*dimy + y;
+ auto screen = gps->screen + index*4;
+ if (screen[3] & 0x80)
+ return Pen(0,0,0,-1);
+
+ Pen pen(
+ screen[0], screen[1], screen[2], screen[3]?true:false,
+ gps->screentexpos[index]
+ );
+
+ if (pen.tile)
+ {
+ if (gps->screentexpos_grayscale[index])
+ {
+ pen.tile_mode = Screen::Pen::TileColor;
+ pen.tile_fg = gps->screentexpos_cf[index];
+ pen.tile_bg = gps->screentexpos_cbr[index];
+ }
+ else if (gps->screentexpos_addcolor[index])
+ {
+ pen.tile_mode = Screen::Pen::CharColor;
+ }
+ }
+
+ return pen;
+}
+
bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text)
{
if (!gps || y < 0 || y >= gps->dimy) return false;
@@ -132,7 +167,7 @@ bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text)
bool Screen::fillRect(const Pen &pen, int x1, int y1, int x2, int y2)
{
- if (!gps) return false;
+ if (!gps || !pen.valid()) return false;
if (x1 < 0) x1 = 0;
if (y1 < 0) y1 = 0;
diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp
index 282157a3f..01b7b50f4 100644
--- a/library/modules/Units.cpp
+++ b/library/modules/Units.cpp
@@ -63,11 +63,15 @@ using namespace std;
#include "df/burrow.h"
#include "df/creature_raw.h"
#include "df/caste_raw.h"
+#include "df/game_mode.h"
+#include "df/unit_misc_trait.h"
+#include "df/unit_skill.h"
using namespace DFHack;
using namespace df::enums;
using df::global::world;
using df::global::ui;
+using df::global::gamemode;
bool Units::isValid()
{
@@ -613,6 +617,58 @@ df::nemesis_record *Units::getNemesis(df::unit *unit)
return NULL;
}
+
+bool Units::isHidingCurse(df::unit *unit)
+{
+ if (!unit->job.hunt_target)
+ {
+ auto identity = Units::getIdentity(unit);
+ if (identity && identity->unk_4c == 0)
+ return true;
+ }
+
+ return false;
+}
+
+int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr)
+{
+ auto &aobj = unit->body.physical_attrs[attr];
+ int value = std::max(0, aobj.value - aobj.soft_demotion);
+
+ if (auto mod = unit->curse.attr_change)
+ {
+ int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr];
+
+ if (isHidingCurse(unit))
+ value = std::min(value, mvalue);
+ else
+ value = mvalue;
+ }
+
+ return std::max(0, value);
+}
+
+int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr)
+{
+ auto soul = unit->status.current_soul;
+ if (!soul) return 0;
+
+ auto &aobj = soul->mental_attrs[attr];
+ int value = std::max(0, aobj.value - aobj.soft_demotion);
+
+ if (auto mod = unit->curse.attr_change)
+ {
+ int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr];
+
+ if (isHidingCurse(unit))
+ value = std::min(value, mvalue);
+ else
+ value = mvalue;
+ }
+
+ return std::max(0, value);
+}
+
static bool casteFlagSet(int race, int caste, df::caste_raw_flags flag)
{
auto creature = df::creature_raw::find(race);
@@ -626,8 +682,9 @@ static bool casteFlagSet(int race, int caste, df::caste_raw_flags flag)
return craw->flags.is_set(flag);
}
-static bool isCrazed(df::unit *unit)
+bool Units::isCrazed(df::unit *unit)
{
+ CHECK_NULL_POINTER(unit);
if (unit->flags3.bits.scuttle)
return false;
if (unit->curse.rem_tags1.bits.CRAZED)
@@ -637,13 +694,64 @@ static bool isCrazed(df::unit *unit)
return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED);
}
-static bool isOpposedToLife(df::unit *unit)
+bool Units::isOpposedToLife(df::unit *unit)
{
+ CHECK_NULL_POINTER(unit);
if (unit->curse.rem_tags1.bits.OPPOSED_TO_LIFE)
return false;
if (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE)
return true;
- return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CANNOT_UNDEAD);
+ return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE);
+}
+
+bool Units::hasExtravision(df::unit *unit)
+{
+ CHECK_NULL_POINTER(unit);
+ if (unit->curse.rem_tags1.bits.EXTRAVISION)
+ return false;
+ if (unit->curse.add_tags1.bits.EXTRAVISION)
+ return true;
+ return casteFlagSet(unit->race, unit->caste, caste_raw_flags::EXTRAVISION);
+}
+
+bool Units::isBloodsucker(df::unit *unit)
+{
+ CHECK_NULL_POINTER(unit);
+ if (unit->curse.rem_tags1.bits.BLOODSUCKER)
+ return false;
+ if (unit->curse.add_tags1.bits.BLOODSUCKER)
+ return true;
+ return casteFlagSet(unit->race, unit->caste, caste_raw_flags::BLOODSUCKER);
+}
+
+bool Units::isMischievous(df::unit *unit)
+{
+ CHECK_NULL_POINTER(unit);
+ if (unit->curse.rem_tags1.bits.MISCHIEVOUS)
+ return false;
+ if (unit->curse.add_tags1.bits.MISCHIEVOUS)
+ return true;
+ return casteFlagSet(unit->race, unit->caste, caste_raw_flags::MISCHIEVOUS);
+}
+
+df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create)
+{
+ CHECK_NULL_POINTER(unit);
+
+ auto &vec = unit->status.misc_traits;
+ for (size_t i = 0; i < vec.size(); i++)
+ if (vec[i]->id == type)
+ return vec[i];
+
+ if (create)
+ {
+ auto obj = new df::unit_misc_trait();
+ obj->id = type;
+ vec.push_back(obj);
+ return obj;
+ }
+
+ return NULL;
}
bool DFHack::Units::isDead(df::unit *unit)
@@ -753,6 +861,371 @@ double DFHack::Units::getAge(df::unit *unit, bool true_age)
return cur_time - birth_time;
}
+inline void adjust_skill_rating(int &rating, bool is_adventure, int value, int dwarf3_4, int dwarf1_2, int adv9_10, int adv3_4, int adv1_2)
+{
+ if (is_adventure)
+ {
+ if (value >= adv1_2) rating >>= 1;
+ else if (value >= adv3_4) rating = rating*3/4;
+ else if (value >= adv9_10) rating = rating*9/10;
+ }
+ else
+ {
+ if (value >= dwarf1_2) rating >>= 1;
+ else if (value >= dwarf3_4) rating = rating*3/4;
+ }
+}
+
+int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id)
+{
+ CHECK_NULL_POINTER(unit);
+
+ /*
+ * This is 100% reverse-engineered from DF code.
+ */
+
+ if (!unit->status.current_soul)
+ return 0;
+
+ // Retrieve skill from unit soul:
+
+ df::enum_field key(skill_id);
+ auto skill = binsearch_in_vector(unit->status.current_soul->skills, &df::unit_skill::id, key);
+
+ int rating = 0;
+ if (skill)
+ rating = std::max(0, int(skill->rating) - skill->rusty);
+
+ // Apply special states
+
+ if (unit->counters.soldier_mood == df::unit::T_counters::None)
+ {
+ if (unit->counters.nausea > 0) rating >>= 1;
+ if (unit->counters.winded > 0) rating >>= 1;
+ if (unit->counters.stunned > 0) rating >>= 1;
+ if (unit->counters.dizziness > 0) rating >>= 1;
+ if (unit->counters2.fever > 0) rating >>= 1;
+ }
+
+ if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance)
+ {
+ if (!unit->flags3.bits.ghostly && !unit->flags3.bits.scuttle &&
+ !unit->flags2.bits.vision_good && !unit->flags2.bits.vision_damaged &&
+ !hasExtravision(unit))
+ {
+ rating >>= 2;
+ }
+ if (unit->counters.pain >= 100 && unit->mood == -1)
+ {
+ rating >>= 1;
+ }
+ if (unit->counters2.exhaustion >= 2000)
+ {
+ rating = rating*3/4;
+ if (unit->counters2.exhaustion >= 4000)
+ {
+ rating = rating*3/4;
+ if (unit->counters2.exhaustion >= 6000)
+ rating = rating*3/4;
+ }
+ }
+ }
+
+ // Hunger etc timers
+
+ bool is_adventure = (gamemode && *gamemode == game_mode::ADVENTURE);
+
+ if (!unit->flags3.bits.scuttle && isBloodsucker(unit))
+ {
+ using namespace df::enums::misc_trait_type;
+
+ if (auto trait = getMiscTrait(unit, TimeSinceSuckedBlood))
+ {
+ adjust_skill_rating(
+ rating, is_adventure, trait->value,
+ 302400, 403200, // dwf 3/4; 1/2
+ 1209600, 1209600, 2419200 // adv 9/10; 3/4; 1/2
+ );
+ }
+ }
+
+ adjust_skill_rating(
+ rating, is_adventure, unit->counters2.thirst_timer,
+ 50000, 50000, 115200, 172800, 345600
+ );
+ adjust_skill_rating(
+ rating, is_adventure, unit->counters2.hunger_timer,
+ 75000, 75000, 172800, 1209600, 2592000
+ );
+ if (is_adventure && unit->counters2.sleepiness_timer >= 846000)
+ rating >>= 2;
+ else
+ adjust_skill_rating(
+ rating, is_adventure, unit->counters2.sleepiness_timer,
+ 150000, 150000, 172800, 259200, 345600
+ );
+
+ return rating;
+}
+
+inline void adjust_speed_rating(int &rating, bool is_adventure, int value, int dwarf100, int dwarf200, int adv50, int adv75, int adv100, int adv200)
+{
+ if (is_adventure)
+ {
+ if (value >= adv200) rating += 200;
+ else if (value >= adv100) rating += 100;
+ else if (value >= adv75) rating += 75;
+ else if (value >= adv50) rating += 50;
+ }
+ else
+ {
+ if (value >= dwarf200) rating += 200;
+ else if (value >= dwarf100) rating += 100;
+ }
+}
+
+static int calcInventoryWeight(df::unit *unit)
+{
+ int armor_skill = Units::getEffectiveSkill(unit, job_skill::ARMOR);
+ int armor_mul = 15 - std::min(15, armor_skill);
+
+ int inv_weight = 0, inv_weight_fraction = 0;
+
+ for (size_t i = 0; i < unit->inventory.size(); i++)
+ {
+ auto item = unit->inventory[i]->item;
+ if (!item->flags.bits.weight_computed)
+ continue;
+
+ int wval = item->weight;
+ int wfval = item->weight_fraction;
+ auto mode = unit->inventory[i]->mode;
+
+ if ((mode == df::unit_inventory_item::Worn ||
+ mode == df::unit_inventory_item::WrappedAround) &&
+ item->isArmor() && armor_skill > 1)
+ {
+ wval = wval * armor_mul / 16;
+ wfval = wfval * armor_mul / 16;
+ }
+
+ inv_weight += wval;
+ inv_weight_fraction += wfval;
+ }
+
+ return inv_weight*100 + inv_weight_fraction/10000;
+}
+
+int Units::computeMovementSpeed(df::unit *unit)
+{
+ using namespace df::enums::physical_attribute_type;
+
+ /*
+ * Pure reverse-engineered computation of unit _slowness_,
+ * i.e. number of ticks to move * 100.
+ */
+
+ // Base speed
+
+ auto creature = df::creature_raw::find(unit->race);
+ if (!creature)
+ return 0;
+
+ auto craw = vector_get(creature->caste, unit->caste);
+ if (!craw)
+ return 0;
+
+ int speed = craw->misc.speed;
+
+ if (unit->flags3.bits.ghostly)
+ return speed;
+
+ // Curse multiplier
+
+ if (unit->curse.speed_mul_percent != 100)
+ {
+ speed *= 100;
+ if (unit->curse.speed_mul_percent != 0)
+ speed /= unit->curse.speed_mul_percent;
+ }
+
+ speed += unit->curse.speed_add;
+
+ // Swimming
+
+ auto cur_liquid = unit->status2.liquid_type.bits.liquid_type;
+ bool in_magma = (cur_liquid == tile_liquid::Magma);
+
+ if (unit->flags2.bits.swimming)
+ {
+ speed = craw->misc.swim_speed;
+ if (in_magma)
+ speed *= 2;
+
+ if (craw->flags.is_set(caste_raw_flags::SWIMS_LEARNED))
+ {
+ int skill = Units::getEffectiveSkill(unit, job_skill::SWIMMING);
+
+ // Originally a switch:
+ if (skill > 1)
+ speed = speed * std::max(6, 21-skill) / 20;
+ }
+ }
+ else
+ {
+ int delta = 150*unit->status2.liquid_depth;
+ if (in_magma)
+ delta *= 2;
+ speed += delta;
+ }
+
+ // General counters and flags
+
+ if (unit->profession == profession::BABY)
+ speed += 3000;
+
+ if (unit->flags3.bits.unk15)
+ speed /= 20;
+
+ if (unit->counters2.exhaustion >= 2000)
+ {
+ speed += 200;
+ if (unit->counters2.exhaustion >= 4000)
+ {
+ speed += 200;
+ if (unit->counters2.exhaustion >= 6000)
+ speed += 200;
+ }
+ }
+
+ if (unit->flags2.bits.gutted) speed += 2000;
+
+ if (unit->counters.soldier_mood == df::unit::T_counters::None)
+ {
+ if (unit->counters.nausea > 0) speed += 1000;
+ if (unit->counters.winded > 0) speed += 1000;
+ if (unit->counters.stunned > 0) speed += 1000;
+ if (unit->counters.dizziness > 0) speed += 1000;
+ if (unit->counters2.fever > 0) speed += 1000;
+ }
+
+ if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance)
+ {
+ if (unit->counters.pain >= 100 && unit->mood == -1)
+ speed += 1000;
+ }
+
+ // Hunger etc timers
+
+ bool is_adventure = (gamemode && *gamemode == game_mode::ADVENTURE);
+
+ if (!unit->flags3.bits.scuttle && Units::isBloodsucker(unit))
+ {
+ using namespace df::enums::misc_trait_type;
+
+ if (auto trait = Units::getMiscTrait(unit, TimeSinceSuckedBlood))
+ {
+ adjust_speed_rating(
+ speed, is_adventure, trait->value,
+ 302400, 403200, // dwf 100; 200
+ 1209600, 1209600, 1209600, 2419200 // adv 50; 75; 100; 200
+ );
+ }
+ }
+
+ adjust_speed_rating(
+ speed, is_adventure, unit->counters2.thirst_timer,
+ 50000, 0x7fffffff, 172800, 172800, 172800, 345600
+ );
+ adjust_speed_rating(
+ speed, is_adventure, unit->counters2.hunger_timer,
+ 75000, 0x7fffffff, 1209600, 1209600, 1209600, 2592000
+ );
+ adjust_speed_rating(
+ speed, is_adventure, unit->counters2.sleepiness_timer,
+ 57600, 150000, 172800, 259200, 345600, 864000
+ );
+
+ // Activity state
+
+ if (unit->relations.draggee_id != -1) speed += 1000;
+
+ if (unit->flags1.bits.on_ground)
+ speed += 2000;
+ else if (unit->flags3.bits.on_crutch)
+ {
+ int skill = Units::getEffectiveSkill(unit, job_skill::CRUTCH_WALK);
+ speed += 2000 - 100*std::min(20, skill);
+ }
+
+ if (unit->flags1.bits.hidden_in_ambush && !Units::isMischievous(unit))
+ {
+ int skill = Units::getEffectiveSkill(unit, job_skill::SNEAK);
+ speed += 2000 - 100*std::min(20, skill);
+ }
+
+ if (unsigned(unit->counters2.paralysis-1) <= 98)
+ speed += unit->counters2.paralysis*10;
+ if (unsigned(unit->counters.webbed-1) <= 8)
+ speed += unit->counters.webbed*100;
+
+ // Muscle weight vs vascular tissue (?)
+
+ auto &attr_tissue = unit->body.physical_attr_tissues;
+ int muscle = attr_tissue[STRENGTH];
+ int blood = attr_tissue[AGILITY];
+ speed = std::max(speed*3/4, std::min(speed*3/2, int(int64_t(speed)*muscle/blood)));
+
+ // Attributes
+
+ int strength_attr = Units::getPhysicalAttrValue(unit, STRENGTH);
+ int agility_attr = Units::getPhysicalAttrValue(unit, AGILITY);
+
+ int total_attr = std::max(200, std::min(3800, strength_attr + agility_attr));
+ speed = ((total_attr-200)*(speed/2) + (3800-total_attr)*(speed*3/2))/3600;
+
+ // Stance
+
+ if (!unit->flags1.bits.on_ground && unit->status2.able_stand > 2)
+ {
+ // WTF
+ int as = unit->status2.able_stand;
+ int x = (as-1) - (as>>1);
+ int y = as - unit->status2.able_stand_impair;
+ if (unit->flags3.bits.on_crutch) y--;
+ y = y * 500 / x;
+ if (y > 0) speed += y;
+ }
+
+ // Mood
+
+ if (unit->mood == mood_type::Melancholy) speed += 8000;
+
+ // Inventory encumberance
+
+ int total_weight = calcInventoryWeight(unit);
+ int free_weight = std::max(1, muscle/10 + strength_attr*3);
+
+ if (free_weight < total_weight)
+ {
+ int delta = (total_weight - free_weight)/10 + 1;
+ if (!is_adventure)
+ delta = std::min(5000, delta);
+ speed += delta;
+ }
+
+ // skipped: unknown loop on inventory items that amounts to 0 change
+
+ if (is_adventure)
+ {
+ auto player = vector_get(world->units.active, 0);
+ if (player && player->id == unit->relations.group_leader_id)
+ speed = std::min(speed, computeMovementSpeed(player));
+ }
+
+ return std::min(10000, std::max(0, speed));
+}
+
static bool noble_pos_compare(const Units::NoblePosition &a, const Units::NoblePosition &b)
{
if (a.position->precedence < b.position->precedence)
diff --git a/library/modules/World.cpp b/library/modules/World.cpp
index 393e7cbfe..e14aa02a0 100644
--- a/library/modules/World.cpp
+++ b/library/modules/World.cpp
@@ -285,13 +285,13 @@ PersistentDataItem World::GetPersistentData(int entry_id)
PersistentDataItem World::GetPersistentData(const std::string &key, bool *added)
{
- *added = false;
+ if (added) *added = false;
PersistentDataItem rv = GetPersistentData(key);
if (!rv.isValid())
{
- *added = true;
+ if (added) *added = true;
rv = AddPersistentData(key);
}
diff --git a/library/xml b/library/xml
index abcb667bc..d55f1cf43 160000
--- a/library/xml
+++ b/library/xml
@@ -1 +1 @@
-Subproject commit abcb667bc832048552d8cbc8f4830936f8b63399
+Subproject commit d55f1cf43dd71d3abee724bfa88a0a401b4ccaa3
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index 023cd6e83..6e207385e 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -44,6 +44,9 @@ endif()
install(DIRECTORY lua/
DESTINATION ${DFHACK_LUA_DESTINATION}/plugins
FILES_MATCHING PATTERN "*.lua")
+install(DIRECTORY raw/
+ DESTINATION ${DFHACK_DATA_DESTINATION}/raw
+ FILES_MATCHING PATTERN "*.txt")
# Protobuf
FILE(GLOB PROJECT_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto)
@@ -81,7 +84,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(weather weather.cpp)
DFHACK_PLUGIN(colonies colonies.cpp)
DFHACK_PLUGIN(mode mode.cpp)
- DFHACK_PLUGIN(liquids liquids.cpp Brushes.h)
+ DFHACK_PLUGIN(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua)
DFHACK_PLUGIN(tiletypes tiletypes.cpp Brushes.h)
DFHACK_PLUGIN(tubefill tubefill.cpp)
DFHACK_PLUGIN(autodump autodump.cpp)
@@ -92,7 +95,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(seedwatch seedwatch.cpp)
DFHACK_PLUGIN(initflags initflags.cpp)
DFHACK_PLUGIN(stockpiles stockpiles.cpp)
- DFHACK_PLUGIN(rename rename.cpp PROTOBUFS rename)
+ DFHACK_PLUGIN(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename)
DFHACK_PLUGIN(jobutils jobutils.cpp)
DFHACK_PLUGIN(workflow workflow.cpp)
DFHACK_PLUGIN(showmood showmood.cpp)
@@ -114,6 +117,8 @@ if (BUILD_SUPPORTED)
# this one exports functions to lua
DFHACK_PLUGIN(burrows burrows.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(sort sort.cpp LINK_LIBRARIES lua)
+ DFHACK_PLUGIN(steam-engine steam-engine.cpp)
+ DFHACK_PLUGIN(power-meter power-meter.cpp LINK_LIBRARIES lua)
# not yet. busy with other crud again...
#DFHACK_PLUGIN(versionosd versionosd.cpp)
endif()
diff --git a/plugins/changelayer.cpp b/plugins/changelayer.cpp
index 317a0fa36..3ab1899af 100644
--- a/plugins/changelayer.cpp
+++ b/plugins/changelayer.cpp
@@ -19,6 +19,7 @@
#include "df/world_data.h"
#include "df/world_geo_biome.h"
#include "df/world_geo_layer.h"
+#include "df/region_map_entry.h"
using namespace DFHack;
using namespace df::enums;
diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt
index 8274accfb..39e8f7b60 100644
--- a/plugins/devel/CMakeLists.txt
+++ b/plugins/devel/CMakeLists.txt
@@ -18,3 +18,7 @@ DFHACK_PLUGIN(stripcaged stripcaged.cpp)
DFHACK_PLUGIN(rprobe rprobe.cpp)
DFHACK_PLUGIN(nestboxes nestboxes.cpp)
DFHACK_PLUGIN(vshook vshook.cpp)
+DFHACK_PLUGIN(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
+IF(UNIX)
+DFHACK_PLUGIN(ref-index ref-index.cpp)
+ENDIF()
diff --git a/plugins/devel/ref-index.cpp b/plugins/devel/ref-index.cpp
new file mode 100644
index 000000000..686f6918b
--- /dev/null
+++ b/plugins/devel/ref-index.cpp
@@ -0,0 +1,149 @@
+#include "Core.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include "df/item.h"
+#include "df/unit.h"
+#include "df/world.h"
+#include "df/general_ref_item.h"
+#include "df/general_ref_unit.h"
+
+using std::vector;
+using std::string;
+using std::stack;
+using namespace DFHack;
+
+using df::global::gps;
+
+DFHACK_PLUGIN("ref-index");
+
+#define global_id id
+
+template
+T get_from_global_id_vector(int32_t id, const std::vector &vect, int32_t *cache)
+{
+ size_t size = vect.size();
+ int32_t start=0;
+ int32_t end=(int32_t)size-1;
+
+ // Check the cached location. If it is a match, this provides O(1) lookup.
+ // Otherwise it works like one binsearch iteration.
+ if (size_t(*cache) < size)
+ {
+ T cptr = vect[*cache];
+ if (cptr->global_id == id)
+ return cptr;
+ if (cptr->global_id < id)
+ start = *cache+1;
+ else
+ end = *cache-1;
+ }
+
+ // Regular binsearch. The end check provides O(1) caching for missing item.
+ if (start <= end && vect[end]->global_id >= id)
+ {
+ do {
+ int32_t mid=(start+end)>>1;
+
+ T cptr=vect[mid];
+ if(cptr->global_id==id)
+ {
+ *cache = mid;
+ return cptr;
+ }
+ else if(cptr->global_id>id)end=mid-1;
+ else start=mid+1;
+ } while(start<=end);
+ }
+
+ *cache = end+1;
+ return NULL;
+}
+
+template T *find_object(int32_t id, int32_t *cache);
+template<> df::item *find_object(int32_t id, int32_t *cache) {
+ return get_from_global_id_vector(id, df::global::world->items.all, cache);
+}
+template<> df::unit *find_object(int32_t id, int32_t *cache) {
+ return get_from_global_id_vector(id, df::global::world->units.all, cache);
+}
+
+template
+struct CachedRef {
+ int32_t id;
+ int32_t cache;
+ CachedRef(int32_t id = -1) : id(id), cache(-1) {}
+ T *target() { return find_object(id, &cache); }
+};
+
+#ifdef LINUX_BUILD
+struct item_hook : df::general_ref_item {
+ typedef df::general_ref_item interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(df::item*, getItem, ())
+ {
+ // HUGE HACK: ASSUMES THERE ARE 4 USABLE BYTES AFTER THE OBJECT
+ // This actually is true with glibc allocator due to granularity.
+ return find_object(item_id, 1+&item_id);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(item_hook, getItem);
+
+struct unit_hook : df::general_ref_unit {
+ typedef df::general_ref_unit interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(df::unit*, getUnit, ())
+ {
+ // HUGE HACK: ASSUMES THERE ARE 4 USABLE BYTES AFTER THE OBJECT
+ // This actually is true with glibc allocator due to granularity.
+ return find_object(unit_id, 1+&unit_id);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(unit_hook, getUnit);
+
+command_result hook_refs(color_ostream &out, vector & parameters)
+{
+ auto &hook = INTERPOSE_HOOK(item_hook, getItem);
+ if (hook.is_applied())
+ {
+ hook.remove();
+ INTERPOSE_HOOK(unit_hook, getUnit).remove();
+ }
+ else
+ {
+ hook.apply();
+ INTERPOSE_HOOK(unit_hook, getUnit).apply();
+ }
+
+ if (hook.is_applied())
+ out.print("Hook is applied.\n");
+ else
+ out.print("Hook is not applied.\n");
+ return CR_OK;
+}
+#endif
+
+DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands)
+{
+#ifdef LINUX_BUILD
+ commands.push_back(PluginCommand("hook-refs","Inject O(1) cached lookup into general refs.",hook_refs));
+#endif
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown ( color_ostream &out )
+{
+ return CR_OK;
+}
diff --git a/plugins/devel/rprobe.cpp b/plugins/devel/rprobe.cpp
index 7a091a962..805489d5e 100644
--- a/plugins/devel/rprobe.cpp
+++ b/plugins/devel/rprobe.cpp
@@ -27,6 +27,7 @@ using namespace std;
#include "df/world_region_details.h"
#include "df/world_geo_biome.h"
#include "df/world_geo_layer.h"
+#include "df/region_map_entry.h"
#include "df/inclusion_type.h"
#include "df/viewscreen_choose_start_sitest.h"
@@ -79,7 +80,7 @@ command_result rprobe (color_ostream &out, vector & parameters)
if (parameters.size() == 2)
{
- if (parameters[0] == "wet")
+ if (parameters[0] == "rai")
set_field = 0;
else if (parameters[0] == "veg")
set_field = 1;
@@ -87,7 +88,7 @@ command_result rprobe (color_ostream &out, vector & parameters)
set_field = 2;
else if (parameters[0] == "evi")
set_field = 3;
- else if (parameters[0] == "hil")
+ else if (parameters[0] == "dra")
set_field = 4;
else if (parameters[0] == "sav")
set_field = 5;
@@ -113,11 +114,11 @@ command_result rprobe (color_ostream &out, vector & parameters)
{
coord2d rg = screen->biome_rgn[i];
- df::world_data::T_region_map* rd = &data->region_map[rg.x][rg.y];
+ auto rd = &data->region_map[rg.x][rg.y];
if (set && i == to_set) {
if (set_field == 0)
- rd->wetness = set_val;
+ rd->rainfall = set_val;
else if (set_field == 1)
rd->vegetation = set_val;
else if (set_field == 2)
@@ -125,11 +126,11 @@ command_result rprobe (color_ostream &out, vector & parameters)
else if (set_field == 3)
rd->evilness = set_val;
else if (set_field == 4)
- rd->hilliness = set_val;
+ rd->drainage = set_val;
else if (set_field == 5)
rd->savagery = set_val;
else if (set_field == 6)
- rd->saltiness = set_val;
+ rd->salinity = set_val;
}
out << i << ": x = " << rg.x << ", y = " << rg.y;
@@ -140,13 +141,13 @@ command_result rprobe (color_ostream &out, vector & parameters)
" landmass_id: " << rd->landmass_id <<
" flags: " << hex << rd->flags.as_int() << dec << endl;
out <<
- "wet: " << rd->wetness << " " <<
+ "rai: " << rd->rainfall << " " <<
"veg: " << rd->vegetation << " " <<
"tem: " << rd->temperature << " " <<
"evi: " << rd->evilness << " " <<
- "hil: " << rd->hilliness << " " <<
+ "dra: " << rd->drainage << " " <<
"sav: " << rd->savagery << " " <<
- "sal: " << rd->saltiness;
+ "sal: " << rd->salinity;
int32_t *p = (int32_t *)rd;
int c = sizeof(*rd) / sizeof(int32_t);
diff --git a/plugins/devel/siege-engine.cpp b/plugins/devel/siege-engine.cpp
new file mode 100644
index 000000000..c36e9fb32
--- /dev/null
+++ b/plugins/devel/siege-engine.cpp
@@ -0,0 +1,1005 @@
+#include "Core.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include "df/graphic.h"
+#include "df/building_siegeenginest.h"
+#include "df/builtin_mats.h"
+#include "df/world.h"
+#include "df/buildings_other_id.h"
+#include "df/job.h"
+#include "df/building_drawbuffer.h"
+#include "df/ui.h"
+#include "df/viewscreen_dwarfmodest.h"
+#include "df/ui_build_selector.h"
+#include "df/flow_info.h"
+#include "df/report.h"
+#include "df/proj_itemst.h"
+#include "df/unit.h"
+#include "df/unit_soul.h"
+#include "df/unit_skill.h"
+#include "df/physical_attribute_type.h"
+#include "df/creature_raw.h"
+#include "df/caste_raw.h"
+#include "df/caste_raw_flags.h"
+#include "df/assumed_identity.h"
+#include "df/game_mode.h"
+#include "df/unit_misc_trait.h"
+
+#include "MiscUtils.h"
+
+using std::vector;
+using std::string;
+using std::stack;
+using namespace DFHack;
+using namespace df::enums;
+
+using df::global::gamemode;
+using df::global::gps;
+using df::global::world;
+using df::global::ui;
+using df::global::ui_build_selector;
+
+using Screen::Pen;
+
+DFHACK_PLUGIN("siege-engine");
+
+/*
+ * Misc. utils
+ */
+
+typedef std::pair coord_range;
+
+static void set_range(coord_range *target, df::coord p1, df::coord p2)
+{
+ if (!p1.isValid() || !p2.isValid())
+ {
+ *target = coord_range();
+ }
+ else
+ {
+ target->first.x = std::min(p1.x, p2.x);
+ target->first.y = std::min(p1.y, p2.y);
+ target->first.z = std::min(p1.z, p2.z);
+ target->second.x = std::max(p1.x, p2.x);
+ target->second.y = std::max(p1.y, p2.y);
+ target->second.z = std::max(p1.z, p2.z);
+ }
+}
+
+static bool is_range_valid(const coord_range &target)
+{
+ return target.first.isValid() && target.second.isValid();
+}
+
+static bool is_in_range(const coord_range &target, df::coord pos)
+{
+ return target.first.isValid() && target.second.isValid() &&
+ target.first.x <= pos.x && pos.x <= target.second.x &&
+ target.first.y <= pos.y && pos.y <= target.second.y &&
+ target.first.z <= pos.z && pos.z <= target.second.z;
+}
+
+static std::pair get_engine_range(df::building_siegeenginest *bld)
+{
+ if (bld->type == siegeengine_type::Ballista)
+ return std::make_pair(0, 200);
+ else
+ return std::make_pair(30, 100);
+}
+
+static void orient_engine(df::building_siegeenginest *bld, df::coord target)
+{
+ int dx = target.x - bld->centerx;
+ int dy = target.y - bld->centery;
+
+ if (abs(dx) > abs(dy))
+ bld->facing = (dx > 0) ?
+ df::building_siegeenginest::Right :
+ df::building_siegeenginest::Left;
+ else
+ bld->facing = (dy > 0) ?
+ df::building_siegeenginest::Down :
+ df::building_siegeenginest::Up;
+}
+
+static int random_int(int val)
+{
+ return int(int64_t(rand())*val/RAND_MAX);
+}
+
+static int point_distance(df::coord speed)
+{
+ return std::max(abs(speed.x), std::max(abs(speed.y), abs(speed.z)));
+}
+
+/*
+ * Configuration management
+ */
+
+static bool enable_plugin();
+
+struct EngineInfo {
+ int id;
+ coord_range target;
+ df::coord center;
+ int proj_speed, hit_delay;
+
+ bool hasTarget() { return is_range_valid(target); }
+ bool onTarget(df::coord pos) { return is_in_range(target, pos); }
+ df::coord getTargetSize() { return target.second - target.first; }
+};
+
+static std::map engines;
+static std::map coord_engines;
+
+static EngineInfo *find_engine(df::building *bld, bool create = false)
+{
+ if (!bld)
+ return NULL;
+
+ auto it = engines.find(bld);
+ if (it != engines.end())
+ return &it->second;
+ if (!create)
+ return NULL;
+
+ auto *obj = &engines[bld];
+
+ obj->id = bld->id;
+ obj->center = df::coord(bld->centerx, bld->centery, bld->z);
+ obj->proj_speed = 2;
+ obj->hit_delay = 3;
+
+ coord_engines[obj->center] = bld;
+ return obj;
+}
+
+static EngineInfo *find_engine(df::coord pos)
+{
+ return find_engine(coord_engines[pos]);
+}
+
+static void clear_engines()
+{
+ engines.clear();
+ coord_engines.clear();
+}
+
+static void load_engines()
+{
+ clear_engines();
+
+ auto pworld = Core::getInstance().getWorld();
+ std::vector vec;
+
+ pworld->GetPersistentData(&vec, "siege-engine/target/", true);
+ for (auto it = vec.begin(); it != vec.end(); ++it)
+ {
+ auto engine = find_engine(df::building::find(it->ival(0)), true);
+ if (!engine) continue;
+ engine->target.first = df::coord(it->ival(1), it->ival(2), it->ival(3));
+ engine->target.second = df::coord(it->ival(4), it->ival(5), it->ival(6));
+ }
+}
+
+static int getTargetArea(lua_State *L)
+{
+ auto bld = Lua::CheckDFObject(L, 1);
+ if (!bld) luaL_argerror(L, 1, "null building");
+ auto engine = find_engine(bld);
+
+ if (engine && engine->hasTarget())
+ {
+ Lua::Push(L, engine->target.first);
+ Lua::Push(L, engine->target.second);
+ }
+ else
+ {
+ lua_pushnil(L);
+ lua_pushnil(L);
+ }
+
+ return 2;
+}
+
+static void clearTargetArea(df::building_siegeenginest *bld)
+{
+ CHECK_NULL_POINTER(bld);
+
+ if (auto engine = find_engine(bld))
+ engine->target = coord_range();
+
+ auto pworld = Core::getInstance().getWorld();
+ auto key = stl_sprintf("siege-engine/target/%d", bld->id);
+ pworld->DeletePersistentData(pworld->GetPersistentData(key));
+}
+
+static bool setTargetArea(df::building_siegeenginest *bld, df::coord target_min, df::coord target_max)
+{
+ CHECK_NULL_POINTER(bld);
+ CHECK_INVALID_ARGUMENT(target_min.isValid() && target_max.isValid());
+
+ if (!enable_plugin())
+ return false;
+
+ auto pworld = Core::getInstance().getWorld();
+ auto key = stl_sprintf("siege-engine/target/%d", bld->id);
+ auto entry = pworld->GetPersistentData(key, NULL);
+ if (!entry.isValid())
+ return false;
+
+ auto engine = find_engine(bld, true);
+
+ set_range(&engine->target, target_min, target_max);
+
+ entry.ival(0) = bld->id;
+ entry.ival(1) = engine->target.first.x;
+ entry.ival(2) = engine->target.first.y;
+ entry.ival(3) = engine->target.first.z;
+ entry.ival(4) = engine->target.second.x;
+ entry.ival(5) = engine->target.second.y;
+ entry.ival(6) = engine->target.second.z;
+
+ df::coord sum = target_min + target_max;
+ orient_engine(bld, df::coord(sum.x/2, sum.y/2, sum.z/2));
+
+ return true;
+}
+
+/*
+ * Trajectory
+ */
+
+struct ProjectilePath {
+ df::coord origin, goal, target, fudge_delta;
+ int divisor, fudge_factor;
+ df::coord speed, direction;
+
+ ProjectilePath(df::coord origin, df::coord goal) :
+ origin(origin), goal(goal), target(goal), fudge_factor(1)
+ {
+ fudge_delta = df::coord(0,0,0);
+ calc_line();
+ }
+
+ void fudge(int factor, df::coord delta)
+ {
+ fudge_factor = factor;
+ fudge_delta = delta;
+ auto diff = goal - origin;
+ diff.x *= fudge_factor;
+ diff.y *= fudge_factor;
+ diff.z *= fudge_factor;
+ target = origin + diff + fudge_delta;
+ calc_line();
+ }
+
+ void calc_line()
+ {
+ speed = target - origin;
+ divisor = point_distance(speed);
+ if (divisor <= 0) divisor = 1;
+ direction = df::coord(speed.x>=0?1:-1,speed.y>=0?1:-1,speed.z>=0?1:-1);
+ }
+
+ df::coord operator[] (int i) const
+ {
+ int div2 = divisor * 2;
+ int bias = divisor-1;
+ return origin + df::coord(
+ (2*speed.x*i + direction.x*bias)/div2,
+ (2*speed.y*i + direction.y*bias)/div2,
+ (2*speed.z*i + direction.z*bias)/div2
+ );
+ }
+};
+
+static bool isPassableTile(df::coord pos)
+{
+ auto ptile = Maps::getTileType(pos);
+ return !ptile || FlowPassable(*ptile);
+}
+
+struct PathMetrics {
+ enum CollisionType {
+ Impassable,
+ Floor,
+ Ceiling,
+ MapEdge
+ } hit_type;
+
+ int collision_step;
+ int goal_step, goal_z_step;
+ std::vector coords;
+
+ bool hits() { return collision_step > goal_step; }
+
+ PathMetrics(const ProjectilePath &path, bool list_coords = false)
+ {
+ coords.clear();
+ collision_step = goal_step = goal_z_step = 1000000;
+
+ int step = 0;
+ df::coord prev_pos = path.origin;
+ if (list_coords)
+ coords.push_back(prev_pos);
+
+ for (;;) {
+ df::coord cur_pos = path[++step];
+ if (cur_pos == prev_pos)
+ break;
+ if (list_coords)
+ coords.push_back(cur_pos);
+
+ if (cur_pos.z == path.goal.z)
+ {
+ goal_z_step = std::min(step, goal_z_step);
+ if (cur_pos == path.goal)
+ goal_step = step;
+ }
+
+ if (!Maps::isValidTilePos(cur_pos))
+ {
+ hit_type = PathMetrics::MapEdge;
+ break;
+ }
+
+ if (!isPassableTile(cur_pos))
+ {
+ hit_type = Impassable;
+ break;
+ }
+
+ if (cur_pos.z != prev_pos.z)
+ {
+ int top_z = std::max(prev_pos.z, cur_pos.z);
+ auto ptile = Maps::getTileType(cur_pos.x, cur_pos.y, top_z);
+
+ if (ptile && !LowPassable(*ptile))
+ {
+ hit_type = (cur_pos.z > prev_pos.z ? Ceiling : Floor);
+ break;
+ }
+ }
+
+ prev_pos = cur_pos;
+ }
+
+ collision_step = step;
+ }
+};
+
+struct AimContext {
+ df::building_siegeenginest *bld;
+ df::coord origin;
+ coord_range building_rect;
+ EngineInfo *engine;
+ std::pair fire_range;
+
+ AimContext(df::building_siegeenginest *bld, EngineInfo *engine)
+ : bld(bld), engine(engine)
+ {
+ origin = df::coord(bld->centerx, bld->centery, bld->z);
+ building_rect = coord_range(
+ df::coord(bld->x1, bld->y1, bld->z),
+ df::coord(bld->x2, bld->y2, bld->z)
+ );
+ fire_range = get_engine_range(bld);
+ }
+
+ bool isInRange(const PathMetrics &raytrace)
+ {
+ return raytrace.goal_step >= fire_range.first &&
+ raytrace.goal_step <= fire_range.second;
+ }
+
+ bool adjustToPassable(df::coord *pos)
+ {
+ if (isPassableTile(*pos))
+ return true;
+
+ for (df::coord fudge = *pos;
+ fudge.z < engine->target.second.z; fudge.z++)
+ {
+ if (!isPassableTile(fudge))
+ continue;
+ *pos = fudge;
+ return true;
+ }
+
+ for (df::coord fudge = *pos;
+ fudge.z > engine->target.first.z; fudge.z--)
+ {
+ if (!isPassableTile(fudge))
+ continue;
+ *pos = fudge;
+ return true;
+ }
+
+ return false;
+ }
+
+};
+
+static std::string getTileStatus(df::building_siegeenginest *bld, df::coord tile_pos)
+{
+ AimContext context(bld, NULL);
+
+ ProjectilePath path(context.origin, tile_pos);
+ PathMetrics raytrace(path);
+
+ if (raytrace.hits())
+ {
+ if (context.isInRange(raytrace))
+ return "ok";
+ else
+ return "out_of_range";
+ }
+ else
+ return "blocked";
+}
+
+static void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::coord2d ltop, df::coord2d size)
+{
+ CHECK_NULL_POINTER(bld);
+
+ AimContext context(bld, find_engine(bld));
+
+ for (int x = 0; x < size.x; x++)
+ {
+ for (int y = 0; y < size.y; y++)
+ {
+ df::coord tile_pos = view + df::coord(x,y,0);
+ if (is_in_range(context.building_rect, tile_pos))
+ continue;
+
+ Pen cur_tile = Screen::readTile(ltop.x+x, ltop.y+y);
+ if (!cur_tile.valid())
+ continue;
+
+ ProjectilePath path(context.origin, tile_pos);
+ PathMetrics raytrace(path);
+
+ int color;
+ if (raytrace.hits())
+ {
+ if (context.isInRange(raytrace))
+ color = COLOR_GREEN;
+ else
+ color = COLOR_CYAN;
+ }
+ else
+ color = COLOR_RED;
+
+ if (cur_tile.fg && cur_tile.ch != ' ')
+ {
+ cur_tile.fg = color;
+ cur_tile.bg = 0;
+ }
+ else
+ {
+ cur_tile.fg = 0;
+ cur_tile.bg = color;
+ }
+
+ cur_tile.bold = (context.engine && context.engine->onTarget(tile_pos));
+
+ if (cur_tile.tile)
+ cur_tile.tile_mode = Pen::CharColor;
+
+ Screen::paintTile(cur_tile, ltop.x+x, ltop.y+y);
+ }
+ }
+}
+
+/*
+ * Unit tracking
+ */
+
+static const float MAX_TIME = 1000000.0f;
+
+struct UnitPath {
+ df::unit *unit;
+ std::map path;
+
+ struct Hit {
+ UnitPath *path;
+ df::coord pos;
+ int dist;
+ float time, lmargin, rmargin;
+ };
+
+ static std::map cache;
+
+ static UnitPath *get(df::unit *unit)
+ {
+ auto &cv = cache[unit];
+ if (!cv) cv = new UnitPath(unit);
+ return cv;
+ };
+
+ UnitPath(df::unit *unit) : unit(unit)
+ {
+ df::coord pos = unit->pos;
+ df::coord dest = unit->path.dest;
+ auto &upath = unit->path.path;
+
+ if (dest.isValid() && !upath.x.empty())
+ {
+ float time = unit->counters.job_counter+0.5f;
+ float speed = Units::computeMovementSpeed(unit)/100.0f;
+
+ for (size_t i = 0; i < upath.size(); i++)
+ {
+ df::coord new_pos = upath[i];
+ if (new_pos == pos)
+ continue;
+
+ float delay = speed;
+ if (new_pos.x != pos.x && new_pos.y != pos.y)
+ delay *= 362.0/256.0;
+
+ path[time] = pos;
+ pos = new_pos;
+ time += delay + 1;
+ }
+ }
+
+ path[MAX_TIME] = pos;
+ }
+
+ void get_margin(std::map::iterator &it, float time, float *lmargin, float *rmargin)
+ {
+ auto it2 = it;
+ *lmargin = (it == path.begin()) ? MAX_TIME : time - (--it2)->first;
+ *rmargin = (it->first == MAX_TIME) ? MAX_TIME : it->first - time;
+ }
+
+ df::coord posAtTime(float time, float *lmargin = NULL, float *rmargin = NULL)
+ {
+ CHECK_INVALID_ARGUMENT(time < MAX_TIME);
+
+ auto it = path.upper_bound(time);
+ if (lmargin)
+ get_margin(it, time, lmargin, rmargin);
+ return it->second;
+ }
+
+ bool findHits(EngineInfo *engine, std::vector *hit_points, float bias)
+ {
+ df::coord origin = engine->center;
+
+ Hit info;
+ info.path = this;
+
+ for (auto it = path.begin(); it != path.end(); ++it)
+ {
+ info.pos = it->second;
+ info.dist = point_distance(origin - info.pos);
+ info.time = float(info.dist)*(engine->proj_speed+1) + engine->hit_delay + bias;
+ get_margin(it, info.time, &info.lmargin, &info.rmargin);
+
+ if (info.lmargin > 0 && info.rmargin > 0)
+ {
+ if (engine->onTarget(info.pos))
+ hit_points->push_back(info);
+ }
+ }
+
+ return !hit_points->empty();
+ }
+};
+
+std::map UnitPath::cache;
+
+static void push_margin(lua_State *L, float margin)
+{
+ if (margin == MAX_TIME)
+ lua_pushnil(L);
+ else
+ lua_pushnumber(L, margin);
+}
+
+static int traceUnitPath(lua_State *L)
+{
+ auto unit = Lua::CheckDFObject(L, 1);
+
+ CHECK_NULL_POINTER(unit);
+
+ size_t idx = 1;
+ auto info = UnitPath::get(unit);
+ lua_createtable(L, info->path.size(), 0);
+
+ float last_time = 0.0f;
+ for (auto it = info->path.begin(); it != info->path.end(); ++it)
+ {
+ Lua::Push(L, it->second);
+ if (idx > 1)
+ {
+ lua_pushnumber(L, last_time);
+ lua_setfield(L, -2, "from");
+ }
+ if (idx < info->path.size())
+ {
+ lua_pushnumber(L, it->first);
+ lua_setfield(L, -2, "to");
+ }
+ lua_rawseti(L, -2, idx++);
+ last_time = it->first;
+ }
+
+ return 1;
+}
+
+static int unitPosAtTime(lua_State *L)
+{
+ auto unit = Lua::CheckDFObject(L, 1);
+ float time = luaL_checknumber(L, 2);
+
+ CHECK_NULL_POINTER(unit);
+
+ float lmargin, rmargin;
+ auto info = UnitPath::get(unit);
+
+ Lua::Push(L, info->posAtTime(time, &lmargin, &rmargin));
+ push_margin(L, lmargin);
+ push_margin(L, rmargin);
+ return 3;
+}
+
+static void proposeUnitHits(EngineInfo *engine, std::vector *hits, float bias)
+{
+ auto &active = world->units.active;
+
+ for (size_t i = 0; i < active.size(); i++)
+ {
+ auto unit = active[i];
+
+ if (unit->flags1.bits.dead ||
+ unit->flags3.bits.ghostly ||
+ unit->flags1.bits.caged ||
+ unit->flags1.bits.chained ||
+ unit->flags1.bits.rider ||
+ unit->flags1.bits.hidden_in_ambush)
+ continue;
+
+ UnitPath::get(unit)->findHits(engine, hits, bias);
+ }
+}
+
+static int proposeUnitHits(lua_State *L)
+{
+ auto bld = Lua::CheckDFObject(L, 1);
+ float bias = luaL_optnumber(L, 2, 0);
+
+ auto engine = find_engine(bld);
+ if (!engine)
+ luaL_error(L, "no such engine");
+ if (!engine->hasTarget())
+ luaL_error(L, "target not set");
+
+ std::vector hits;
+ proposeUnitHits(engine, &hits, bias);
+
+ lua_createtable(L, hits.size(), 0);
+
+ for (size_t i = 0; i < hits.size(); i++)
+ {
+ auto &hit = hits[i];
+ lua_createtable(L, 0, 6);
+ Lua::PushDFObject(L, hit.path->unit); lua_setfield(L, -2, "unit");
+ Lua::Push(L, hit.pos); lua_setfield(L, -2, "pos");
+ lua_pushnumber(L, hit.dist); lua_setfield(L, -2, "dist");
+ lua_pushnumber(L, hit.time); lua_setfield(L, -2, "time");
+ push_margin(L, hit.lmargin); lua_setfield(L, -2, "lmargin");
+ push_margin(L, hit.rmargin); lua_setfield(L, -2, "rmargin");
+ lua_rawseti(L, -2, i+1);
+ }
+
+ return 1;
+}
+
+#if 0
+struct UnitContext {
+ AimContext &ctx;
+
+ struct UnitInfo {
+ df::unit *unit;
+
+ UnitPath path;
+ float score;
+
+ UnitInfo(df::unit *unit) : unit(unit), path(unit) {}
+ };
+
+ std::map units;
+
+ UnitContext(AimContext &ctx) : ctx(ctx) {}
+
+ ~UnitContext()
+ {
+ for (auto it = units.begin(); it != units.end(); ++it)
+ delete it->second;
+ }
+
+ float unit_score(df::unit *unit)
+ {
+ float score = 1.0f;
+
+ if (unit->flags1.bits.tame && unit->civ_id == ui->civ_id)
+ score = -1.0f;
+ if (unit->flags1.bits.diplomat || unit->flags1.bits.merchant)
+ score = -2.0f;
+ else if (Units::isCitizen(unit))
+ score = -10.0f;
+ else
+ {
+ if (unit->flags1.bits.marauder)
+ score += 0.5f;
+ if (unit->flags1.bits.active_invader)
+ score += 1.0f;
+ if (unit->flags1.bits.invader_origin)
+ score += 1.0f;
+ if (unit->flags1.bits.invades)
+ score += 1.0f;
+ if (unit->flags1.bits.hidden_ambusher)
+ score += 1.0f;
+ }
+
+ if (unit->flags1.bits.ridden)
+ {
+ for (size_t i = 0; i < unit->refs.size(); i++)
+ {
+ if (!unit->refs[i]->getType() == general_ref_type::UNIT_RIDER)
+ continue;
+ if (auto rider = unit->refs[i]->getUnit())
+ score += unit_score(rider);
+ }
+ }
+ }
+
+ void select_units()
+ {
+ auto &active = world->units.active;
+
+ for (size_t i = 0; i < active.size(); i++)
+ {
+ auto unit = active[i];
+ if (unit->flags1.bits.dead ||
+ unit->flags3.bits.ghostly ||
+ unit->flags1.bits.caged ||
+ unit->flags1.bits.chained ||
+ unit->flags1.bits.rider ||
+ unit->flags1.bits.hidden_in_ambush)
+ continue;
+
+ auto info = units[unit] = new UnitInfo(unit);
+
+ info->findHits(ctx, ctx.proj_hit_delay);
+ info->score = unit_score(unit);
+ }
+ }
+};
+#endif
+
+/*
+ * Projectile hook
+ */
+
+struct projectile_hook : df::proj_itemst {
+ typedef df::proj_itemst interpose_base;
+
+ void aimAtPoint(AimContext &context, ProjectilePath &path, bool bad_shot = false)
+ {
+ target_pos = path.target;
+
+ PathMetrics raytrace(path);
+
+ // Materialize map blocks, or the projectile will crash into them
+ for (int i = 0; i < raytrace.collision_step; i++)
+ Maps::ensureTileBlock(path[i]);
+
+ if (flags.bits.piercing)
+ {
+ if (bad_shot)
+ fall_threshold = std::min(raytrace.goal_z_step, raytrace.collision_step);
+ }
+ else
+ {
+ if (bad_shot)
+ fall_threshold = context.fire_range.second;
+ else
+ fall_threshold = raytrace.goal_step;
+ }
+
+ fall_threshold = std::max(fall_threshold, context.fire_range.first);
+ fall_threshold = std::min(fall_threshold, context.fire_range.second);
+ }
+
+ void aimAtArea(AimContext &context)
+ {
+ df::coord target, last_passable;
+ df::coord tbase = context.engine->target.first;
+ df::coord tsize = context.engine->getTargetSize();
+ bool success = false;
+
+ for (int i = 0; i < 50; i++)
+ {
+ target = tbase + df::coord(
+ random_int(tsize.x), random_int(tsize.y), random_int(tsize.z)
+ );
+
+ if (context.adjustToPassable(&target))
+ last_passable = target;
+ else
+ continue;
+
+ ProjectilePath path(context.origin, target);
+ PathMetrics raytrace(path);
+
+ if (raytrace.hits() && context.isInRange(raytrace))
+ {
+ aimAtPoint(context, path);
+ return;
+ }
+ }
+
+ if (!last_passable.isValid())
+ last_passable = target;
+
+ ProjectilePath path(context.origin, last_passable);
+ aimAtPoint(context, path, true);
+ }
+
+ void doCheckMovement()
+ {
+ if (distance_flown != 0 || fall_counter != fall_delay)
+ return;
+
+ auto engine = find_engine(origin_pos);
+ if (!engine || !engine->hasTarget())
+ return;
+
+ auto bld0 = df::building::find(engine->id);
+ auto bld = strict_virtual_cast(bld0);
+ if (!bld)
+ return;
+
+ AimContext context(bld, engine);
+
+ aimAtArea(context);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, checkMovement, ())
+ {
+ if (flags.bits.high_flying || flags.bits.piercing)
+ doCheckMovement();
+
+ return INTERPOSE_NEXT(checkMovement)();
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(projectile_hook, checkMovement);
+
+/*
+ * Initialization
+ */
+
+DFHACK_PLUGIN_LUA_FUNCTIONS {
+ DFHACK_LUA_FUNCTION(clearTargetArea),
+ DFHACK_LUA_FUNCTION(setTargetArea),
+ DFHACK_LUA_FUNCTION(getTileStatus),
+ DFHACK_LUA_FUNCTION(paintAimScreen),
+ DFHACK_LUA_END
+};
+
+DFHACK_PLUGIN_LUA_COMMANDS {
+ DFHACK_LUA_COMMAND(getTargetArea),
+ DFHACK_LUA_COMMAND(traceUnitPath),
+ DFHACK_LUA_COMMAND(unitPosAtTime),
+ DFHACK_LUA_COMMAND(proposeUnitHits),
+ DFHACK_LUA_END
+};
+
+static bool is_enabled = false;
+
+static void enable_hooks(bool enable)
+{
+ is_enabled = enable;
+
+ INTERPOSE_HOOK(projectile_hook, checkMovement).apply(enable);
+
+ if (enable)
+ load_engines();
+ else
+ clear_engines();
+}
+
+static bool enable_plugin()
+{
+ if (is_enabled)
+ return true;
+
+ auto pworld = Core::getInstance().getWorld();
+ auto entry = pworld->GetPersistentData("siege-engine/enabled", NULL);
+ if (!entry.isValid())
+ return false;
+
+ enable_hooks(true);
+ return true;
+}
+
+static void clear_caches()
+{
+ if (!UnitPath::cache.empty())
+ {
+ for (auto it = UnitPath::cache.begin(); it != UnitPath::cache.end(); ++it)
+ delete it->second;
+
+ UnitPath::cache.clear();
+ }
+}
+
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
+{
+ switch (event) {
+ case SC_MAP_LOADED:
+ {
+ auto pworld = Core::getInstance().getWorld();
+ bool enable = pworld->GetPersistentData("siege-engine/enabled").isValid();
+
+ if (enable)
+ {
+ out.print("Enabling the siege engine plugin.\n");
+ enable_hooks(true);
+ }
+ else
+ enable_hooks(false);
+ }
+ break;
+ case SC_MAP_UNLOADED:
+ enable_hooks(false);
+ break;
+ default:
+ break;
+ }
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands)
+{
+ if (Core::getInstance().isMapLoaded())
+ plugin_onstatechange(out, SC_MAP_LOADED);
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown ( color_ostream &out )
+{
+ enable_hooks(false);
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_onupdate ( color_ostream &out )
+{
+ clear_caches();
+ return CR_OK;
+}
diff --git a/plugins/liquids.cpp b/plugins/liquids.cpp
index b036e4fa8..6df530a92 100644
--- a/plugins/liquids.cpp
+++ b/plugins/liquids.cpp
@@ -27,6 +27,7 @@
#include
#include
#include
+#include
using std::vector;
using std::string;
using std::endl;
@@ -41,6 +42,7 @@ using std::set;
#include "modules/Gui.h"
#include "TileTypes.h"
#include "modules/MapCache.h"
+#include "LuaTools.h"
#include "Brushes.h"
using namespace MapExtras;
using namespace DFHack;
@@ -50,7 +52,6 @@ CommandHistory liquids_hist;
command_result df_liquids (color_ostream &out, vector & parameters);
command_result df_liquids_here (color_ostream &out, vector & parameters);
-command_result df_liquids_execute (color_ostream &out);
DFHACK_PLUGIN("liquids");
@@ -74,13 +75,79 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
return CR_OK;
}
-// static stuff to be remembered between sessions
-static string brushname = "point";
-static string mode="magma";
-static string flowmode="f+";
-static string _setmode ="s.";
-static unsigned int amount = 7;
-static int width = 1, height = 1, z_levels = 1;
+enum BrushType {
+ B_POINT, B_RANGE, B_BLOCK, B_COLUMN, B_FLOOD
+};
+
+static const char *brush_name[] = {
+ "point", "range", "block", "column", "flood", NULL
+};
+
+enum PaintMode {
+ P_WATER, P_MAGMA, P_OBSIDIAN, P_OBSIDIAN_FLOOR,
+ P_RIVER_SOURCE, P_FLOW_BITS, P_WCLEAN
+};
+
+static const char *paint_mode_name[] = {
+ "water", "magma", "obsidian", "obsidian_floor",
+ "riversource", "flowbits", "wclean", NULL
+};
+
+enum ModifyMode {
+ M_INC, M_KEEP, M_DEC
+};
+
+static const char *modify_mode_name[] = {
+ "+", ".", "-", NULL
+};
+
+enum PermaflowMode {
+ PF_KEEP, PF_NONE,
+ PF_NORTH, PF_SOUTH, PF_EAST, PF_WEST,
+ PF_NORTHEAST, PF_NORTHWEST, PF_SOUTHEAST, PF_SOUTHWEST
+};
+
+static const char *permaflow_name[] = {
+ ".", "-", "N", "S", "E", "W",
+ "NE", "NW", "SE", "SW", NULL
+};
+
+#define X(name) tile_liquid_flow_dir::name
+static const df::tile_liquid_flow_dir permaflow_id[] = {
+ X(none), X(none), X(north), X(south), X(east), X(west),
+ X(northeast), X(northwest), X(southeast), X(southwest)
+};
+#undef X
+
+struct OperationMode {
+ BrushType brush;
+ PaintMode paint;
+ ModifyMode flowmode;
+ ModifyMode setmode;
+ PermaflowMode permaflow;
+ unsigned int amount;
+ df::coord size;
+
+ OperationMode() :
+ brush(B_POINT), paint(P_MAGMA),
+ flowmode(M_INC), setmode(M_KEEP), permaflow(PF_KEEP), amount(7),
+ size(1,1,1)
+ {}
+} cur_mode;
+
+command_result df_liquids_execute(color_ostream &out);
+command_result df_liquids_execute(color_ostream &out, OperationMode &mode, df::coord pos);
+
+static void print_prompt(std::ostream &str, OperationMode &cur_mode)
+{
+ str <<"[" << paint_mode_name[cur_mode.paint] << ":" << brush_name[cur_mode.brush];
+ if (cur_mode.brush == B_RANGE)
+ str << "(w" << cur_mode.size.x << ":h" << cur_mode.size.y << ":z" << cur_mode.size.z << ")";
+ str << ":" << cur_mode.amount << ":f" << modify_mode_name[cur_mode.flowmode]
+ << ":s" << modify_mode_name[cur_mode.setmode]
+ << ":pf" << permaflow_name[cur_mode.permaflow]
+ << "]";
+}
command_result df_liquids (color_ostream &out_, vector & parameters)
{
@@ -117,10 +184,8 @@ command_result df_liquids (color_ostream &out_, vector & parameters)
string input = "";
std::stringstream str;
- str <<"[" << mode << ":" << brushname;
- if (brushname == "range")
- str << "(w" << width << ":h" << height << ":z" << z_levels << ")";
- str << ":" << amount << ":" << flowmode << ":" << _setmode << "]#";
+ print_prompt(str, cur_mode);
+ str << "# ";
if(out.lineedit(str.str(),input,liquids_hist) == -1)
return CR_FAILURE;
liquids_hist.add(input);
@@ -147,6 +212,10 @@ command_result df_liquids (color_ostream &out_, vector & parameters)
<< "f+ - make the spawned liquid flow" << endl
<< "f. - don't change flow state (read state in flow mode)" << endl
<< "f- - make the spawned liquid static" << endl
+ << "Permaflow (only for water):" << endl
+ << "pf. - don't change permaflow state" << endl
+ << "pf- - make the spawned liquid static" << endl
+ << "pf[NS][EW] - make the spawned liquid permanently flow" << endl
<< "0-7 - set liquid amount" << endl
<< "Brush:" << endl
<< "point - single tile [p]" << endl
@@ -168,38 +237,39 @@ command_result df_liquids (color_ostream &out_, vector & parameters)
}
else if(command == "m")
{
- mode = "magma";
+ cur_mode.paint = P_MAGMA;
}
else if(command == "o")
{
- mode = "obsidian";
+ cur_mode.paint = P_OBSIDIAN;
}
else if(command == "of")
{
- mode = "obsidian_floor";
+ cur_mode.paint = P_OBSIDIAN_FLOOR;
}
else if(command == "w")
{
- mode = "water";
+ cur_mode.paint = P_WATER;
}
else if(command == "f")
{
- mode = "flowbits";
+ cur_mode.paint = P_FLOW_BITS;
}
else if(command == "rs")
{
- mode = "riversource";
+ cur_mode.paint = P_RIVER_SOURCE;
}
else if(command == "wclean")
{
- mode = "wclean";
+ cur_mode.paint = P_WCLEAN;
}
else if(command == "point" || command == "p")
{
- brushname = "point";
+ cur_mode.brush = B_POINT;
}
else if(command == "range" || command == "r")
{
+ int width, height, z_levels;
command_result res = parseRectangle(out, commands, 1, commands.size(),
width, height, z_levels);
if (res != CR_OK)
@@ -209,24 +279,26 @@ command_result df_liquids (color_ostream &out_, vector & parameters)
if (width == 1 && height == 1 && z_levels == 1)
{
- brushname = "point";
+ cur_mode.brush = B_POINT;
+ cur_mode.size = df::coord(1, 1, 1);
}
else
{
- brushname = "range";
+ cur_mode.brush = B_RANGE;
+ cur_mode.size = df::coord(width, height, z_levels);
}
}
else if(command == "block")
{
- brushname = "block";
+ cur_mode.brush = B_BLOCK;
}
else if(command == "column")
{
- brushname = "column";
+ cur_mode.brush = B_COLUMN;
}
else if(command == "flood")
{
- brushname = "flood";
+ cur_mode.brush = B_FLOOD;
}
else if(command == "q")
{
@@ -234,45 +306,59 @@ command_result df_liquids (color_ostream &out_, vector & parameters)
}
else if(command == "f+")
{
- flowmode = "f+";
+ cur_mode.flowmode = M_INC;
}
else if(command == "f-")
{
- flowmode = "f-";
+ cur_mode.flowmode = M_DEC;
}
else if(command == "f.")
{
- flowmode = "f.";
+ cur_mode.flowmode = M_KEEP;
}
else if(command == "s+")
{
- _setmode = "s+";
+ cur_mode.setmode = M_INC;
}
else if(command == "s-")
{
- _setmode = "s-";
+ cur_mode.setmode = M_DEC;
}
else if(command == "s.")
{
- _setmode = "s.";
+ cur_mode.setmode = M_KEEP;
+ }
+ else if (command.size() > 2 && memcmp(command.c_str(), "pf", 2) == 0)
+ {
+ auto *tail = command.c_str()+2;
+ for (int pm = PF_KEEP; pm <= PF_SOUTHWEST; pm++)
+ {
+ if (strcmp(tail, permaflow_name[pm]) != 0)
+ continue;
+ cur_mode.permaflow = PermaflowMode(pm);
+ tail = NULL;
+ break;
+ }
+ if (tail)
+ out << command << " : invalid permaflow mode" << endl;
}
// blah blah, bad code, bite me.
else if(command == "0")
- amount = 0;
+ cur_mode.amount = 0;
else if(command == "1")
- amount = 1;
+ cur_mode.amount = 1;
else if(command == "2")
- amount = 2;
+ cur_mode.amount = 2;
else if(command == "3")
- amount = 3;
+ cur_mode.amount = 3;
else if(command == "4")
- amount = 4;
+ cur_mode.amount = 4;
else if(command == "5")
- amount = 5;
+ cur_mode.amount = 5;
else if(command == "6")
- amount = 6;
+ cur_mode.amount = 6;
else if(command == "7")
- amount = 7;
+ cur_mode.amount = 7;
else if(command.empty())
{
df_liquids_execute(out);
@@ -298,78 +384,75 @@ command_result df_liquids_here (color_ostream &out, vector & parameters
}
out.print("Run liquids-here with these parameters: ");
- out << "[" << mode << ":" << brushname;
- if (brushname == "range")
- out << "(w" << width << ":h" << height << ":z" << z_levels << ")";
- out << ":" << amount << ":" << flowmode << ":" << _setmode << "]\n";
+ print_prompt(out, cur_mode);
+ out << endl;
return df_liquids_execute(out);
}
command_result df_liquids_execute(color_ostream &out)
{
- // create brush type depending on old parameters
- Brush * brush;
+ CoreSuspender suspend;
- if (brushname == "point")
+ auto cursor = Gui::getCursorPos();
+ if (!cursor.isValid())
{
- brush = new RectangleBrush(1,1,1,0,0,0);
- //width = 1;
- //height = 1;
- //z_levels = 1;
+ out.printerr("Can't get cursor coords! Make sure you have a cursor active in DF.\n");
+ return CR_WRONG_USAGE;
}
- else if (brushname == "range")
- {
- brush = new RectangleBrush(width,height,z_levels,0,0,0);
- }
- else if(brushname == "block")
+
+ auto rv = df_liquids_execute(out, cur_mode, cursor);
+ if (rv == CR_OK)
+ out << "OK" << endl;
+ return rv;
+}
+
+command_result df_liquids_execute(color_ostream &out, OperationMode &cur_mode, df::coord cursor)
+{
+ // create brush type depending on old parameters
+ Brush *brush;
+
+ switch (cur_mode.brush)
{
+ case B_POINT:
+ brush = new RectangleBrush(1,1,1,0,0,0);
+ break;
+ case B_RANGE:
+ brush = new RectangleBrush(cur_mode.size.x,cur_mode.size.y,cur_mode.size.z,0,0,0);
+ break;
+ case B_BLOCK:
brush = new BlockBrush();
- }
- else if(brushname == "column")
- {
+ break;
+ case B_COLUMN:
brush = new ColumnBrush();
- }
- else if(brushname == "flood")
- {
+ break;
+ case B_FLOOD:
brush = new FloodBrush(&Core::getInstance());
- }
- else
- {
+ break;
+ default:
// this should never happen!
out << "Old brushtype is invalid! Resetting to point brush.\n";
- brushname = "point";
- width = 1;
- height = 1;
- z_levels = 1;
- brush = new RectangleBrush(width,height,z_levels,0,0,0);
+ cur_mode.brush = B_POINT;
+ brush = new RectangleBrush(1,1,1,0,0,0);
}
- CoreSuspender suspend;
+ std::auto_ptr brush_ref(brush);
- do
+ if (!Maps::IsValid())
{
- if (!Maps::IsValid())
- {
- out << "Can't see any DF map loaded." << endl;
- break;;
- }
- int32_t x,y,z;
- if(!Gui::getCursorCoords(x,y,z))
- {
- out << "Can't get cursor coords! Make sure you have a cursor active in DF." << endl;
- break;
- }
- out << "cursor coords: " << x << "/" << y << "/" << z << endl;
- MapCache mcache;
- DFHack::DFCoord cursor(x,y,z);
- coord_vec all_tiles = brush->points(mcache,cursor);
- out << "working..." << endl;
+ out << "Can't see any DF map loaded." << endl;
+ return CR_FAILURE;
+ }
- // Force the game to recompute its walkability cache
- df::global::world->reindex_pathfinding = true;
+ MapCache mcache;
+ coord_vec all_tiles = brush->points(mcache,cursor);
- if(mode == "obsidian")
+ // Force the game to recompute its walkability cache
+ df::global::world->reindex_pathfinding = true;
+
+ switch (cur_mode.paint)
+ {
+ case P_OBSIDIAN:
{
coord_vec::iterator iter = all_tiles.begin();
while (iter != all_tiles.end())
@@ -383,8 +466,9 @@ command_result df_liquids_execute(color_ostream &out)
mcache.setDesignationAt(*iter, des);
iter ++;
}
+ break;
}
- if(mode == "obsidian_floor")
+ case P_OBSIDIAN_FLOOR:
{
coord_vec::iterator iter = all_tiles.begin();
while (iter != all_tiles.end())
@@ -392,8 +476,9 @@ command_result df_liquids_execute(color_ostream &out)
mcache.setTiletypeAt(*iter, findRandomVariant(tiletype::LavaFloor1));
iter ++;
}
+ break;
}
- else if(mode == "riversource")
+ case P_RIVER_SOURCE:
{
coord_vec::iterator iter = all_tiles.begin();
while (iter != all_tiles.end())
@@ -413,8 +498,9 @@ command_result df_liquids_execute(color_ostream &out)
iter++;
}
+ break;
}
- else if(mode=="wclean")
+ case P_WCLEAN:
{
coord_vec::iterator iter = all_tiles.begin();
while (iter != all_tiles.end())
@@ -426,8 +512,11 @@ command_result df_liquids_execute(color_ostream &out)
mcache.setDesignationAt(current,des);
iter++;
}
+ break;
}
- else if(mode== "magma" || mode== "water" || mode == "flowbits")
+ case P_MAGMA:
+ case P_WATER:
+ case P_FLOW_BITS:
{
set seen_blocks;
coord_vec::iterator iter = all_tiles.begin();
@@ -442,6 +531,7 @@ command_result df_liquids_execute(color_ostream &out)
iter ++;
continue;
}
+ auto raw_block = block->getRaw();
df::tile_designation des = mcache.designationAt(current);
df::tiletype tt = mcache.tiletypeAt(current);
// don't put liquids into places where they don't belong...
@@ -450,30 +540,29 @@ command_result df_liquids_execute(color_ostream &out)
iter++;
continue;
}
- if(mode != "flowbits")
+ if(cur_mode.paint != P_FLOW_BITS)
{
unsigned old_amount = des.bits.flow_size;
unsigned new_amount = old_amount;
df::tile_liquid old_liquid = des.bits.liquid_type;
df::tile_liquid new_liquid = old_liquid;
// Compute new liquid type and amount
- if(_setmode == "s.")
- {
- new_amount = amount;
- }
- else if(_setmode == "s+")
- {
- if(old_amount < amount)
- new_amount = amount;
- }
- else if(_setmode == "s-")
+ switch (cur_mode.setmode)
{
- if (old_amount > amount)
- new_amount = amount;
+ case M_KEEP:
+ new_amount = cur_mode.amount;
+ break;
+ case M_INC:
+ if(old_amount < cur_mode.amount)
+ new_amount = cur_mode.amount;
+ break;
+ case M_DEC:
+ if (old_amount > cur_mode.amount)
+ new_amount = cur_mode.amount;
}
- if (mode == "magma")
+ if (cur_mode.paint == P_MAGMA)
new_liquid = tile_liquid::Magma;
- else if (mode == "water")
+ else if (cur_mode.paint == P_WATER)
new_liquid = tile_liquid::Water;
// Store new amount and type
des.bits.flow_size = new_amount;
@@ -502,40 +591,77 @@ command_result df_liquids_execute(color_ostream &out)
// request flow engine updates
block->enableBlockUpdates(new_amount != old_amount, new_liquid != old_liquid);
}
+ if (cur_mode.permaflow != PF_KEEP && raw_block)
+ {
+ auto &flow = raw_block->liquid_flow[current.x&15][current.y&15];
+ flow.bits.perm_flow_dir = permaflow_id[cur_mode.permaflow];
+ flow.bits.temp_flow_timer = 0;
+ }
seen_blocks.insert(block);
iter++;
}
set ::iterator biter = seen_blocks.begin();
while (biter != seen_blocks.end())
{
- if(flowmode == "f+")
+ switch (cur_mode.flowmode)
{
+ case M_INC:
(*biter)->enableBlockUpdates(true);
- }
- else if(flowmode == "f-")
- {
+ break;
+ case M_DEC:
if (auto block = (*biter)->getRaw())
{
block->flags.bits.update_liquid = false;
block->flags.bits.update_liquid_twice = false;
}
- }
- else
- {
- auto bflags = (*biter)->BlockFlags();
- out << "flow bit 1 = " << bflags.bits.update_liquid << endl;
- out << "flow bit 2 = " << bflags.bits.update_liquid_twice << endl;
+ break;
+ case M_KEEP:
+ {
+ auto bflags = (*biter)->BlockFlags();
+ out << "flow bit 1 = " << bflags.bits.update_liquid << endl;
+ out << "flow bit 2 = " << bflags.bits.update_liquid_twice << endl;
+ }
}
biter ++;
}
+ break;
}
- if(mcache.WriteAll())
- out << "OK" << endl;
- else
- out << "Something failed horribly! RUN!" << endl;
- } while (0);
+ }
+
+ if(!mcache.WriteAll())
+ {
+ out << "Something failed horribly! RUN!" << endl;
+ return CR_FAILURE;
+ }
- // cleanup
- delete brush;
return CR_OK;
}
+
+static int paint(lua_State *L)
+{
+ df::coord pos;
+ OperationMode mode;
+
+ lua_settop(L, 8);
+ Lua::CheckDFAssign(L, &pos, 1);
+ if (!pos.isValid())
+ luaL_argerror(L, 1, "invalid cursor position");
+ mode.brush = (BrushType)luaL_checkoption(L, 2, NULL, brush_name);
+ mode.paint = (PaintMode)luaL_checkoption(L, 3, NULL, paint_mode_name);
+ mode.amount = luaL_optint(L, 4, 7);
+ if (mode.amount < 0 || mode.amount > 7)
+ luaL_argerror(L, 4, "invalid liquid amount");
+ if (!lua_isnil(L, 5))
+ Lua::CheckDFAssign(L, &mode.size, 5);
+ mode.setmode = (ModifyMode)luaL_checkoption(L, 6, ".", modify_mode_name);
+ mode.flowmode = (ModifyMode)luaL_checkoption(L, 7, "+", modify_mode_name);
+ mode.permaflow = (PermaflowMode)luaL_checkoption(L, 8, ".", permaflow_name);
+
+ lua_pushboolean(L, df_liquids_execute(*Lua::GetOutput(L), mode, pos));
+ return 1;
+}
+
+DFHACK_PLUGIN_LUA_COMMANDS {
+ DFHACK_LUA_COMMAND(paint),
+ DFHACK_LUA_END
+};
diff --git a/plugins/lua/liquids.lua b/plugins/lua/liquids.lua
new file mode 100644
index 000000000..22ce4da35
--- /dev/null
+++ b/plugins/lua/liquids.lua
@@ -0,0 +1,11 @@
+local _ENV = mkmodule('plugins.liquids')
+
+--[[
+
+ Native functions:
+
+ * paint(pos,brush,paint,amount,size,setmode,flowmode)
+
+--]]
+
+return _ENV
\ No newline at end of file
diff --git a/plugins/lua/power-meter.lua b/plugins/lua/power-meter.lua
new file mode 100644
index 000000000..310e51c4e
--- /dev/null
+++ b/plugins/lua/power-meter.lua
@@ -0,0 +1,11 @@
+local _ENV = mkmodule('plugins.power-meter')
+
+--[[
+
+ Native functions:
+
+ * makePowerMeter(plate_info,min_power,max_power,invert)
+
+--]]
+
+return _ENV
\ No newline at end of file
diff --git a/plugins/lua/rename.lua b/plugins/lua/rename.lua
new file mode 100644
index 000000000..0e7128f57
--- /dev/null
+++ b/plugins/lua/rename.lua
@@ -0,0 +1,13 @@
+local _ENV = mkmodule('plugins.rename')
+
+--[[
+
+ Native functions:
+
+ * canRenameBuilding(building)
+ * isRenamingBuilding(building)
+ * renameBuilding(building, name)
+
+--]]
+
+return _ENV
\ No newline at end of file
diff --git a/plugins/lua/siege-engine.lua b/plugins/lua/siege-engine.lua
new file mode 100644
index 000000000..01b5d1447
--- /dev/null
+++ b/plugins/lua/siege-engine.lua
@@ -0,0 +1,13 @@
+local _ENV = mkmodule('plugins.siege-engine')
+
+--[[
+
+ Native functions:
+
+ * getTargetArea(building) -> point1, point2
+ * clearTargetArea(building)
+ * setTargetArea(building, point1, point2) -> true/false
+
+--]]
+
+return _ENV
\ No newline at end of file
diff --git a/plugins/manipulator.cpp b/plugins/manipulator.cpp
index 434ba08c8..1a90d2eea 100644
--- a/plugins/manipulator.cpp
+++ b/plugins/manipulator.cpp
@@ -11,6 +11,7 @@
#include
#include
#include
+#include
#include
#include "df/world.h"
@@ -22,6 +23,7 @@
#include "df/unit.h"
#include "df/unit_soul.h"
#include "df/unit_skill.h"
+#include "df/creature_graphics_role.h"
#include "df/creature_raw.h"
#include "df/caste_raw.h"
@@ -37,204 +39,204 @@ using df::global::ui;
using df::global::gps;
using df::global::enabler;
-DFHACK_PLUGIN("manipulator");
-
struct SkillLevel
{
- const char *name;
- int points;
- char abbrev;
+ const char *name;
+ int points;
+ char abbrev;
};
-#define NUM_SKILL_LEVELS (sizeof(skill_levels) / sizeof(SkillLevel))
+#define NUM_SKILL_LEVELS (sizeof(skill_levels) / sizeof(SkillLevel))
// The various skill rankings. Zero skill is hardcoded to "Not" and '-'.
const SkillLevel skill_levels[] = {
- {"Dabbling", 500, '0'},
- {"Novice", 600, '1'},
- {"Adequate", 700, '2'},
- {"Competent", 800, '3'},
- {"Skilled", 900, '4'},
- {"Proficient", 1000, '5'},
- {"Talented", 1100, '6'},
- {"Adept", 1200, '7'},
- {"Expert", 1300, '8'},
- {"Professional",1400, '9'},
- {"Accomplished",1500, 'A'},
- {"Great", 1600, 'B'},
- {"Master", 1700, 'C'},
- {"High Master", 1800, 'D'},
- {"Grand Master",1900, 'E'},
- {"Legendary", 2000, 'U'},
- {"Legendary+1", 2100, 'V'},
- {"Legendary+2", 2200, 'W'},
- {"Legendary+3", 2300, 'X'},
- {"Legendary+4", 2400, 'Y'},
- {"Legendary+5", 0, 'Z'}
+ {"Dabbling", 500, '0'},
+ {"Novice", 600, '1'},
+ {"Adequate", 700, '2'},
+ {"Competent", 800, '3'},
+ {"Skilled", 900, '4'},
+ {"Proficient", 1000, '5'},
+ {"Talented", 1100, '6'},
+ {"Adept", 1200, '7'},
+ {"Expert", 1300, '8'},
+ {"Professional",1400, '9'},
+ {"Accomplished",1500, 'A'},
+ {"Great", 1600, 'B'},
+ {"Master", 1700, 'C'},
+ {"High Master", 1800, 'D'},
+ {"Grand Master",1900, 'E'},
+ {"Legendary", 2000, 'U'},
+ {"Legendary+1", 2100, 'V'},
+ {"Legendary+2", 2200, 'W'},
+ {"Legendary+3", 2300, 'X'},
+ {"Legendary+4", 2400, 'Y'},
+ {"Legendary+5", 0, 'Z'}
};
struct SkillColumn
{
- df::profession profession;
- df::unit_labor labor;
- df::job_skill skill;
- char label[3];
+ int group; // for navigation and mass toggling
+ int8_t color; // for column headers
+ df::profession profession; // to display graphical tiles
+ df::unit_labor labor; // toggled when pressing Enter
+ df::job_skill skill; // displayed rating
+ char label[3]; // column header
bool special; // specified labor is mutually exclusive with all other special labors
};
-#define NUM_COLUMNS (sizeof(columns) / sizeof(SkillColumn))
+#define NUM_COLUMNS (sizeof(columns) / sizeof(SkillColumn))
-// All of the skill/labor columns we want to display. Includes profession (for color), labor, skill, and 2 character label
+// All of the skill/labor columns we want to display.
const SkillColumn columns[] = {
// Mining
- {profession::MINER, unit_labor::MINE, job_skill::MINING, "Mi", true},
+ {0, 7, profession::MINER, unit_labor::MINE, job_skill::MINING, "Mi", true},
// Woodworking
- {profession::WOODWORKER, unit_labor::CARPENTER, job_skill::CARPENTRY, "Ca"},
- {profession::WOODWORKER, unit_labor::BOWYER, job_skill::BOWYER, "Bw"},
- {profession::WOODWORKER, unit_labor::CUTWOOD, job_skill::WOODCUTTING, "WC", true},
+ {1, 14, profession::CARPENTER, unit_labor::CARPENTER, job_skill::CARPENTRY, "Ca"},
+ {1, 14, profession::BOWYER, unit_labor::BOWYER, job_skill::BOWYER, "Bw"},
+ {1, 14, profession::WOODCUTTER, unit_labor::CUTWOOD, job_skill::WOODCUTTING, "WC", true},
// Stoneworking
- {profession::STONEWORKER, unit_labor::MASON, job_skill::MASONRY, "Ma"},
- {profession::STONEWORKER, unit_labor::DETAIL, job_skill::DETAILSTONE, "En"},
+ {2, 15, profession::MASON, unit_labor::MASON, job_skill::MASONRY, "Ma"},
+ {2, 15, profession::ENGRAVER, unit_labor::DETAIL, job_skill::DETAILSTONE, "En"},
// Hunting/Related
- {profession::RANGER, unit_labor::ANIMALTRAIN, job_skill::ANIMALTRAIN, "Tr"},
- {profession::RANGER, unit_labor::ANIMALCARE, job_skill::ANIMALCARE, "Ca"},
- {profession::RANGER, unit_labor::HUNT, job_skill::SNEAK, "Hu", true},
- {profession::RANGER, unit_labor::TRAPPER, job_skill::TRAPPING, "Tr"},
- {profession::RANGER, unit_labor::DISSECT_VERMIN, job_skill::DISSECT_VERMIN, "Di"},
+ {3, 2, profession::ANIMAL_TRAINER, unit_labor::ANIMALTRAIN, job_skill::ANIMALTRAIN, "Tn"},
+ {3, 2, profession::ANIMAL_CARETAKER, unit_labor::ANIMALCARE, job_skill::ANIMALCARE, "Ca"},
+ {3, 2, profession::HUNTER, unit_labor::HUNT, job_skill::SNEAK, "Hu", true},
+ {3, 2, profession::TRAPPER, unit_labor::TRAPPER, job_skill::TRAPPING, "Tr"},
+ {3, 2, profession::ANIMAL_DISSECTOR, unit_labor::DISSECT_VERMIN, job_skill::DISSECT_VERMIN, "Di"},
// Healthcare
- {profession::DOCTOR, unit_labor::DIAGNOSE, job_skill::DIAGNOSE, "Di"},
- {profession::DOCTOR, unit_labor::SURGERY, job_skill::SURGERY, "Su"},
- {profession::DOCTOR, unit_labor::BONE_SETTING, job_skill::SET_BONE, "Bo"},
- {profession::DOCTOR, unit_labor::SUTURING, job_skill::SUTURE, "St"},
- {profession::DOCTOR, unit_labor::DRESSING_WOUNDS, job_skill::DRESS_WOUNDS, "Dr"},
- {profession::DOCTOR, unit_labor::FEED_WATER_CIVILIANS, job_skill::NONE, "Fd"},
- {profession::DOCTOR, unit_labor::RECOVER_WOUNDED, job_skill::NONE, "Re"},
+ {4, 5, profession::DIAGNOSER, unit_labor::DIAGNOSE, job_skill::DIAGNOSE, "Di"},
+ {4, 5, profession::SURGEON, unit_labor::SURGERY, job_skill::SURGERY, "Su"},
+ {4, 5, profession::BONE_SETTER, unit_labor::BONE_SETTING, job_skill::SET_BONE, "Bo"},
+ {4, 5, profession::SUTURER, unit_labor::SUTURING, job_skill::SUTURE, "St"},
+ {4, 5, profession::DOCTOR, unit_labor::DRESSING_WOUNDS, job_skill::DRESS_WOUNDS, "Dr"},
+ {4, 5, profession::NONE, unit_labor::FEED_WATER_CIVILIANS, job_skill::NONE, "Fd"},
+ {4, 5, profession::NONE, unit_labor::RECOVER_WOUNDED, job_skill::NONE, "Re"},
// Farming/Related
- {profession::FARMER, unit_labor::BUTCHER, job_skill::BUTCHER, "Bu"},
- {profession::FARMER, unit_labor::TANNER, job_skill::TANNER, "Ta"},
- {profession::FARMER, unit_labor::PLANT, job_skill::PLANT, "Gr"},
- {profession::FARMER, unit_labor::DYER, job_skill::DYER, "Dy"},
- {profession::FARMER, unit_labor::SOAP_MAKER, job_skill::SOAP_MAKING, "So"},
- {profession::FARMER, unit_labor::BURN_WOOD, job_skill::WOOD_BURNING, "WB"},
- {profession::FARMER, unit_labor::POTASH_MAKING, job_skill::POTASH_MAKING, "Po"},
- {profession::FARMER, unit_labor::LYE_MAKING, job_skill::LYE_MAKING, "Ly"},
- {profession::FARMER, unit_labor::MILLER, job_skill::MILLING, "Ml"},
- {profession::FARMER, unit_labor::BREWER, job_skill::BREWING, "Br"},
- {profession::FARMER, unit_labor::HERBALIST, job_skill::HERBALISM, "He"},
- {profession::FARMER, unit_labor::PROCESS_PLANT, job_skill::PROCESSPLANTS, "Th"},
- {profession::FARMER, unit_labor::MAKE_CHEESE, job_skill::CHEESEMAKING, "Ch"},
- {profession::FARMER, unit_labor::MILK, job_skill::MILK, "Mk"},
- {profession::FARMER, unit_labor::SHEARER, job_skill::SHEARING, "Sh"},
- {profession::FARMER, unit_labor::SPINNER, job_skill::SPINNING, "Sp"},
- {profession::FARMER, unit_labor::COOK, job_skill::COOK, "Co"},
- {profession::FARMER, unit_labor::PRESSING, job_skill::PRESSING, "Pr"},
- {profession::FARMER, unit_labor::BEEKEEPING, job_skill::BEEKEEPING, "Be"},
+ {5, 6, profession::BUTCHER, unit_labor::BUTCHER, job_skill::BUTCHER, "Bu"},
+ {5, 6, profession::TANNER, unit_labor::TANNER, job_skill::TANNER, "Ta"},
+ {5, 6, profession::PLANTER, unit_labor::PLANT, job_skill::PLANT, "Gr"},
+ {5, 6, profession::DYER, unit_labor::DYER, job_skill::DYER, "Dy"},
+ {5, 6, profession::SOAP_MAKER, unit_labor::SOAP_MAKER, job_skill::SOAP_MAKING, "So"},
+ {5, 6, profession::WOOD_BURNER, unit_labor::BURN_WOOD, job_skill::WOOD_BURNING, "WB"},
+ {5, 6, profession::POTASH_MAKER, unit_labor::POTASH_MAKING, job_skill::POTASH_MAKING, "Po"},
+ {5, 6, profession::LYE_MAKER, unit_labor::LYE_MAKING, job_skill::LYE_MAKING, "Ly"},
+ {5, 6, profession::MILLER, unit_labor::MILLER, job_skill::MILLING, "Ml"},
+ {5, 6, profession::BREWER, unit_labor::BREWER, job_skill::BREWING, "Br"},
+ {5, 6, profession::HERBALIST, unit_labor::HERBALIST, job_skill::HERBALISM, "He"},
+ {5, 6, profession::THRESHER, unit_labor::PROCESS_PLANT, job_skill::PROCESSPLANTS, "Th"},
+ {5, 6, profession::CHEESE_MAKER, unit_labor::MAKE_CHEESE, job_skill::CHEESEMAKING, "Ch"},
+ {5, 6, profession::MILKER, unit_labor::MILK, job_skill::MILK, "Mk"},
+ {5, 6, profession::SHEARER, unit_labor::SHEARER, job_skill::SHEARING, "Sh"},
+ {5, 6, profession::SPINNER, unit_labor::SPINNER, job_skill::SPINNING, "Sp"},
+ {5, 6, profession::COOK, unit_labor::COOK, job_skill::COOK, "Co"},
+ {5, 6, profession::PRESSER, unit_labor::PRESSING, job_skill::PRESSING, "Pr"},
+ {5, 6, profession::BEEKEEPER, unit_labor::BEEKEEPING, job_skill::BEEKEEPING, "Be"},
// Fishing/Related
- {profession::FISHERMAN, unit_labor::FISH, job_skill::FISH, "Fi"},
- {profession::FISHERMAN, unit_labor::CLEAN_FISH, job_skill::PROCESSFISH, "Cl"},
- {profession::FISHERMAN, unit_labor::DISSECT_FISH, job_skill::DISSECT_FISH, "Di"},
+ {6, 1, profession::FISHERMAN, unit_labor::FISH, job_skill::FISH, "Fi"},
+ {6, 1, profession::FISH_CLEANER, unit_labor::CLEAN_FISH, job_skill::PROCESSFISH, "Cl"},
+ {6, 1, profession::FISH_DISSECTOR, unit_labor::DISSECT_FISH, job_skill::DISSECT_FISH, "Di"},
// Metalsmithing
- {profession::METALSMITH, unit_labor::SMELT, job_skill::SMELT, "Fu"},
- {profession::METALSMITH, unit_labor::FORGE_WEAPON, job_skill::FORGE_WEAPON, "We"},
- {profession::METALSMITH, unit_labor::FORGE_ARMOR, job_skill::FORGE_ARMOR, "Ar"},
- {profession::METALSMITH, unit_labor::FORGE_FURNITURE, job_skill::FORGE_FURNITURE, "Bl"},
- {profession::METALSMITH, unit_labor::METAL_CRAFT, job_skill::METALCRAFT, "Cr"},
+ {7, 8, profession::FURNACE_OPERATOR, unit_labor::SMELT, job_skill::SMELT, "Fu"},
+ {7, 8, profession::WEAPONSMITH, unit_labor::FORGE_WEAPON, job_skill::FORGE_WEAPON, "We"},
+ {7, 8, profession::ARMORER, unit_labor::FORGE_ARMOR, job_skill::FORGE_ARMOR, "Ar"},
+ {7, 8, profession::BLACKSMITH, unit_labor::FORGE_FURNITURE, job_skill::FORGE_FURNITURE, "Bl"},
+ {7, 8, profession::METALCRAFTER, unit_labor::METAL_CRAFT, job_skill::METALCRAFT, "Cr"},
// Jewelry
- {profession::JEWELER, unit_labor::CUT_GEM, job_skill::CUTGEM, "Cu"},
- {profession::JEWELER, unit_labor::ENCRUST_GEM, job_skill::ENCRUSTGEM, "Se"},
+ {8, 10, profession::GEM_CUTTER, unit_labor::CUT_GEM, job_skill::CUTGEM, "Cu"},
+ {8, 10, profession::GEM_SETTER, unit_labor::ENCRUST_GEM, job_skill::ENCRUSTGEM, "Se"},
// Crafts
- {profession::CRAFTSMAN, unit_labor::LEATHER, job_skill::LEATHERWORK, "Le"},
- {profession::CRAFTSMAN, unit_labor::WOOD_CRAFT, job_skill::WOODCRAFT, "Wo"},
- {profession::CRAFTSMAN, unit_labor::STONE_CRAFT, job_skill::STONECRAFT, "St"},
- {profession::CRAFTSMAN, unit_labor::BONE_CARVE, job_skill::BONECARVE, "Bo"},
- {profession::CRAFTSMAN, unit_labor::GLASSMAKER, job_skill::GLASSMAKER, "Gl"},
- {profession::CRAFTSMAN, unit_labor::WEAVER, job_skill::WEAVING, "We"},
- {profession::CRAFTSMAN, unit_labor::CLOTHESMAKER, job_skill::CLOTHESMAKING, "Cl"},
- {profession::CRAFTSMAN, unit_labor::EXTRACT_STRAND, job_skill::EXTRACT_STRAND, "Ad"},
- {profession::CRAFTSMAN, unit_labor::POTTERY, job_skill::POTTERY, "Po"},
- {profession::CRAFTSMAN, unit_labor::GLAZING, job_skill::GLAZING, "Gl"},
- {profession::CRAFTSMAN, unit_labor::WAX_WORKING, job_skill::WAX_WORKING, "Wx"},
+ {9, 9, profession::LEATHERWORKER, unit_labor::LEATHER, job_skill::LEATHERWORK, "Le"},
+ {9, 9, profession::WOODCRAFTER, unit_labor::WOOD_CRAFT, job_skill::WOODCRAFT, "Wo"},
+ {9, 9, profession::STONECRAFTER, unit_labor::STONE_CRAFT, job_skill::STONECRAFT, "St"},
+ {9, 9, profession::BONE_CARVER, unit_labor::BONE_CARVE, job_skill::BONECARVE, "Bo"},
+ {9, 9, profession::GLASSMAKER, unit_labor::GLASSMAKER, job_skill::GLASSMAKER, "Gl"},
+ {9, 9, profession::WEAVER, unit_labor::WEAVER, job_skill::WEAVING, "We"},
+ {9, 9, profession::CLOTHIER, unit_labor::CLOTHESMAKER, job_skill::CLOTHESMAKING, "Cl"},
+ {9, 9, profession::STRAND_EXTRACTOR, unit_labor::EXTRACT_STRAND, job_skill::EXTRACT_STRAND, "Ad"},
+ {9, 9, profession::POTTER, unit_labor::POTTERY, job_skill::POTTERY, "Po"},
+ {9, 9, profession::GLAZER, unit_labor::GLAZING, job_skill::GLAZING, "Gl"},
+ {9, 9, profession::WAX_WORKER, unit_labor::WAX_WORKING, job_skill::WAX_WORKING, "Wx"},
// Engineering
- {profession::ENGINEER, unit_labor::SIEGECRAFT, job_skill::SIEGECRAFT, "En"},
- {profession::ENGINEER, unit_labor::SIEGEOPERATE, job_skill::SIEGEOPERATE, "Op"},
- {profession::ENGINEER, unit_labor::MECHANIC, job_skill::MECHANICS, "Me"},
- {profession::ENGINEER, unit_labor::OPERATE_PUMP, job_skill::OPERATE_PUMP, "Pu"},
+ {10, 12, profession::SIEGE_ENGINEER, unit_labor::SIEGECRAFT, job_skill::SIEGECRAFT, "En"},
+ {10, 12, profession::SIEGE_OPERATOR, unit_labor::SIEGEOPERATE, job_skill::SIEGEOPERATE, "Op"},
+ {10, 12, profession::MECHANIC, unit_labor::MECHANIC, job_skill::MECHANICS, "Me"},
+ {10, 12, profession::PUMP_OPERATOR, unit_labor::OPERATE_PUMP, job_skill::OPERATE_PUMP, "Pu"},
// Hauling
- {profession::STANDARD, unit_labor::HAUL_STONE, job_skill::NONE, "St"},
- {profession::STANDARD, unit_labor::HAUL_WOOD, job_skill::NONE, "Wo"},
- {profession::STANDARD, unit_labor::HAUL_ITEM, job_skill::NONE, "It"},
- {profession::STANDARD, unit_labor::HAUL_BODY, job_skill::NONE, "Bu"},
- {profession::STANDARD, unit_labor::HAUL_FOOD, job_skill::NONE, "Fo"},
- {profession::STANDARD, unit_labor::HAUL_REFUSE, job_skill::NONE, "Re"},
- {profession::STANDARD, unit_labor::HAUL_FURNITURE, job_skill::NONE, "Fu"},
- {profession::STANDARD, unit_labor::HAUL_ANIMAL, job_skill::NONE, "An"},
- {profession::STANDARD, unit_labor::PUSH_HAUL_VEHICLE, job_skill::NONE, "Ve"},
+ {11, 3, profession::NONE, unit_labor::HAUL_STONE, job_skill::NONE, "St"},
+ {11, 3, profession::NONE, unit_labor::HAUL_WOOD, job_skill::NONE, "Wo"},
+ {11, 3, profession::NONE, unit_labor::HAUL_ITEM, job_skill::NONE, "It"},
+ {11, 3, profession::NONE, unit_labor::HAUL_BODY, job_skill::NONE, "Bu"},
+ {11, 3, profession::NONE, unit_labor::HAUL_FOOD, job_skill::NONE, "Fo"},
+ {11, 3, profession::NONE, unit_labor::HAUL_REFUSE, job_skill::NONE, "Re"},
+ {11, 3, profession::NONE, unit_labor::HAUL_FURNITURE, job_skill::NONE, "Fu"},
+ {11, 3, profession::NONE, unit_labor::HAUL_ANIMAL, job_skill::NONE, "An"},
+ {11, 3, profession::NONE, unit_labor::PUSH_HAUL_VEHICLE, job_skill::NONE, "Ve"},
// Other Jobs
- {profession::CHILD, unit_labor::ARCHITECT, job_skill::DESIGNBUILDING, "Ar"},
- {profession::CHILD, unit_labor::ALCHEMIST, job_skill::ALCHEMY, "Al"},
- {profession::CHILD, unit_labor::CLEAN, job_skill::NONE, "Cl"},
-// Military
- {profession::WRESTLER, unit_labor::NONE, job_skill::WRESTLING, "Wr"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::AXE, "Ax"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::SWORD, "Sw"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::MACE, "Mc"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::HAMMER, "Ha"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::SPEAR, "Sp"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::DAGGER, "Kn"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::CROSSBOW, "Cb"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::BOW, "Bo"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::BLOWGUN, "Bl"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::PIKE, "Pk"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::WHIP, "La"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::ARMOR, "Ar"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::SHIELD, "Sh"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::BITE, "Bi"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::GRASP_STRIKE, "Pu"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::STANCE_STRIKE, "Ki"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::DODGING, "Do"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::MISC_WEAPON, "Mi"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::MELEE_COMBAT, "Fi"},
- {profession::WRESTLER, unit_labor::NONE, job_skill::RANGED_COMBAT, "Ar"},
-
- {profession::RECRUIT, unit_labor::NONE, job_skill::LEADERSHIP, "Le"},
- {profession::RECRUIT, unit_labor::NONE, job_skill::TEACHING, "Te"},
- {profession::RECRUIT, unit_labor::NONE, job_skill::KNOWLEDGE_ACQUISITION, "Lr"},
- {profession::RECRUIT, unit_labor::NONE, job_skill::DISCIPLINE, "Di"},
- {profession::RECRUIT, unit_labor::NONE, job_skill::CONCENTRATION, "Co"},
- {profession::RECRUIT, unit_labor::NONE, job_skill::SITUATIONAL_AWARENESS, "Aw"},
- {profession::RECRUIT, unit_labor::NONE, job_skill::COORDINATION, "Cr"},
- {profession::RECRUIT, unit_labor::NONE, job_skill::BALANCE, "Ba"},
-
+ {12, 4, profession::ARCHITECT, unit_labor::ARCHITECT, job_skill::DESIGNBUILDING, "Ar"},
+ {12, 4, profession::ALCHEMIST, unit_labor::ALCHEMIST, job_skill::ALCHEMY, "Al"},
+ {12, 4, profession::NONE, unit_labor::CLEAN, job_skill::NONE, "Cl"},
+// Military - Weapons
+ {13, 7, profession::WRESTLER, unit_labor::NONE, job_skill::WRESTLING, "Wr"},
+ {13, 7, profession::AXEMAN, unit_labor::NONE, job_skill::AXE, "Ax"},
+ {13, 7, profession::SWORDSMAN, unit_labor::NONE, job_skill::SWORD, "Sw"},
+ {13, 7, profession::MACEMAN, unit_labor::NONE, job_skill::MACE, "Mc"},
+ {13, 7, profession::HAMMERMAN, unit_labor::NONE, job_skill::HAMMER, "Ha"},
+ {13, 7, profession::SPEARMAN, unit_labor::NONE, job_skill::SPEAR, "Sp"},
+ {13, 7, profession::CROSSBOWMAN, unit_labor::NONE, job_skill::CROSSBOW, "Cb"},
+ {13, 7, profession::THIEF, unit_labor::NONE, job_skill::DAGGER, "Kn"},
+ {13, 7, profession::BOWMAN, unit_labor::NONE, job_skill::BOW, "Bo"},
+ {13, 7, profession::BLOWGUNMAN, unit_labor::NONE, job_skill::BLOWGUN, "Bl"},
+ {13, 7, profession::PIKEMAN, unit_labor::NONE, job_skill::PIKE, "Pk"},
+ {13, 7, profession::LASHER, unit_labor::NONE, job_skill::WHIP, "La"},
+// Military - Other Combat
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::BITE, "Bi"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::GRASP_STRIKE, "St"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::STANCE_STRIKE, "Ki"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::MISC_WEAPON, "Mi"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::MELEE_COMBAT, "Fg"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::RANGED_COMBAT, "Ac"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::ARMOR, "Ar"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::SHIELD, "Sh"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::DODGING, "Do"},
+// Military - Misc
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::LEADERSHIP, "Ld"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::TEACHING, "Te"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::KNOWLEDGE_ACQUISITION, "St"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::DISCIPLINE, "Di"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::CONCENTRATION, "Co"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::SITUATIONAL_AWARENESS, "Ob"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::COORDINATION, "Cr"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::BALANCE, "Ba"},
// Social
- {profession::STANDARD, unit_labor::NONE, job_skill::PERSUASION, "Pe"},
- {profession::STANDARD, unit_labor::NONE, job_skill::NEGOTIATION, "Ne"},
- {profession::STANDARD, unit_labor::NONE, job_skill::JUDGING_INTENT, "Ju"},
- {profession::STANDARD, unit_labor::NONE, job_skill::LYING, "Ly"},
- {profession::STANDARD, unit_labor::NONE, job_skill::INTIMIDATION, "In"},
- {profession::STANDARD, unit_labor::NONE, job_skill::CONVERSATION, "Cn"},
- {profession::STANDARD, unit_labor::NONE, job_skill::COMEDY, "Cm"},
- {profession::STANDARD, unit_labor::NONE, job_skill::FLATTERY, "Fl"},
- {profession::STANDARD, unit_labor::NONE, job_skill::CONSOLE, "Cs"},
- {profession::STANDARD, unit_labor::NONE, job_skill::PACIFY, "Pc"},
- {profession::ADMINISTRATOR, unit_labor::NONE, job_skill::APPRAISAL, "Ap"},
- {profession::ADMINISTRATOR, unit_labor::NONE, job_skill::ORGANIZATION, "Or"},
- {profession::ADMINISTRATOR, unit_labor::NONE, job_skill::RECORD_KEEPING, "RK"},
-
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::PERSUASION, "Pe"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::NEGOTIATION, "Ne"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::JUDGING_INTENT, "Ju"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::LYING, "Li"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::INTIMIDATION, "In"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::CONVERSATION, "Cn"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::COMEDY, "Cm"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::FLATTERY, "Fl"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::CONSOLE, "Cs"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::PACIFY, "Pc"},
+// Noble
+ {17, 5, profession::TRADER, unit_labor::NONE, job_skill::APPRAISAL, "Ap"},
+ {17, 5, profession::ADMINISTRATOR, unit_labor::NONE, job_skill::ORGANIZATION, "Or"},
+ {17, 5, profession::CLERK, unit_labor::NONE, job_skill::RECORD_KEEPING, "RK"},
// Miscellaneous
- {profession::STANDARD, unit_labor::NONE, job_skill::THROW, "Th"},
- {profession::STANDARD, unit_labor::NONE, job_skill::CRUTCH_WALK, "CW"},
- {profession::STANDARD, unit_labor::NONE, job_skill::SWIMMING, "Sw"},
- {profession::STANDARD, unit_labor::NONE, job_skill::KNAPPING, "Kn"},
-
- {profession::DRUNK, unit_labor::NONE, job_skill::WRITING, "Wr"},
- {profession::DRUNK, unit_labor::NONE, job_skill::PROSE, "Pr"},
- {profession::DRUNK, unit_labor::NONE, job_skill::POETRY, "Po"},
- {profession::DRUNK, unit_labor::NONE, job_skill::READING, "Rd"},
- {profession::DRUNK, unit_labor::NONE, job_skill::SPEAKING, "Sp"},
-
- {profession::ADMINISTRATOR, unit_labor::NONE, job_skill::MILITARY_TACTICS, "MT"},
- {profession::ADMINISTRATOR, unit_labor::NONE, job_skill::TRACKING, "Tr"},
- {profession::ADMINISTRATOR, unit_labor::NONE, job_skill::MAGIC_NATURE, "Dr"},
+ {18, 3, profession::NONE, unit_labor::NONE, job_skill::THROW, "Th"},
+ {18, 3, profession::NONE, unit_labor::NONE, job_skill::CRUTCH_WALK, "CW"},
+ {18, 3, profession::NONE, unit_labor::NONE, job_skill::SWIMMING, "Sw"},
+ {18, 3, profession::NONE, unit_labor::NONE, job_skill::KNAPPING, "Kn"},
+
+ {19, 6, profession::NONE, unit_labor::NONE, job_skill::WRITING, "Wr"},
+ {19, 6, profession::NONE, unit_labor::NONE, job_skill::PROSE, "Pr"},
+ {19, 6, profession::NONE, unit_labor::NONE, job_skill::POETRY, "Po"},
+ {19, 6, profession::NONE, unit_labor::NONE, job_skill::READING, "Rd"},
+ {19, 6, profession::NONE, unit_labor::NONE, job_skill::SPEAKING, "Sp"},
+
+ {20, 5, profession::NONE, unit_labor::NONE, job_skill::MILITARY_TACTICS, "MT"},
+ {20, 5, profession::NONE, unit_labor::NONE, job_skill::TRACKING, "Tr"},
+ {20, 5, profession::NONE, unit_labor::NONE, job_skill::MAGIC_NATURE, "Dr"},
};
struct UnitInfo
@@ -247,17 +249,69 @@ struct UnitInfo
int8_t color;
};
-#define FILTER_NONWORKERS 0x0001
-#define FILTER_NONDWARVES 0x0002
-#define FILTER_NONCIV 0x0004
-#define FILTER_ANIMALS 0x0008
-#define FILTER_LIVING 0x0010
-#define FILTER_DEAD 0x0020
+enum altsort_mode {
+ ALTSORT_NAME,
+ ALTSORT_PROFESSION,
+ ALTSORT_MAX
+};
+
+bool descending;
+df::job_skill sort_skill;
+df::unit_labor sort_labor;
+
+bool sortByName (const UnitInfo *d1, const UnitInfo *d2)
+{
+ if (descending)
+ return (d1->name > d2->name);
+ else
+ return (d1->name < d2->name);
+}
+
+bool sortByProfession (const UnitInfo *d1, const UnitInfo *d2)
+{
+ if (descending)
+ return (d1->profession > d2->profession);
+ else
+ return (d1->profession < d2->profession);
+}
+
+bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2)
+{
+ if (sort_skill != job_skill::NONE)
+ {
+ df::unit_skill *s1 = binsearch_in_vector>(d1->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill);
+ df::unit_skill *s2 = binsearch_in_vector>(d2->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill);
+ int l1 = s1 ? s1->rating : 0;
+ int l2 = s2 ? s2->rating : 0;
+ int e1 = s1 ? s1->experience : 0;
+ int e2 = s2 ? s2->experience : 0;
+ if (descending)
+ {
+ if (l1 != l2)
+ return l1 > l2;
+ if (e1 != e2)
+ return e1 > e2;
+ }
+ else
+ {
+ if (l1 != l2)
+ return l1 < l2;
+ if (e1 != e2)
+ return e1 < e2;
+ }
+ }
+ if (sort_labor != unit_labor::NONE)
+ {
+ if (descending)
+ return d1->unit->status.labors[sort_labor] > d2->unit->status.labors[sort_labor];
+ else
+ return d1->unit->status.labors[sort_labor] < d2->unit->status.labors[sort_labor];
+ }
+ return sortByName(d1, d2);
+}
class viewscreen_unitlaborsst : public dfhack_viewscreen {
public:
- static viewscreen_unitlaborsst *create (char pushtype, df::viewscreen *scr = NULL);
-
void feed(set *events);
void render();
@@ -267,86 +321,41 @@ public:
std::string getFocusString() { return "unitlabors"; }
- viewscreen_unitlaborsst();
+ viewscreen_unitlaborsst(vector &src);
~viewscreen_unitlaborsst() { };
protected:
vector units;
- int filter;
+ altsort_mode altsort;
int first_row, sel_row;
int first_column, sel_column;
int height, name_width, prof_width, labors_width;
-// bool descending;
-// int sort_skill;
-// int sort_labor;
-
- void readUnits ();
void calcSize ();
};
-viewscreen_unitlaborsst::viewscreen_unitlaborsst()
-{
- filter = FILTER_LIVING;
- first_row = sel_row = 0;
- first_column = sel_column = 0;
- calcSize();
- readUnits();
-}
-
-void viewscreen_unitlaborsst::readUnits ()
+viewscreen_unitlaborsst::viewscreen_unitlaborsst(vector &src)
{
- for (size_t i = 0; i < units.size(); i++)
- delete units[i];
- units.clear();
-
- UnitInfo *cur = new UnitInfo;
- for (size_t i = 0; i < world->units.active.size(); i++)
+ for (size_t i = 0; i < src.size(); i++)
{
- df::unit *unit = world->units.active[i];
+ UnitInfo *cur = new UnitInfo;
+ df::unit *unit = src[i];
cur->unit = unit;
cur->allowEdit = true;
if (unit->race != ui->race_id)
- {
cur->allowEdit = false;
- if (!(filter & FILTER_NONDWARVES))
- continue;
- }
if (unit->civ_id != ui->civ_id)
- {
cur->allowEdit = false;
- if (!(filter & FILTER_NONCIV))
- continue;
- }
if (unit->flags1.bits.dead)
- {
cur->allowEdit = false;
- if (!(filter & FILTER_DEAD))
- continue;
- }
- else
- {
- if (!(filter & FILTER_LIVING))
- continue;
- }
if (!ENUM_ATTR(profession, can_assign_labor, unit->profession))
- {
cur->allowEdit = false;
- if (!(filter & FILTER_NONWORKERS))
- continue;
- }
-
- if (!unit->name.first_name.length())
- {
- if (!(filter & FILTER_ANIMALS))
- continue;
- }
cur->name = Translation::TranslateName(&unit->name, false);
cur->transname = Translation::TranslateName(&unit->name, true);
@@ -354,9 +363,13 @@ void viewscreen_unitlaborsst::readUnits ()
cur->color = Units::getProfessionColor(unit);
units.push_back(cur);
- cur = new UnitInfo;
}
- delete cur;
+ std::sort(units.begin(), units.end(), sortByName);
+
+ altsort = ALTSORT_NAME;
+ first_row = sel_row = 0;
+ first_column = sel_column = 0;
+ calcSize();
}
void viewscreen_unitlaborsst::calcSize()
@@ -421,8 +434,6 @@ void viewscreen_unitlaborsst::feed(set *events)
return;
}
- // TODO - allow modifying filters
-
if (!units.size())
return;
@@ -458,16 +469,16 @@ void viewscreen_unitlaborsst::feed(set *events)
{
// go to beginning of current column group; if already at the beginning, go to the beginning of the previous one
sel_column--;
- df::profession cur = columns[sel_column].profession;
- while ((sel_column > 0) && columns[sel_column - 1].profession == cur)
+ int cur = columns[sel_column].group;
+ while ((sel_column > 0) && columns[sel_column - 1].group == cur)
sel_column--;
}
if ((sel_column != NUM_COLUMNS - 1) && events->count(interface_key::CURSOR_DOWN_Z))
{
// go to end of current column group; if already at the end, go to the end of the next one
sel_column++;
- df::profession cur = columns[sel_column].profession;
- while ((sel_column < NUM_COLUMNS - 1) && columns[sel_column + 1].profession == cur)
+ int cur = columns[sel_column].group;
+ while ((sel_column < NUM_COLUMNS - 1) && columns[sel_column + 1].group == cur)
sel_column++;
}
@@ -486,9 +497,10 @@ void viewscreen_unitlaborsst::feed(set *events)
{
df::unit *unit = cur->unit;
const SkillColumn &col = columns[sel_column];
+ bool newstatus = !unit->status.labors[col.labor];
if (col.special)
{
- if (!unit->status.labors[col.labor])
+ if (newstatus)
{
for (int i = 0; i < NUM_COLUMNS; i++)
{
@@ -498,12 +510,91 @@ void viewscreen_unitlaborsst::feed(set *events)
}
unit->military.pickup_flags.bits.update = true;
}
- unit->status.labors[col.labor] = !unit->status.labors[col.labor];
+ unit->status.labors[col.labor] = newstatus;
+ }
+ if (events->count(interface_key::SELECT_ALL) && (cur->allowEdit))
+ {
+ df::unit *unit = cur->unit;
+ const SkillColumn &col = columns[sel_column];
+ bool newstatus = !unit->status.labors[col.labor];
+ for (int i = 0; i < NUM_COLUMNS; i++)
+ {
+ if (columns[i].group != col.group)
+ continue;
+ if (columns[i].special)
+ {
+ if (newstatus)
+ {
+ for (int j = 0; j < NUM_COLUMNS; j++)
+ {
+ if ((columns[j].labor != unit_labor::NONE) && columns[j].special)
+ unit->status.labors[columns[j].labor] = false;
+ }
+ }
+ unit->military.pickup_flags.bits.update = true;
+ }
+ unit->status.labors[columns[i].labor] = newstatus;
+ }
}
- // TODO: add sorting
+ if (events->count(interface_key::SECONDSCROLL_UP) || events->count(interface_key::SECONDSCROLL_DOWN))
+ {
+ descending = events->count(interface_key::SECONDSCROLL_UP);
+ sort_skill = columns[sel_column].skill;
+ sort_labor = columns[sel_column].labor;
+ std::sort(units.begin(), units.end(), sortBySkill);
+ }
+
+ if (events->count(interface_key::SECONDSCROLL_PAGEUP) || events->count(interface_key::SECONDSCROLL_PAGEDOWN))
+ {
+ descending = events->count(interface_key::SECONDSCROLL_PAGEUP);
+ switch (altsort)
+ {
+ case ALTSORT_NAME:
+ std::sort(units.begin(), units.end(), sortByName);
+ break;
+ case ALTSORT_PROFESSION:
+ std::sort(units.begin(), units.end(), sortByProfession);
+ break;
+ }
+ }
+ if (events->count(interface_key::CHANGETAB))
+ {
+ switch (altsort)
+ {
+ case ALTSORT_NAME:
+ altsort = ALTSORT_PROFESSION;
+ break;
+ case ALTSORT_PROFESSION:
+ altsort = ALTSORT_NAME;
+ break;
+ }
+ }
+
+ if (VIRTUAL_CAST_VAR(unitlist, df::viewscreen_unitlistst, parent))
+ {
+ if (events->count(interface_key::UNITJOB_VIEW) || events->count(interface_key::UNITJOB_ZOOM_CRE))
+ {
+ for (int i = 0; i < unitlist->units[unitlist->page].size(); i++)
+ {
+ if (unitlist->units[unitlist->page][i] == units[sel_row]->unit)
+ {
+ unitlist->cursor_pos[unitlist->page] = i;
+ unitlist->feed(events);
+ if (Screen::isDismissed(unitlist))
+ Screen::dismiss(this);
+ break;
+ }
+ }
+ }
+ }
}
+void OutputString(int8_t color, int &x, int y, const std::string &text)
+{
+ Screen::paintString(Screen::Pen(' ', color, 0), x, y, text);
+ x += text.length();
+}
void viewscreen_unitlaborsst::render()
{
if (Screen::isDismissed(this))
@@ -520,7 +611,7 @@ void viewscreen_unitlaborsst::render()
if (col_offset >= NUM_COLUMNS)
break;
- int8_t fg = Units::getCasteProfessionColor(ui->race_id, -1, columns[col_offset].profession);
+ int8_t fg = columns[col_offset].color;
int8_t bg = 0;
if (col_offset == sel_column)
@@ -528,8 +619,19 @@ void viewscreen_unitlaborsst::render()
fg = 0;
bg = 7;
}
+
Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 1);
Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 2);
+ df::profession profession = columns[col_offset].profession;
+ if (profession != profession::NONE)
+ {
+ auto graphics = world->raws.creatures.all[ui->race_id]->graphics;
+ Screen::paintTile(
+ Screen::Pen(' ', fg, 0,
+ graphics.profession_add_color[creature_graphics_role::DEFAULT][profession],
+ graphics.profession_texpos[creature_graphics_role::DEFAULT][profession]),
+ 1 + name_width + 1 + prof_width + 1 + col, 3);
+ }
}
for (int row = 0; row < height; row++)
@@ -537,6 +639,7 @@ void viewscreen_unitlaborsst::render()
int row_offset = row + first_row;
if (row_offset >= units.size())
break;
+
UnitInfo *cur = units[row_offset];
df::unit *unit = cur->unit;
int8_t fg = 15, bg = 0;
@@ -548,13 +651,14 @@ void viewscreen_unitlaborsst::render()
string name = cur->name;
name.resize(name_width);
- Screen::paintString(Screen::Pen(' ', fg, bg), 1, 3 + row, name);
+ Screen::paintString(Screen::Pen(' ', fg, bg), 1, 4 + row, name);
string profession = cur->profession;
profession.resize(prof_width);
fg = cur->color;
bg = 0;
- Screen::paintString(Screen::Pen(' ', fg, bg), 1 + prof_width + 1, 3 + row, profession);
+
+ Screen::paintString(Screen::Pen(' ', fg, bg), 1 + name_width + 1, 4 + row, profession);
// Print unit's skills and labor assignments
for (int col = 0; col < labors_width; col++)
@@ -562,11 +666,9 @@ void viewscreen_unitlaborsst::render()
int col_offset = col + first_column;
fg = 15;
bg = 0;
+ uint8_t c = 0xFA;
if ((col_offset == sel_column) && (row_offset == sel_row))
fg = 9;
- if ((columns[col_offset].labor != unit_labor::NONE) && (unit->status.labors[columns[col_offset].labor]))
- bg = 7;
- char c = '-';
if (columns[col_offset].skill != job_skill::NONE)
{
df::unit_skill *skill = binsearch_in_vector>(unit->status.current_soul->skills, &df::unit_skill::id, columns[col_offset].skill);
@@ -577,24 +679,44 @@ void viewscreen_unitlaborsst::render()
level = NUM_SKILL_LEVELS - 1;
c = skill_levels[level].abbrev;
}
+ else
+ c = '-';
+ }
+ if (columns[col_offset].labor != unit_labor::NONE)
+ {
+ if (unit->status.labors[columns[col_offset].labor])
+ {
+ bg = 7;
+ if (columns[col_offset].skill == job_skill::NONE)
+ c = 0xF9;
+ }
}
- Screen::paintTile(Screen::Pen(c, fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 3 + row);
+ else
+ bg = 4;
+ Screen::paintTile(Screen::Pen(c, fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 4 + row);
}
}
UnitInfo *cur = units[sel_row];
+ bool canToggle = false;
if (cur != NULL)
{
df::unit *unit = cur->unit;
- string str = cur->transname;
- if (str.length())
- str += ", ";
- str += cur->profession;
- str += ":";
+ int x = 1;
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->transname);
+ x += cur->transname.length();
- Screen::paintString(Screen::Pen(' ', 15, 0), 1, 3 + height + 2, str);
- int y = 1 + str.length() + 1;
+ if (cur->transname.length())
+ {
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ", ");
+ x += 2;
+ }
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->profession);
+ x += cur->profession.length();
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ": ");
+ x += 2;
+ string str;
if (columns[sel_column].skill == job_skill::NONE)
{
str = ENUM_ATTR_STR(unit_labor, caption, columns[sel_column].labor);
@@ -602,7 +724,6 @@ void viewscreen_unitlaborsst::render()
str += " Enabled";
else
str += " Not Enabled";
- Screen::paintString(Screen::Pen(' ', 9, 0), y, 3 + height + 2, str);
}
else
{
@@ -618,11 +739,50 @@ void viewscreen_unitlaborsst::render()
}
else
str = stl_sprintf("Not %s (0/500)", ENUM_ATTR_STR(job_skill, caption_noun, columns[sel_column].skill));
- Screen::paintString(Screen::Pen(' ', 9, 0), y, 3 + height + 2, str);
}
+ Screen::paintString(Screen::Pen(' ', 9, 0), x, 3 + height + 2, str);
+
+ canToggle = (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE);
}
- // TODO - print command help info
+ int x = 2;
+ OutputString(10, x, gps->dimy - 3, "Enter"); // SELECT key
+ OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle labor, ");
+
+ OutputString(10, x, gps->dimy - 3, "Shift+Enter"); // SELECT_ALL key
+ OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle Group, ");
+
+ OutputString(10, x, gps->dimy - 3, "v"); // UNITJOB_VIEW key
+ OutputString(15, x, gps->dimy - 3, ": ViewCre, ");
+
+ OutputString(10, x, gps->dimy - 3, "c"); // UNITJOB_ZOOM_CRE key
+ OutputString(15, x, gps->dimy - 3, ": Zoom-Cre");
+
+ x = 2;
+ OutputString(10, x, gps->dimy - 2, "Esc"); // LEAVESCREEN key
+ OutputString(15, x, gps->dimy - 2, ": Done, ");
+
+ OutputString(10, x, gps->dimy - 2, "+"); // SECONDSCROLL_DOWN key
+ OutputString(10, x, gps->dimy - 2, "-"); // SECONDSCROLL_UP key
+ OutputString(15, x, gps->dimy - 2, ": Sort by Skill, ");
+
+ OutputString(10, x, gps->dimy - 2, "*"); // SECONDSCROLL_PAGEDOWN key
+ OutputString(10, x, gps->dimy - 2, "/"); // SECONDSCROLL_PAGEUP key
+ OutputString(15, x, gps->dimy - 2, ": Sort by (");
+ OutputString(10, x, gps->dimy - 2, "Tab"); // CHANGETAB key
+ OutputString(15, x, gps->dimy - 2, ") ");
+ switch (altsort)
+ {
+ case ALTSORT_NAME:
+ OutputString(15, x, gps->dimy - 2, "Name");
+ break;
+ case ALTSORT_PROFESSION:
+ OutputString(15, x, gps->dimy - 2, "Profession");
+ break;
+ default:
+ OutputString(15, x, gps->dimy - 2, "Unknown");
+ break;
+ }
}
struct unitlist_hook : df::viewscreen_unitlistst
@@ -633,29 +793,43 @@ struct unitlist_hook : df::viewscreen_unitlistst
{
if (input->count(interface_key::UNITVIEW_PRF_PROF))
{
- Screen::dismiss(this);
- Screen::show(new viewscreen_unitlaborsst());
- return;
+ if (units[page].size())
+ {
+ Screen::show(new viewscreen_unitlaborsst(units[page]));
+ return;
+ }
}
INTERPOSE_NEXT(feed)(input);
}
+
+ DEFINE_VMETHOD_INTERPOSE(void, render, ())
+ {
+ INTERPOSE_NEXT(render)();
+
+ if (units[page].size())
+ {
+ int x = 2;
+ OutputString(12, x, gps->dimy - 2, "l"); // UNITVIEW_PRF_PROF key
+ OutputString(15, x, gps->dimy - 2, ": Manage labors");
+ }
+ }
};
IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, feed);
+IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, render);
+
+DFHACK_PLUGIN("manipulator");
DFhackCExport command_result plugin_init ( color_ostream &out, vector &commands)
{
- if (gps)
- {
- if (!INTERPOSE_HOOK(unitlist_hook, feed).apply())
- out.printerr("Could not interpose viewscreen_unitlistst::feed\n");
- }
-
+ if (!gps || !INTERPOSE_HOOK(unitlist_hook, feed).apply() || !INTERPOSE_HOOK(unitlist_hook, render).apply())
+ out.printerr("Could not insert Dwarf Manipulator hooks!\n");
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
INTERPOSE_HOOK(unitlist_hook, feed).remove();
+ INTERPOSE_HOOK(unitlist_hook, render).remove();
return CR_OK;
}
diff --git a/plugins/power-meter.cpp b/plugins/power-meter.cpp
new file mode 100644
index 000000000..0466b68e4
--- /dev/null
+++ b/plugins/power-meter.cpp
@@ -0,0 +1,237 @@
+#include "Core.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include "df/graphic.h"
+#include "df/building_trapst.h"
+#include "df/builtin_mats.h"
+#include "df/world.h"
+#include "df/buildings_other_id.h"
+#include "df/machine.h"
+#include "df/machine_info.h"
+#include "df/building_drawbuffer.h"
+#include "df/ui.h"
+#include "df/viewscreen_dwarfmodest.h"
+#include "df/ui_build_selector.h"
+#include "df/flow_info.h"
+#include "df/report.h"
+
+#include "MiscUtils.h"
+
+using std::vector;
+using std::string;
+using std::stack;
+using namespace DFHack;
+using namespace df::enums;
+
+using df::global::gps;
+using df::global::world;
+using df::global::ui;
+using df::global::ui_build_selector;
+
+DFHACK_PLUGIN("power-meter");
+
+static const uint32_t METER_BIT = 0x80000000U;
+
+static void init_plate_info(df::pressure_plate_info &plate_info)
+{
+ plate_info.water_min = 1;
+ plate_info.water_max = 7;
+ plate_info.flags.whole = METER_BIT;
+ plate_info.flags.bits.water = true;
+ plate_info.flags.bits.resets = true;
+}
+
+/*
+ * Hook for the pressure plate itself. Implements core logic.
+ */
+
+struct trap_hook : df::building_trapst {
+ typedef df::building_trapst interpose_base;
+
+ // Engine detection
+
+ bool is_power_meter()
+ {
+ return trap_type == trap_type::PressurePlate &&
+ (plate_info.flags.whole & METER_BIT) != 0;
+ }
+
+ inline bool is_fully_built()
+ {
+ return getBuildStage() >= getMaxBuildStage();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, getName, (std::string *buf))
+ {
+ if (is_power_meter())
+ {
+ buf->clear();
+ *buf += "Power Meter";
+ return;
+ }
+
+ INTERPOSE_NEXT(getName)(buf);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, updateAction, ())
+ {
+ if (is_power_meter())
+ {
+ auto pdsgn = Maps::getTileDesignation(centerx,centery,z);
+
+ if (pdsgn)
+ {
+ bool active = false;
+ auto &gears = world->buildings.other[buildings_other_id::GEAR_ASSEMBLY];
+
+ for (size_t i = 0; i < gears.size(); i++)
+ {
+ // Adjacent
+ auto gear = gears[i];
+ int deltaxy = abs(centerx - gear->centerx) + abs(centery - gear->centery);
+ if (gear->z != z || deltaxy != 1)
+ continue;
+ // Linked to machine
+ auto info = gears[i]->getMachineInfo();
+ if (!info || info->machine_id < 0)
+ continue;
+ // an active machine
+ auto machine = df::machine::find(info->machine_id);
+ if (!machine || !machine->flags.bits.active)
+ continue;
+ // with adequate power?
+ int power = machine->cur_power - machine->min_power;
+ if (power < 0 || machine->cur_power <= 0)
+ continue;
+ if (power < plate_info.track_min)
+ continue;
+ if (power > plate_info.track_max && plate_info.track_max >= 0)
+ continue;
+
+ active = true;
+ break;
+ }
+
+ if (plate_info.flags.bits.citizens)
+ active = !active;
+
+ // Temporarily set the tile water amount based on power state
+ auto old_dsgn = *pdsgn;
+ pdsgn->bits.liquid_type = tile_liquid::Water;
+ pdsgn->bits.flow_size = (active ? 7 : 0);
+
+ INTERPOSE_NEXT(updateAction)();
+
+ *pdsgn = old_dsgn;
+ return;
+ }
+ }
+
+ INTERPOSE_NEXT(updateAction)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (df::building_drawbuffer *db, void *unk))
+ {
+ INTERPOSE_NEXT(drawBuilding)(db, unk);
+
+ if (is_power_meter() && is_fully_built())
+ {
+ db->fore[0][0] = 3;
+ }
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(trap_hook, getName);
+IMPLEMENT_VMETHOD_INTERPOSE(trap_hook, updateAction);
+IMPLEMENT_VMETHOD_INTERPOSE(trap_hook, drawBuilding);
+
+static bool enabled = false;
+
+static void enable_hooks(bool enable)
+{
+ enabled = enable;
+
+ INTERPOSE_HOOK(trap_hook, getName).apply(enable);
+ INTERPOSE_HOOK(trap_hook, updateAction).apply(enable);
+ INTERPOSE_HOOK(trap_hook, drawBuilding).apply(enable);
+}
+
+static bool makePowerMeter(df::pressure_plate_info *info, int min_power, int max_power, bool invert)
+{
+ CHECK_NULL_POINTER(info);
+
+ if (!enabled)
+ {
+ auto pworld = Core::getInstance().getWorld();
+ auto entry = pworld->GetPersistentData("power-meter/enabled", NULL);
+ if (!entry.isValid())
+ return false;
+
+ enable_hooks(true);
+ }
+
+ init_plate_info(*info);
+ info->track_min = min_power;
+ info->track_max = max_power;
+ info->flags.bits.citizens = invert;
+ return true;
+}
+
+DFHACK_PLUGIN_LUA_FUNCTIONS {
+ DFHACK_LUA_FUNCTION(makePowerMeter),
+ DFHACK_LUA_END
+};
+
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
+{
+ switch (event) {
+ case SC_MAP_LOADED:
+ {
+ auto pworld = Core::getInstance().getWorld();
+ bool enable = pworld->GetPersistentData("power-meter/enabled").isValid();
+
+ if (enable)
+ {
+ out.print("Enabling the power meter plugin.\n");
+ enable_hooks(true);
+ }
+ }
+ break;
+ case SC_MAP_UNLOADED:
+ enable_hooks(false);
+ break;
+ default:
+ break;
+ }
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands)
+{
+ if (Core::getInstance().isMapLoaded())
+ plugin_onstatechange(out, SC_MAP_LOADED);
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown ( color_ostream &out )
+{
+ enable_hooks(false);
+ return CR_OK;
+}
diff --git a/plugins/probe.cpp b/plugins/probe.cpp
index 2ae6846d5..45ef1bbfb 100644
--- a/plugins/probe.cpp
+++ b/plugins/probe.cpp
@@ -27,6 +27,7 @@ using namespace std;
#include "df/world.h"
#include "df/world_raws.h"
#include "df/building_def.h"
+#include "df/region_map_entry.h"
using std::vector;
using std::string;
@@ -224,8 +225,7 @@ command_result df_probe (color_ostream &out, vector & parameters)
int bx = clip_range(block.region_pos.x + (offset % 3) - 1, 0, world->world_data->world_width-1);
int by = clip_range(block.region_pos.y + (offset / 3) - 1, 0, world->world_data->world_height-1);
- df::world_data::T_region_map* biome =
- &world->world_data->region_map[bx][by];
+ auto biome = &world->world_data->region_map[bx][by];
int sav = biome->savagery;
int evi = biome->evilness;
diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp
index 91abd544e..5eab897c0 100644
--- a/plugins/prospector.cpp
+++ b/plugins/prospector.cpp
@@ -25,8 +25,12 @@ using namespace std;
#include "df/world.h"
#include "df/world_data.h"
#include "df/world_region_details.h"
+#include "df/world_region_feature.h"
#include "df/world_geo_biome.h"
#include "df/world_geo_layer.h"
+#include "df/world_underground_region.h"
+#include "df/feature_init.h"
+#include "df/region_map_entry.h"
#include "df/inclusion_type.h"
#include "df/viewscreen_choose_start_sitest.h"
@@ -108,9 +112,10 @@ struct compare_pair_second
}
};
-static void printMatdata(color_ostream &con, const matdata &data)
+static void printMatdata(color_ostream &con, const matdata &data, bool only_z = false)
{
- con << std::setw(9) << data.count;
+ if (!only_z)
+ con << std::setw(9) << data.count;
if(data.lower_z != data.upper_z)
con <<" Z:" << std::setw(4) << data.lower_z << ".." << data.upper_z << std::endl;
@@ -225,116 +230,247 @@ static coord2d biome_delta[] = {
coord2d(-1,-1), coord2d(0,-1), coord2d(1,-1)
};
-static command_result embark_prospector(color_ostream &out, df::viewscreen_choose_start_sitest *screen,
- bool showHidden, bool showValue)
+struct EmbarkTileLayout {
+ coord2d biome_off, biome_pos;
+ df::region_map_entry *biome;
+ int elevation, max_soil_depth;
+ int min_z, base_z;
+ std::map penalty;
+};
+
+bool estimate_underground(color_ostream &out, EmbarkTileLayout &tile, df::world_region_details *details, int x, int y)
{
- if (!world || !world->world_data)
+ // Find actual biome
+ int bv = clip_range(details->biome[x][y] & 15, 1, 9);
+ tile.biome_off = biome_delta[bv-1];
+
+ df::world_data *data = world->world_data;
+ int bx = clip_range(details->pos.x + tile.biome_off.x, 0, data->world_width-1);
+ int by = clip_range(details->pos.y + tile.biome_off.y, 0, data->world_height-1);
+ tile.biome_pos = coord2d(bx, by);
+ tile.biome = &data->region_map[bx][by];
+
+ // Compute surface elevation
+ tile.elevation = (
+ details->elevation[x][y] + details->elevation[x][y+1] +
+ details->elevation[x+1][y] + details->elevation[x+1][y+1]
+ ) / 4;
+ tile.max_soil_depth = std::max((154-tile.biome->elevation)/5,0);
+ tile.base_z = tile.elevation;
+ tile.penalty.clear();
+
+ auto &features = details->features[x][y];
+
+ // Collect global feature layer depths and apply penalties
+ std::map layer_bottom, layer_top;
+ bool sea_found = false;
+
+ for (size_t i = 0; i < features.size(); i++)
{
- out.printerr("World data is not available.\n");
- return CR_FAILURE;
+ auto feature = features[i];
+ auto layer = df::world_underground_region::find(feature->layer);
+ if (!layer || feature->min_z == -30000) continue;
+
+ layer_bottom[layer->layer_depth] = feature->min_z;
+ layer_top[layer->layer_depth] = feature->max_z;
+ tile.base_z = std::min(tile.base_z, (int)feature->min_z);
+
+ float penalty = 1.0f;
+ switch (layer->type) {
+ case df::world_underground_region::Cavern:
+ penalty = 0.75f;
+ break;
+ case df::world_underground_region::MagmaSea:
+ sea_found = true;
+ tile.min_z = feature->min_z;
+ for (int i = feature->min_z; i <= feature->max_z; i++)
+ tile.penalty[i] = 0.2 + 0.6f*(i-feature->min_z)/(feature->max_z-feature->min_z+1);
+ break;
+ case df::world_underground_region::Underworld:
+ penalty = 0.0f;
+ break;
+ }
+
+ if (penalty != 1.0f)
+ {
+ for (int i = feature->min_z; i <= feature->max_z; i++)
+ tile.penalty[i] = penalty;
+ }
}
- df::world_data *data = world->world_data;
- coord2d cur_region = screen->region_pos;
- int d_idx = linear_index(data->region_details, &df::world_region_details::pos, cur_region);
- auto cur_details = vector_get(data->region_details, d_idx);
+ if (!sea_found)
+ {
+ out.printerr("Could not find magma sea.\n");
+ return false;
+ }
- if (!cur_details)
+ // Scan for big local features and apply their penalties
+ for (size_t i = 0; i < features.size(); i++)
{
- out.printerr("Current region details are not available.\n");
- return CR_FAILURE;
+ auto feature = features[i];
+ auto lfeature = Maps::getLocalInitFeature(details->pos, feature->feature_idx);
+ if (!lfeature)
+ continue;
+
+ switch (lfeature->getType())
+ {
+ case feature_type::pit:
+ case feature_type::magma_pool:
+ case feature_type::volcano:
+ for (int i = layer_bottom[lfeature->end_depth];
+ i <= layer_top[lfeature->start_depth]; i++)
+ tile.penalty[i] = std::min(0.4f, map_find(tile.penalty, i, 1.0f));
+ break;
+ default:
+ break;
+ }
}
- // Compute biomes
- std::map biomes;
+ return true;
+}
+
+void add_materials(EmbarkTileLayout &tile, matdata &data, float amount, int min_z, int max_z)
+{
+ for (int z = min_z; z <= max_z; z++)
+ data.add(z, int(map_find(tile.penalty, z, 1)*amount));
+}
+
+bool estimate_materials(color_ostream &out, EmbarkTileLayout &tile, MatMap &layerMats, MatMap &veinMats)
+{
+ using namespace geo_layer_type;
+
+ df::world_geo_biome *geo_biome = df::world_geo_biome::find(tile.biome->geo_index);
- if (screen->biome_highlighted)
+ if (!geo_biome)
{
- out.print("Processing one embark tile of biome F%d.\n\n", screen->biome_idx+1);
- biomes[screen->biome_rgn[screen->biome_idx]]++;
+ out.printerr("Region geo-biome not found: (%d,%d)\n",
+ tile.biome_pos.x, tile.biome_pos.y);
+ return false;
}
- else
+
+ // soil depth increases by 1 every 5 levels below 150
+ int top_z_level = tile.elevation - tile.max_soil_depth;
+
+ for (unsigned i = 0; i < geo_biome->layers.size(); i++)
{
- for (int x = screen->embark_pos_min.x; x <= screen->embark_pos_max.x; x++)
+ auto layer = geo_biome->layers[i];
+ switch (layer->type)
{
- for (int y = screen->embark_pos_min.y; y <= screen->embark_pos_max.y; y++)
- {
- int bv = clip_range(cur_details->biome[x][y], 1, 9);
- biomes[cur_region + biome_delta[bv-1]]++;
- }
+ case SOIL:
+ case SOIL_OCEAN:
+ case SOIL_SAND:
+ top_z_level += layer->top_height - layer->bottom_height + 1;
+ break;
+ default:;
}
}
- // Compute material maps
- MatMap layerMats;
- MatMap veinMats;
+ top_z_level = std::max(top_z_level, tile.elevation)-1;
- for (auto biome_it = biomes.begin(); biome_it != biomes.end(); ++biome_it)
+ for (unsigned i = 0; i < geo_biome->layers.size(); i++)
{
- int bx = clip_range(biome_it->first.x, 0, data->world_width-1);
- int by = clip_range(biome_it->first.y, 0, data->world_height-1);
- auto ®ion = data->region_map[bx][by];
- df::world_geo_biome *geo_biome = df::world_geo_biome::find(region.geo_index);
+ auto layer = geo_biome->layers[i];
- if (!geo_biome)
- {
- out.printerr("Region geo-biome not found: (%d,%d)\n", bx, by);
- return CR_FAILURE;
- }
+ int top_z = std::min(layer->top_height + top_z_level, tile.elevation-1);
+ int bottom_z = std::max(layer->bottom_height + top_z_level, tile.min_z);
+ if (i+1 == geo_biome->layers.size()) // stretch layer if needed
+ bottom_z = tile.min_z;
+ if (top_z < bottom_z)
+ continue;
+
+ float layer_size = 48*48;
+
+ int sums[ENUM_LAST_ITEM(inclusion_type)+1] = { 0 };
- int cnt = biome_it->second;
+ for (unsigned j = 0; j < layer->vein_mat.size(); j++)
+ if (is_valid_enum_item(layer->vein_type[j]))
+ sums[layer->vein_type[j]] += layer->vein_unk_38[j];
- for (unsigned i = 0; i < geo_biome->layers.size(); i++)
+ for (unsigned j = 0; j < layer->vein_mat.size(); j++)
{
- auto layer = geo_biome->layers[i];
+ // TODO: find out how to estimate the real density
+ // this code assumes that vein_unk_38 is the weight
+ // used when choosing the vein material
+ float size = float(layer->vein_unk_38[j]);
+ df::inclusion_type type = layer->vein_type[j];
- layerMats[layer->mat_index].add(layer->bottom_height, 0);
+ switch (type)
+ {
+ case inclusion_type::VEIN:
+ // 3 veins of 80 tiles avg
+ size = size * 80 * 3 / sums[type];
+ break;
+ case inclusion_type::CLUSTER:
+ // 1 cluster of 700 tiles avg
+ size = size * 700 * 1 / sums[type];
+ break;
+ case inclusion_type::CLUSTER_SMALL:
+ size = size * 6 * 7 / sums[type];
+ break;
+ case inclusion_type::CLUSTER_ONE:
+ size = size * 1 * 5 / sums[type];
+ break;
+ default:
+ // shouldn't actually happen
+ size = 1;
+ }
- int level_cnt = layer->top_height - layer->bottom_height + 1;
- int layer_size = 48*48*cnt*level_cnt;
+ layer_size -= size;
- int sums[(int)ENUM_LAST_ITEM(inclusion_type)+1] = { 0 };
+ add_materials(tile, veinMats[layer->vein_mat[j]], size, bottom_z, top_z);
+ }
- for (unsigned j = 0; j < layer->vein_mat.size(); j++)
- if (is_valid_enum_item(layer->vein_type[j]))
- sums[layer->vein_type[j]] += layer->vein_unk_38[j];
+ add_materials(tile, layerMats[layer->mat_index], layer_size, bottom_z, top_z);
+ }
- for (unsigned j = 0; j < layer->vein_mat.size(); j++)
- {
- // TODO: find out how to estimate the real density
- // this code assumes that vein_unk_38 is the weight
- // used when choosing the vein material
- int size = layer->vein_unk_38[j]*cnt*level_cnt;
- df::inclusion_type type = layer->vein_type[j];
+ return true;
+}
- switch (type)
- {
- case inclusion_type::VEIN:
- // 3 veins of 80 tiles avg
- size = size * 80 * 3 / sums[type];
- break;
- case inclusion_type::CLUSTER:
- // 1 cluster of 700 tiles avg
- size = size * 700 * 1 / sums[type];
- break;
- case inclusion_type::CLUSTER_SMALL:
- size = size * 6 * 7 / sums[type];
- break;
- case inclusion_type::CLUSTER_ONE:
- size = size * 1 * 5 / sums[type];
- break;
- default:
- // shouldn't actually happen
- size = cnt*level_cnt;
- }
+static command_result embark_prospector(color_ostream &out, df::viewscreen_choose_start_sitest *screen,
+ bool showHidden, bool showValue)
+{
+ if (!world || !world->world_data)
+ {
+ out.printerr("World data is not available.\n");
+ return CR_FAILURE;
+ }
+
+ df::world_data *data = world->world_data;
+ coord2d cur_region = screen->region_pos;
+ int d_idx = linear_index(data->region_details, &df::world_region_details::pos, cur_region);
+ auto cur_details = vector_get(data->region_details, d_idx);
- veinMats[layer->vein_mat[j]].add(layer->bottom_height, 0);
- veinMats[layer->vein_mat[j]].add(layer->top_height, size);
+ if (!cur_details)
+ {
+ out.printerr("Current region details are not available.\n");
+ return CR_FAILURE;
+ }
- layer_size -= size;
- }
+ // Compute material maps
+ MatMap layerMats;
+ MatMap veinMats;
+ matdata world_bottom;
- layerMats[layer->mat_index].add(layer->top_height, std::max(0,layer_size));
+ // Compute biomes
+ std::map biomes;
+
+ /*if (screen->biome_highlighted)
+ {
+ out.print("Processing one embark tile of biome F%d.\n\n", screen->biome_idx+1);
+ biomes[screen->biome_rgn[screen->biome_idx]]++;
+ }*/
+
+ for (int x = screen->embark_pos_min.x; x <= screen->embark_pos_max.x; x++)
+ {
+ for (int y = screen->embark_pos_min.y; y <= screen->embark_pos_max.y; y++)
+ {
+ EmbarkTileLayout tile;
+ if (!estimate_underground(out, tile, cur_details, x, y) ||
+ !estimate_materials(out, tile, layerMats, veinMats))
+ return CR_FAILURE;
+
+ world_bottom.add(tile.base_z, 0);
+ world_bottom.add(tile.elevation-1, 0);
}
}
@@ -348,7 +484,10 @@ static command_result embark_prospector(color_ostream &out, df::viewscreen_choos
mats->Finish();
}
- out << "Warning: the above data is only a very rough estimate." << std::endl;
+ out << "Embark depth: " << (world_bottom.upper_z-world_bottom.lower_z+1) << " ";
+ printMatdata(out, world_bottom, true);
+
+ out << std::endl << "Warning: the above data is only a very rough estimate." << std::endl;
return CR_OK;
}
@@ -536,6 +675,8 @@ command_result prospector (color_ostream &con, vector & parameters)
case tiletype_material::LAVA_STONE:
// TODO ?
break;
+ default:
+ break;
}
}
}
diff --git a/plugins/proto/rename.proto b/plugins/proto/rename.proto
index aa1e95f48..810091fc7 100644
--- a/plugins/proto/rename.proto
+++ b/plugins/proto/rename.proto
@@ -17,3 +17,10 @@ message RenameUnitIn {
optional string nickname = 2;
optional string profession = 3;
}
+
+// RPC RenameBuilding : RenameBuildingIn -> EmptyMessage
+message RenameBuildingIn {
+ required int32 building_id = 1;
+
+ optional string name = 2;
+}
diff --git a/plugins/raw/building_steam_engine.txt b/plugins/raw/building_steam_engine.txt
new file mode 100644
index 000000000..48657b0c1
--- /dev/null
+++ b/plugins/raw/building_steam_engine.txt
@@ -0,0 +1,92 @@
+building_steam_engine
+
+[OBJECT:BUILDING]
+
+[BUILDING_WORKSHOP:STEAM_ENGINE]
+ [NAME:Steam Engine]
+ [NAME_COLOR:4:0:1]
+ [DIM:3:3]
+ [WORK_LOCATION:2:3]
+ [BUILD_LABOR:MECHANIC]
+ [BUILD_KEY:CUSTOM_ALT_S]
+ [BLOCK:1:1:1:1]
+ [BLOCK:2:1:1:1]
+ [BLOCK:3:1:0:1]
+ [TILE:0:1:240:' ':254]
+ [TILE:0:2:' ':' ':128]
+ [TILE:0:3:246:' ':' ']
+ [COLOR:0:1:6:0:0:0:0:0:7:0:0]
+ [COLOR:0:2:0:0:0:0:0:0:7:0:0]
+ [COLOR:0:3:MAT:0:0:0:0:0:0]
+ [TILE:1:1:246:128:' ']
+ [TILE:1:2:' ':' ':254]
+ [TILE:1:3:254:'/':240]
+ [COLOR:1:1:MAT:7:0:0:0:0:0]
+ [COLOR:1:2:0:0:0:0:0:0:7:0:0]
+ [COLOR:1:3:7:0:0:6:0:0:6:0:0]
+ [TILE:2:1:21:' ':128]
+ [TILE:2:2:128:' ':246]
+ [TILE:2:3:177:19:177]
+ [COLOR:2:1:6:0:0:0:0:0:7:0:0]
+ [COLOR:2:2:7:0:0:0:0:0:MAT]
+ [COLOR:2:3:7:0:0:6:0:0:7:0:0]
+ Tile 15 marks places where machines can connect.
+ Tile 19 marks the hearth (color changed to reflect power).
+ [TILE:3:1:15:246:15]
+ [TILE:3:2:'\':19:'/']
+ [TILE:3:3:7:' ':7]
+ Color 1:?:1 water indicator, 4:?:1 magma indicator:
+ [COLOR:3:1:7:0:0:MAT:7:0:0]
+ [COLOR:3:2:6:0:0:0:0:1:6:0:0]
+ [COLOR:3:3:1:7:1:0:0:0:4:7:1]
+ [BUILD_ITEM:1:BARREL:NONE:INORGANIC:NONE][EMPTY][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:PIPE_SECTION:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON:INORGANIC:NONE][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:CHAIN:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:TRAPPARTS:NONE:NONE:NONE][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:BLOCKS:NONE:NONE:NONE][BUILDMAT][FIRE_BUILD_SAFE]
+
+[BUILDING_WORKSHOP:MAGMA_STEAM_ENGINE]
+ [NAME:Magma Steam Engine]
+ [NAME_COLOR:4:0:1]
+ [DIM:3:3]
+ [WORK_LOCATION:2:3]
+ [BUILD_LABOR:MECHANIC]
+ [BUILD_KEY:CUSTOM_ALT_E]
+ [NEEDS_MAGMA]
+ [BLOCK:1:1:1:1]
+ [BLOCK:2:1:1:1]
+ [BLOCK:3:1:0:1]
+ [TILE:0:1:240:' ':254]
+ [TILE:0:2:' ':' ':128]
+ [TILE:0:3:246:' ':' ']
+ [COLOR:0:1:6:0:0:0:0:0:7:0:0]
+ [COLOR:0:2:0:0:0:0:0:0:7:0:0]
+ [COLOR:0:3:MAT:0:0:0:0:0:0]
+ [TILE:1:1:246:128:' ']
+ [TILE:1:2:' ':' ':254]
+ [TILE:1:3:254:'/':240]
+ [COLOR:1:1:MAT:7:0:0:0:0:0]
+ [COLOR:1:2:0:0:0:0:0:0:7:0:0]
+ [COLOR:1:3:7:0:0:6:0:0:6:0:0]
+ [TILE:2:1:21:' ':128]
+ [TILE:2:2:128:' ':246]
+ [TILE:2:3:177:19:177]
+ [COLOR:2:1:6:0:0:0:0:0:7:0:0]
+ [COLOR:2:2:7:0:0:0:0:0:MAT]
+ [COLOR:2:3:7:0:0:6:0:0:7:0:0]
+ Tile 15 marks places where machines can connect.
+ Tile 19 marks the hearth (color changed to reflect power).
+ [TILE:3:1:15:246:15]
+ [TILE:3:2:'\':19:'/']
+ [TILE:3:3:7:' ':7]
+ Color 1:?:1 water indicator, 4:?:1 magma indicator:
+ [COLOR:3:1:7:0:0:MAT:7:0:0]
+ [COLOR:3:2:6:0:0:0:0:1:6:0:0]
+ [COLOR:3:3:1:7:1:0:0:0:4:7:1]
+ [BUILD_ITEM:1:BARREL:NONE:INORGANIC:NONE][EMPTY][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:PIPE_SECTION:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON:INORGANIC:NONE][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:CHAIN:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:TRAPPARTS:NONE:NONE:NONE][CAN_USE_ARTIFACT]
+ [BUILD_ITEM:1:BLOCKS:NONE:NONE:NONE][BUILDMAT][MAGMA_BUILD_SAFE]
diff --git a/plugins/raw/item_trapcomp_steam_engine.txt b/plugins/raw/item_trapcomp_steam_engine.txt
new file mode 100644
index 000000000..c35f6ef45
--- /dev/null
+++ b/plugins/raw/item_trapcomp_steam_engine.txt
@@ -0,0 +1,12 @@
+item_trapcomp_steam_engine
+
+[OBJECT:ITEM]
+
+[ITEM_TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON]
+[NAME:piston:pistons]
+[ADJECTIVE:heavy]
+[SIZE:1800]
+[HITS:1]
+[MATERIAL_SIZE:6]
+[METAL]
+[ATTACK:BLUNT:40:200:bash:bashes:NO_SUB:2000]
diff --git a/plugins/raw/reaction_steam_engine.txt b/plugins/raw/reaction_steam_engine.txt
new file mode 100644
index 000000000..175ffdd50
--- /dev/null
+++ b/plugins/raw/reaction_steam_engine.txt
@@ -0,0 +1,14 @@
+reaction_steam_engine
+
+[OBJECT:REACTION]
+
+[REACTION:STOKE_BOILER]
+ [NAME:stoke the boiler]
+ [BUILDING:STEAM_ENGINE:CUSTOM_S]
+ [BUILDING:MAGMA_STEAM_ENGINE:CUSTOM_S]
+ [FUEL]
+ [SKILL:SMELT]
+ Dimension is the number of days it can produce 100 power * 100.
+ I.e. with 2000 it means energy of 1 job = 1 water wheel for 20 days.
+ [PRODUCT:100:1:LIQUID_MISC:NONE:WATER][PRODUCT_DIMENSION:2000]
+
diff --git a/plugins/rename.cpp b/plugins/rename.cpp
index 1871d0f73..99dc6848a 100644
--- a/plugins/rename.cpp
+++ b/plugins/rename.cpp
@@ -3,11 +3,15 @@
#include "Export.h"
#include "PluginManager.h"
+#include
+#include
+
#include "modules/Gui.h"
#include "modules/Translation.h"
#include "modules/Units.h"
+#include "modules/World.h"
-#include "DataDefs.h"
+#include
#include "df/ui.h"
#include "df/world.h"
#include "df/squad.h"
@@ -18,6 +22,11 @@
#include "df/historical_figure_info.h"
#include "df/assumed_identity.h"
#include "df/language_name.h"
+#include "df/building_stockpilest.h"
+#include "df/building_workshopst.h"
+#include "df/building_furnacest.h"
+#include "df/building_trapst.h"
+#include "df/building_siegeenginest.h"
#include "RemoteServer.h"
#include "rename.pb.h"
@@ -36,6 +45,8 @@ using namespace dfproto;
using df::global::ui;
using df::global::world;
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event);
+
static command_result rename(color_ostream &out, vector & parameters);
DFHACK_PLUGIN("rename");
@@ -51,8 +62,32 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector clear(); \
+ *buf += name; \
+ *buf += " ("; \
+ if (tag) *buf += (const char*)tag; \
+ else { std::string tmp; INTERPOSE_NEXT(getName)(&tmp); *buf += tmp; } \
+ *buf += ")"; \
+ return; \
+ } \
+ else \
+ INTERPOSE_NEXT(getName)(buf); \
+ } \
+ }; \
+ IMPLEMENT_VMETHOD_INTERPOSE(cname##_hook, getName);
+KNOWN_BUILDINGS
+#undef BUILDING
+
+static char getBuildingCode(df::building *bld)
+{
+ CHECK_NULL_POINTER(bld);
+
+#define BUILDING(code, cname, tag) \
+ if (strict_virtual_cast(bld)) return code;
+KNOWN_BUILDINGS
+#undef BUILDING
+
+ return 0;
+}
+
+static bool enable_building_rename(char code, bool enable)
+{
+ switch (code) {
+#define BUILDING(code, cname, tag) \
+ case code: return INTERPOSE_HOOK(cname##_hook, getName).apply(enable);
+KNOWN_BUILDINGS
+#undef BUILDING
+ default:
+ return false;
+ }
+}
+
+static void disable_building_rename()
+{
+#define BUILDING(code, cname, tag) \
+ INTERPOSE_HOOK(cname##_hook, getName).remove();
+KNOWN_BUILDINGS
+#undef BUILDING
+}
+
+static bool is_enabled_building(char code)
+{
+ switch (code) {
+#define BUILDING(code, cname, tag) \
+ case code: return INTERPOSE_HOOK(cname##_hook, getName).is_applied();
+KNOWN_BUILDINGS
+#undef BUILDING
+ default:
+ return false;
+ }
+}
+
+static void init_buildings(bool enable)
+{
+ disable_building_rename();
+
+ if (enable)
+ {
+ auto pworld = Core::getInstance().getWorld();
+ auto entry = pworld->GetPersistentData("rename/building_types");
+
+ if (entry.isValid())
+ {
+ std::string val = entry.val();
+ for (size_t i = 0; i < val.size(); i++)
+ enable_building_rename(val[i], true);
+ }
+ }
+}
+
+static bool canRenameBuilding(df::building *bld)
+{
+ return getBuildingCode(bld) != 0;
+}
+
+static bool isRenamingBuilding(df::building *bld)
+{
+ return is_enabled_building(getBuildingCode(bld));
+}
+
+static bool renameBuilding(df::building *bld, std::string name)
+{
+ char code = getBuildingCode(bld);
+ if (code == 0 && !name.empty())
+ return false;
+
+ if (!name.empty() && !is_enabled_building(code))
+ {
+ auto pworld = Core::getInstance().getWorld();
+ auto entry = pworld->GetPersistentData("rename/building_types", NULL);
+ if (!entry.isValid())
+ return false;
+
+ if (!enable_building_rename(code, true))
+ return false;
+
+ entry.val().push_back(code);
+ }
+
+ bld->name = name;
+ return true;
+}
+
static df::squad *getSquadByIndex(unsigned idx)
{
auto entity = df::historical_entity::find(ui->group_id);
@@ -101,14 +263,37 @@ static command_result RenameUnit(color_ostream &stream, const RenameUnitIn *in)
return CR_OK;
}
+static command_result RenameBuilding(color_ostream &stream, const RenameBuildingIn *in)
+{
+ auto building = df::building::find(in->building_id());
+ if (!building)
+ return CR_NOT_FOUND;
+
+ if (in->has_name())
+ {
+ if (!renameBuilding(building, in->name()))
+ return CR_FAILURE;
+ }
+
+ return CR_OK;
+}
+
DFhackCExport RPCService *plugin_rpcconnect(color_ostream &)
{
RPCService *svc = new RPCService();
svc->addFunction("RenameSquad", RenameSquad);
svc->addFunction("RenameUnit", RenameUnit);
+ svc->addFunction("RenameBuilding", RenameBuilding);
return svc;
}
+DFHACK_PLUGIN_LUA_FUNCTIONS {
+ DFHACK_LUA_FUNCTION(canRenameBuilding),
+ DFHACK_LUA_FUNCTION(isRenamingBuilding),
+ DFHACK_LUA_FUNCTION(renameBuilding),
+ DFHACK_LUA_END
+};
+
static command_result rename(color_ostream &out, vector ¶meters)
{
CoreSuspender suspend;
@@ -167,6 +352,20 @@ static command_result rename(color_ostream &out, vector ¶meters)
unit->custom_profession = parameters[1];
}
+ else if (cmd == "building")
+ {
+ if (parameters.size() != 2)
+ return CR_WRONG_USAGE;
+
+ if (ui->main.mode != ui_sidebar_mode::QueryBuilding)
+ return CR_WRONG_USAGE;
+
+ if (!renameBuilding(world->selected_building, parameters[1]))
+ {
+ out.printerr("This type of building is not supported.\n");
+ return CR_FAILURE;
+ }
+ }
else
{
if (!parameters.empty() && cmd != "?")
diff --git a/plugins/steam-engine.cpp b/plugins/steam-engine.cpp
new file mode 100644
index 000000000..cacfc6e16
--- /dev/null
+++ b/plugins/steam-engine.cpp
@@ -0,0 +1,1007 @@
+#include "Core.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include "df/graphic.h"
+#include "df/building_workshopst.h"
+#include "df/building_def_workshopst.h"
+#include "df/item_liquid_miscst.h"
+#include "df/power_info.h"
+#include "df/workshop_type.h"
+#include "df/builtin_mats.h"
+#include "df/world.h"
+#include "df/buildings_other_id.h"
+#include "df/machine.h"
+#include "df/job.h"
+#include "df/building_drawbuffer.h"
+#include "df/ui.h"
+#include "df/viewscreen_dwarfmodest.h"
+#include "df/ui_build_selector.h"
+#include "df/flow_info.h"
+#include "df/report.h"
+
+#include "MiscUtils.h"
+
+/*
+ * This plugin implements a steam engine workshop. It activates
+ * if there are any workshops in the raws with STEAM_ENGINE in
+ * their token, and provides the necessary behavior.
+ *
+ * Construction:
+ *
+ * The workshop needs water as its input, which it takes via a
+ * passable floor tile below it, like usual magma workshops do.
+ * The magma version also needs magma.
+ *
+ * ISSUE: Since this building is a machine, and machine collapse
+ * code cannot be modified, it would collapse over true open space.
+ * As a loophole, down stair provides support to machines, while
+ * being passable, so use them.
+ *
+ * After constructing the building itself, machines can be connected
+ * to the edge tiles that look like gear boxes. Their exact position
+ * is extracted from the workshop raws.
+ *
+ * ISSUE: Like with collapse above, part of the code involved in
+ * machine connection cannot be modified. As a result, the workshop
+ * can only immediately connect to machine components built AFTER it.
+ * This also means that engines cannot be chained without intermediate
+ * short axles that can be built later.
+ *
+ * Operation:
+ *
+ * In order to operate the engine, queue the Stoke Boiler job.
+ * A furnace operator will come, possibly bringing a bar of fuel,
+ * and perform it. As a result, a "boiling water" item will appear
+ * in the 't' view of the workshop.
+ *
+ * Note: The completion of the job will actually consume one unit
+ * of appropriate liquids from below the workshop.
+ *
+ * Every such item gives 100 power, up to a limit of 300 for coal,
+ * and 500 for a magma engine. The building can host twice that
+ * amount of items to provide longer autonomous running. When the
+ * boiler gets filled to capacity, all queued jobs are suspended;
+ * once it drops back to 3+1 or 5+1 items, they are re-enabled.
+ *
+ * While the engine is providing power, steam is being consumed.
+ * The consumption speed includes a fixed 10% waste rate, and
+ * the remaining 90% are applied proportionally to the actual
+ * load in the machine. With the engine at nominal 300 power with
+ * 150 load in the system, it will consume steam for actual
+ * 300*(10% + 90%*150/300) = 165 power.
+ *
+ * Masterpiece mechanism and chain will decrease the mechanical
+ * power drawn by the engine itself from 10 to 5. Masterpiece
+ * barrel decreases waste rate by 4%. Masterpiece piston and pipe
+ * decrease it by further 4%, and also decrease the whole steam
+ * use rate by 10%.
+ *
+ * Explosions:
+ *
+ * The engine must be constructed using barrel, pipe and piston
+ * from fire-safe, or in the magma version magma-safe metals.
+ *
+ * During operation weak parts get gradually worn out, and
+ * eventually the engine explodes. It should also explode if
+ * toppled during operation by a building destroyer, or a
+ * tantruming dwarf.
+ *
+ * Save files:
+ *
+ * It should be safe to load and view fortresses using engines
+ * from a DF version without DFHack installed, except that in such
+ * case the engines won't work. However actually making modifications
+ * to them, or machines they connect to (including by pulling levers),
+ * can easily result in inconsistent state once this plugin is
+ * available again. The effects may be as weird as negative power
+ * being generated.
+ */
+
+using std::vector;
+using std::string;
+using std::stack;
+using namespace DFHack;
+using namespace df::enums;
+
+using df::global::gps;
+using df::global::world;
+using df::global::ui;
+using df::global::ui_build_selector;
+
+DFHACK_PLUGIN("steam-engine");
+
+/*
+ * List of known steam engine workshop raws.
+ */
+
+struct steam_engine_workshop {
+ int id;
+ df::building_def_workshopst *def;
+ // Cached properties
+ bool is_magma;
+ int max_power, max_capacity;
+ int wear_temp;
+ // Special tiles (relative position)
+ std::vector gear_tiles;
+ df::coord2d hearth_tile;
+ df::coord2d water_tile;
+ df::coord2d magma_tile;
+};
+
+std::vector engines;
+
+steam_engine_workshop *find_steam_engine(int id)
+{
+ for (size_t i = 0; i < engines.size(); i++)
+ if (engines[i].id == id)
+ return &engines[i];
+
+ return NULL;
+}
+
+/*
+ * Misc utilities.
+ */
+
+static const int hearth_colors[6][2] = {
+ { COLOR_BLACK, 1 },
+ { COLOR_BROWN, 0 },
+ { COLOR_RED, 0 },
+ { COLOR_RED, 1 },
+ { COLOR_BROWN, 1 },
+ { COLOR_GREY, 1 }
+};
+
+void enable_updates_at(df::coord pos, bool flow, bool temp)
+{
+ static const int delta[4][2] = { { -1, -1 }, { 1, -1 }, { -1, 1 }, { 1, 1 } };
+
+ for (int i = 0; i < 4; i++)
+ {
+ auto blk = Maps::getTileBlock(pos.x+delta[i][0], pos.y+delta[i][1], pos.z);
+ Maps::enableBlockUpdates(blk, flow, temp);
+ }
+}
+
+void decrement_flow(df::coord pos, int amount)
+{
+ auto pldes = Maps::getTileDesignation(pos);
+ if (!pldes) return;
+
+ int nsize = std::max(0, int(pldes->bits.flow_size - amount));
+ pldes->bits.flow_size = nsize;
+ pldes->bits.flow_forbid = (nsize > 3 || pldes->bits.liquid_type == tile_liquid::Magma);
+
+ enable_updates_at(pos, true, false);
+}
+
+void make_explosion(df::coord center, int power)
+{
+ static const int bias[9] = {
+ 60, 30, 60,
+ 30, 0, 30,
+ 60, 30, 60
+ };
+
+ int mat_type = builtin_mats::WATER, mat_index = -1;
+ int i = 0;
+
+ for (int dx = -1; dx <= 1; dx++)
+ {
+ for (int dy = -1; dy <= 1; dy++)
+ {
+ int size = power - bias[i++];
+ auto pos = center + df::coord(dx,dy,0);
+
+ if (size > 0)
+ Maps::spawnFlow(pos, flow_type::MaterialDust, mat_type, mat_index, size);
+ }
+ }
+
+ Gui::showAutoAnnouncement(
+ announcement_type::CAVE_COLLAPSE, center,
+ "A boiler has exploded!", COLOR_RED, true
+ );
+}
+
+static const int WEAR_TICKS = 806400;
+
+bool add_wear_nodestroy(df::item_actual *item, int rate)
+{
+ if (item->incWearTimer(rate))
+ {
+ while (item->wear_timer >= WEAR_TICKS)
+ {
+ item->wear_timer -= WEAR_TICKS;
+ item->wear++;
+ }
+ }
+
+ return item->wear > 3;
+}
+
+/*
+ * Hook for the liquid item. Implements a special 'boiling'
+ * matter state with a modified description and temperature
+ * locked at boiling-1.
+ */
+
+struct liquid_hook : df::item_liquid_miscst {
+ typedef df::item_liquid_miscst interpose_base;
+
+ static const uint32_t BOILING_FLAG = 0x80000000U;
+
+ DEFINE_VMETHOD_INTERPOSE(void, getItemDescription, (std::string *buf, int8_t mode))
+ {
+ if (mat_state.whole & BOILING_FLAG)
+ buf->append("boiling ");
+
+ INTERPOSE_NEXT(getItemDescription)(buf, mode);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t unk))
+ {
+ if (mat_state.whole & BOILING_FLAG)
+ temp = std::max(int(temp), getBoilingPoint()-1);
+
+ return INTERPOSE_NEXT(adjustTemperature)(temp, unk);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, checkTemperatureDamage, ())
+ {
+ if (mat_state.whole & BOILING_FLAG)
+ temperature = std::max(int(temperature), getBoilingPoint()-1);
+
+ return INTERPOSE_NEXT(checkTemperatureDamage)();
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, getItemDescription);
+IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, adjustTemperature);
+IMPLEMENT_VMETHOD_INTERPOSE(liquid_hook, checkTemperatureDamage);
+
+/*
+ * Hook for the workshop itself. Implements core logic.
+ */
+
+struct workshop_hook : df::building_workshopst {
+ typedef df::building_workshopst interpose_base;
+
+ // Engine detection
+
+ steam_engine_workshop *get_steam_engine()
+ {
+ if (type == workshop_type::Custom)
+ return find_steam_engine(custom_type);
+
+ return NULL;
+ }
+
+ inline bool is_fully_built()
+ {
+ return getBuildStage() >= getMaxBuildStage();
+ }
+
+ // Use high bits of flags to store current steam amount.
+ // This is necessary for consistency if items disappear unexpectedly.
+
+ int get_steam_amount()
+ {
+ return (flags.whole >> 28) & 15;
+ }
+
+ void set_steam_amount(int count)
+ {
+ flags.whole = (flags.whole & 0x0FFFFFFFU) | uint32_t((count & 15) << 28);
+ }
+
+ // Find liquids to consume below the engine.
+
+ bool find_liquids(df::coord *pwater, df::coord *pmagma, bool is_magma, int min_level)
+ {
+ if (!is_magma)
+ pmagma = NULL;
+
+ for (int x = x1; x <= x2; x++)
+ {
+ for (int y = y1; y <= y2; y++)
+ {
+ auto ptile = Maps::getTileType(x,y,z);
+ if (!ptile || !LowPassable(*ptile))
+ continue;
+
+ auto pltile = Maps::getTileType(x,y,z-1);
+ if (!pltile || !FlowPassable(*pltile))
+ continue;
+
+ auto pldes = Maps::getTileDesignation(x,y,z-1);
+ if (!pldes || pldes->bits.flow_size < min_level)
+ continue;
+
+ if (pldes->bits.liquid_type == tile_liquid::Magma)
+ {
+ if (pmagma)
+ *pmagma = df::coord(x,y,z-1);
+ if (pwater->isValid())
+ return true;
+ }
+ else
+ {
+ *pwater = df::coord(x,y,z-1);
+ if (!pmagma || pmagma->isValid())
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ // Absorbs a water item produced by stoke reaction into the engine.
+
+ bool absorb_unit(steam_engine_workshop *engine, df::item_liquid_miscst *liquid)
+ {
+ // Consume liquid inputs
+ df::coord water, magma;
+
+ if (!find_liquids(&water, &magma, engine->is_magma, 1))
+ {
+ // Destroy the item with enormous wear amount.
+ liquid->addWear(WEAR_TICKS*5, true, false);
+ return false;
+ }
+
+ decrement_flow(water, 1);
+ if (engine->is_magma)
+ decrement_flow(magma, 1);
+
+ // Update flags
+ liquid->flags.bits.in_building = true;
+ liquid->mat_state.whole |= liquid_hook::BOILING_FLAG;
+ liquid->temperature = liquid->getBoilingPoint()-1;
+ liquid->temperature_fraction = 0;
+
+ // This affects where the steam appears to come from
+ if (engine->hearth_tile.isValid())
+ liquid->pos = df::coord(x1+engine->hearth_tile.x, y1+engine->hearth_tile.y, z);
+
+ // Enable block temperature updates
+ enable_updates_at(liquid->pos, false, true);
+ return true;
+ }
+
+ bool boil_unit(df::item_liquid_miscst *liquid)
+ {
+ liquid->wear = 4;
+ liquid->flags.bits.in_building = false;
+ liquid->temperature = liquid->getBoilingPoint() + 10;
+
+ return liquid->checkMeltBoil();
+ }
+
+ void suspend_jobs(bool suspend)
+ {
+ for (size_t i = 0; i < jobs.size(); i++)
+ if (jobs[i]->job_type == job_type::CustomReaction)
+ jobs[i]->flags.bits.suspend = suspend;
+ }
+
+ // Scan contained items for boiled steam to absorb.
+
+ df::item_liquid_miscst *collect_steam(steam_engine_workshop *engine, int *count)
+ {
+ df::item_liquid_miscst *first = NULL;
+ *count = 0;
+
+ for (int i = contained_items.size()-1; i >= 0; i--)
+ {
+ auto item = contained_items[i];
+ if (item->use_mode != 0)
+ continue;
+
+ auto liquid = strict_virtual_cast(item->item);
+ if (!liquid)
+ continue;
+
+ if (!liquid->flags.bits.in_building)
+ {
+ if (liquid->mat_type != builtin_mats::WATER ||
+ liquid->age > 1 ||
+ liquid->wear != 0)
+ continue;
+
+ // This may destroy the item
+ if (!absorb_unit(engine, liquid))
+ continue;
+ }
+
+ if (*count < engine->max_capacity)
+ {
+ first = liquid;
+ ++*count;
+ }
+ else
+ {
+ // Overpressure valve
+ boil_unit(liquid);
+ suspend_jobs(true);
+ }
+ }
+
+ return first;
+ }
+
+ void random_boil()
+ {
+ int cnt = 0;
+
+ for (int i = contained_items.size()-1; i >= 0; i--)
+ {
+ auto item = contained_items[i];
+ if (item->use_mode != 0 || !item->item->flags.bits.in_building)
+ continue;
+
+ auto liquid = strict_virtual_cast(item->item);
+ if (!liquid)
+ continue;
+
+ if (cnt == 0 || rand() < RAND_MAX/2)
+ {
+ cnt++;
+ boil_unit(liquid);
+ }
+ }
+ }
+
+ int classify_component(df::building_actual::T_contained_items *item)
+ {
+ if (item->use_mode != 2 || item->item->isBuildMat())
+ return -1;
+
+ switch (item->item->getType())
+ {
+ case item_type::TRAPPARTS:
+ case item_type::CHAIN:
+ return 0;
+ case item_type::BARREL:
+ return 2;
+ default:
+ return 1;
+ }
+ }
+
+ bool check_component_wear(steam_engine_workshop *engine, int count, int power)
+ {
+ int coeffs[3] = { 0, power, count };
+
+ for (int i = contained_items.size()-1; i >= 0; i--)
+ {
+ int type = classify_component(contained_items[i]);
+ if (type < 0)
+ continue;
+
+ df::item *item = contained_items[i]->item;
+ int melt_temp = item->getMeltingPoint();
+ if (coeffs[type] == 0 || melt_temp >= engine->wear_temp)
+ continue;
+
+ // let 500 degree delta at 4 pressure work 1 season
+ float ticks = coeffs[type]*(engine->wear_temp - melt_temp)*3.0f/500.0f/4.0f;
+ if (item->addWear(int(8*(1 + ticks)), true, true))
+ return true;
+ }
+
+ return false;
+ }
+
+ float get_component_quality(int use_type)
+ {
+ float sum = 0, cnt = 0;
+
+ for (size_t i = 0; i < contained_items.size(); i++)
+ {
+ int type = classify_component(contained_items[i]);
+ if (type != use_type)
+ continue;
+
+ sum += contained_items[i]->item->getQuality();
+ cnt += 1;
+ }
+
+ return (cnt > 0 ? sum/cnt : 0);
+ }
+
+ int get_steam_use_rate(steam_engine_workshop *engine, int dimension, int power_level)
+ {
+ // total ticks to wear off completely
+ float ticks = WEAR_TICKS * 4.0f;
+ // dimension == days it lasts * 100
+ ticks /= 1200.0f * dimension / 100.0f;
+ // true power use
+ float power_rate = 1.0f;
+ // check the actual load
+ if (auto mptr = df::machine::find(machine.machine_id))
+ {
+ if (mptr->cur_power >= mptr->min_power)
+ power_rate = float(mptr->min_power) / mptr->cur_power;
+ else
+ power_rate = 0.0f;
+ }
+ // waste rate: 1-10% depending on piston assembly quality
+ float piston_qual = get_component_quality(1);
+ float waste = 0.1f - 0.016f * 0.5f * (piston_qual + get_component_quality(2));
+ float efficiency_coeff = 1.0f - 0.02f * piston_qual;
+ // apply rate and waste factor
+ ticks *= (waste + 0.9f*power_rate)*power_level*efficiency_coeff;
+ // end result
+ return std::max(1, int(ticks));
+ }
+
+ void update_under_construction(steam_engine_workshop *engine)
+ {
+ if (machine.machine_id != -1)
+ return;
+
+ int cur_count = 0;
+
+ if (auto first = collect_steam(engine, &cur_count))
+ {
+ if (add_wear_nodestroy(first, WEAR_TICKS*4/10))
+ {
+ boil_unit(first);
+ cur_count--;
+ }
+ }
+
+ set_steam_amount(cur_count);
+ }
+
+ void update_working(steam_engine_workshop *engine)
+ {
+ int old_count = get_steam_amount();
+ int old_power = std::min(engine->max_power, old_count);
+ int cur_count = 0;
+
+ if (auto first = collect_steam(engine, &cur_count))
+ {
+ int rate = get_steam_use_rate(engine, first->dimension, old_power);
+
+ if (add_wear_nodestroy(first, rate))
+ {
+ boil_unit(first);
+ cur_count--;
+ }
+
+ if (check_component_wear(engine, old_count, old_power))
+ return;
+ }
+
+ if (old_count < engine->max_capacity && cur_count == engine->max_capacity)
+ suspend_jobs(true);
+ else if (cur_count <= engine->max_power+1 && old_count > engine->max_power+1)
+ suspend_jobs(false);
+
+ set_steam_amount(cur_count);
+
+ int cur_power = std::min(engine->max_power, cur_count);
+ if (cur_power != old_power)
+ {
+ auto mptr = df::machine::find(machine.machine_id);
+ if (mptr)
+ mptr->cur_power += (cur_power - old_power)*100;
+ }
+ }
+
+ // Furnaces need architecture, and this is a workshop
+ // only because furnaces cannot connect to machines.
+ DEFINE_VMETHOD_INTERPOSE(bool, needsDesign, ())
+ {
+ if (get_steam_engine())
+ return true;
+
+ return INTERPOSE_NEXT(needsDesign)();
+ }
+
+ // Machine interface
+ DEFINE_VMETHOD_INTERPOSE(void, getPowerInfo, (df::power_info *info))
+ {
+ if (auto engine = get_steam_engine())
+ {
+ info->produced = std::min(engine->max_power, get_steam_amount())*100;
+ info->consumed = 10 - int(get_component_quality(0));
+ return;
+ }
+
+ INTERPOSE_NEXT(getPowerInfo)(info);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(df::machine_info*, getMachineInfo, ())
+ {
+ if (get_steam_engine())
+ return &machine;
+
+ return INTERPOSE_NEXT(getMachineInfo)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, isPowerSource, ())
+ {
+ if (get_steam_engine())
+ return true;
+
+ return INTERPOSE_NEXT(isPowerSource)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, categorize, (bool free))
+ {
+ if (get_steam_engine())
+ {
+ auto &vec = world->buildings.other[buildings_other_id::ANY_MACHINE];
+ insert_into_vector(vec, &df::building::id, (df::building*)this);
+ }
+
+ INTERPOSE_NEXT(categorize)(free);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, uncategorize, ())
+ {
+ if (get_steam_engine())
+ {
+ auto &vec = world->buildings.other[buildings_other_id::ANY_MACHINE];
+ erase_from_vector(vec, &df::building::id, id);
+ }
+
+ INTERPOSE_NEXT(uncategorize)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, canConnectToMachine, (df::machine_tile_set *info))
+ {
+ if (auto engine = get_steam_engine())
+ {
+ int real_cx = centerx, real_cy = centery;
+ bool ok = false;
+
+ for (size_t i = 0; i < engine->gear_tiles.size(); i++)
+ {
+ // the original function connects to the center tile
+ centerx = x1 + engine->gear_tiles[i].x;
+ centery = y1 + engine->gear_tiles[i].y;
+
+ if (!INTERPOSE_NEXT(canConnectToMachine)(info))
+ continue;
+
+ ok = true;
+ break;
+ }
+
+ centerx = real_cx; centery = real_cy;
+ return ok;
+ }
+ else
+ return INTERPOSE_NEXT(canConnectToMachine)(info);
+ }
+
+ // Operation logic
+ DEFINE_VMETHOD_INTERPOSE(bool, isUnpowered, ())
+ {
+ if (auto engine = get_steam_engine())
+ {
+ df::coord water, magma;
+ return !find_liquids(&water, &magma, engine->is_magma, 3);
+ }
+
+ return INTERPOSE_NEXT(isUnpowered)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, updateAction, ())
+ {
+ if (auto engine = get_steam_engine())
+ {
+ if (is_fully_built())
+ update_working(engine);
+ else
+ update_under_construction(engine);
+
+ if (flags.bits.almost_deleted)
+ return;
+ }
+
+ INTERPOSE_NEXT(updateAction)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (df::building_drawbuffer *db, void *unk))
+ {
+ INTERPOSE_NEXT(drawBuilding)(db, unk);
+
+ if (auto engine = get_steam_engine())
+ {
+ if (!is_fully_built())
+ return;
+
+ // If machine is running, tweak gear assemblies
+ auto mptr = df::machine::find(machine.machine_id);
+ if (mptr && (mptr->visual_phase & 1) != 0)
+ {
+ for (size_t i = 0; i < engine->gear_tiles.size(); i++)
+ {
+ auto pos = engine->gear_tiles[i];
+ db->tile[pos.x][pos.y] = 42;
+ }
+ }
+
+ // Use the hearth color to display power level
+ if (engine->hearth_tile.isValid())
+ {
+ auto pos = engine->hearth_tile;
+ int power = std::min(engine->max_power, get_steam_amount());
+ db->fore[pos.x][pos.y] = hearth_colors[power][0];
+ db->bright[pos.x][pos.y] = hearth_colors[power][1];
+ }
+
+ // Set liquid indicator state
+ if (engine->water_tile.isValid() || engine->magma_tile.isValid())
+ {
+ df::coord water, magma;
+ find_liquids(&water, &magma, engine->is_magma, 3);
+ df::coord dwater, dmagma;
+ find_liquids(&dwater, &dmagma, engine->is_magma, 5);
+
+ if (engine->water_tile.isValid())
+ {
+ if (!water.isValid())
+ db->fore[engine->water_tile.x][engine->water_tile.y] = 0;
+ else if (!dwater.isValid())
+ db->bright[engine->water_tile.x][engine->water_tile.y] = 0;
+ }
+ if (engine->magma_tile.isValid() && engine->is_magma)
+ {
+ if (!magma.isValid())
+ db->fore[engine->magma_tile.x][engine->magma_tile.y] = 0;
+ else if (!dmagma.isValid())
+ db->bright[engine->magma_tile.x][engine->magma_tile.y] = 0;
+ }
+ }
+ }
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, deconstructItems, (bool noscatter, bool lost))
+ {
+ if (get_steam_engine())
+ {
+ // Explode if any steam left
+ if (int amount = get_steam_amount())
+ {
+ make_explosion(
+ df::coord((x1+x2)/2, (y1+y2)/2, z),
+ 40 + amount * 20
+ );
+
+ random_boil();
+ }
+ }
+
+ INTERPOSE_NEXT(deconstructItems)(noscatter, lost);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, needsDesign);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, getPowerInfo);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, getMachineInfo);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, isPowerSource);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, categorize);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, uncategorize);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, canConnectToMachine);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, isUnpowered);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, updateAction);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, drawBuilding);
+IMPLEMENT_VMETHOD_INTERPOSE(workshop_hook, deconstructItems);
+
+/*
+ * Hook for the dwarfmode screen. Tweaks the build menu
+ * behavior to suit the steam engine building more.
+ */
+
+struct dwarfmode_hook : df::viewscreen_dwarfmodest
+{
+ typedef df::viewscreen_dwarfmodest interpose_base;
+
+ steam_engine_workshop *get_steam_engine()
+ {
+ if (ui->main.mode == ui_sidebar_mode::Build &&
+ ui_build_selector->stage == 1 &&
+ ui_build_selector->building_type == building_type::Workshop &&
+ ui_build_selector->building_subtype == workshop_type::Custom)
+ {
+ return find_steam_engine(ui_build_selector->custom_type);
+ }
+
+ return NULL;
+ }
+
+ void check_hanging_tiles(steam_engine_workshop *engine)
+ {
+ using df::global::cursor;
+
+ if (!engine) return;
+
+ bool error = false;
+
+ int x1 = cursor->x - engine->def->workloc_x;
+ int y1 = cursor->y - engine->def->workloc_y;
+
+ for (int x = 0; x < engine->def->dim_x; x++)
+ {
+ for (int y = 0; y < engine->def->dim_y; y++)
+ {
+ if (ui_build_selector->tiles[x][y] >= 5)
+ continue;
+
+ auto ptile = Maps::getTileType(x1+x,y1+y,cursor->z);
+ if (ptile && !isOpenTerrain(*ptile))
+ continue;
+
+ ui_build_selector->tiles[x][y] = 6;
+ error = true;
+ }
+ }
+
+ if (error)
+ {
+ const char *msg = "Hanging - cover channels with down stairs.";
+ ui_build_selector->errors.push_back(new std::string(msg));
+ }
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input))
+ {
+ steam_engine_workshop *engine = get_steam_engine();
+
+ // Selector insists that workshops cannot be placed hanging
+ // unless they require magma, so pretend we always do.
+ if (engine)
+ engine->def->needs_magma = true;
+
+ INTERPOSE_NEXT(feed)(input);
+
+ // Restore the flag
+ if (engine)
+ engine->def->needs_magma = engine->is_magma;
+
+ // And now, check for open space. Since these workshops
+ // are machines, they will collapse over true open space.
+ check_hanging_tiles(get_steam_engine());
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(dwarfmode_hook, feed);
+
+/*
+ * Scan raws for matching workshop buildings.
+ */
+
+static bool find_engines()
+{
+ engines.clear();
+
+ auto &wslist = world->raws.buildings.workshops;
+
+ for (size_t i = 0; i < wslist.size(); i++)
+ {
+ if (strstr(wslist[i]->code.c_str(), "STEAM_ENGINE") == NULL)
+ continue;
+
+ steam_engine_workshop ws;
+ ws.def = wslist[i];
+ ws.id = ws.def->id;
+
+ int bs = ws.def->build_stages;
+ for (int x = 0; x < ws.def->dim_x; x++)
+ {
+ for (int y = 0; y < ws.def->dim_y; y++)
+ {
+ switch (ws.def->tile[bs][x][y])
+ {
+ case 15:
+ ws.gear_tiles.push_back(df::coord2d(x,y));
+ break;
+ case 19:
+ ws.hearth_tile = df::coord2d(x,y);
+ break;
+ }
+
+ if (ws.def->tile_color[2][bs][x][y])
+ {
+ switch (ws.def->tile_color[0][bs][x][y])
+ {
+ case 1:
+ ws.water_tile = df::coord2d(x,y);
+ break;
+ case 4:
+ ws.magma_tile = df::coord2d(x,y);
+ break;
+ }
+ }
+ }
+ }
+
+ ws.is_magma = ws.def->needs_magma;
+ ws.max_power = ws.is_magma ? 5 : 3;
+ ws.max_capacity = ws.is_magma ? 10 : 6;
+ ws.wear_temp = ws.is_magma ? 12000 : 11000;
+
+ if (!ws.gear_tiles.empty())
+ engines.push_back(ws);
+ }
+
+ return !engines.empty();
+}
+
+static void enable_hooks(bool enable)
+{
+ INTERPOSE_HOOK(liquid_hook, getItemDescription).apply(enable);
+ INTERPOSE_HOOK(liquid_hook, adjustTemperature).apply(enable);
+ INTERPOSE_HOOK(liquid_hook, checkTemperatureDamage).apply(enable);
+
+ INTERPOSE_HOOK(workshop_hook, needsDesign).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, getPowerInfo).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, getMachineInfo).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, isPowerSource).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, categorize).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, uncategorize).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, canConnectToMachine).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, isUnpowered).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, updateAction).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, drawBuilding).apply(enable);
+ INTERPOSE_HOOK(workshop_hook, deconstructItems).apply(enable);
+
+ INTERPOSE_HOOK(dwarfmode_hook, feed).apply(enable);
+}
+
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
+{
+ switch (event) {
+ case SC_MAP_LOADED:
+ if (find_engines())
+ {
+ out.print("Detected steam engine workshops - enabling plugin.\n");
+ enable_hooks(true);
+ }
+ else
+ enable_hooks(false);
+ break;
+ case SC_MAP_UNLOADED:
+ enable_hooks(false);
+ engines.clear();
+ break;
+ default:
+ break;
+ }
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands)
+{
+ if (Core::getInstance().isMapLoaded())
+ plugin_onstatechange(out, SC_MAP_LOADED);
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown ( color_ostream &out )
+{
+ enable_hooks(false);
+ return CR_OK;
+}
diff --git a/plugins/tweak.cpp b/plugins/tweak.cpp
index 2daa9063b..fbea30231 100644
--- a/plugins/tweak.cpp
+++ b/plugins/tweak.cpp
@@ -7,12 +7,14 @@
#include "PluginManager.h"
#include "modules/Gui.h"
+#include "modules/Screen.h"
#include "modules/Units.h"
#include "modules/Items.h"
#include "MiscUtils.h"
#include "DataDefs.h"
+#include
#include "df/ui.h"
#include "df/world.h"
#include "df/squad.h"
@@ -26,6 +28,12 @@
#include "df/death_info.h"
#include "df/criminal_case.h"
#include "df/unit_inventory_item.h"
+#include "df/viewscreen_dwarfmodest.h"
+#include "df/squad_order_trainst.h"
+#include "df/ui_build_selector.h"
+#include "df/building_trapst.h"
+#include "df/item_actual.h"
+#include "df/contaminant.h"
#include
@@ -37,6 +45,9 @@ using namespace df::enums;
using df::global::ui;
using df::global::world;
+using df::global::ui_build_selector;
+using df::global::ui_menu_width;
+using df::global::ui_area_map_width;
using namespace DFHack::Gui;
@@ -67,6 +78,17 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector & params);
-
-
// to be called by tweak-fixmigrant
// units forced into the fort by removing the flags do not own their clothes
// which has the result that they drop all their clothes and become unhappy because they are naked
@@ -136,6 +155,160 @@ command_result fix_clothing_ownership(color_ostream &out, df::unit* unit)
return CR_OK;
}
+/*
+ * Save or restore cursor position on change to/from main dwarfmode menu.
+ */
+
+static df::coord last_view, last_cursor;
+
+struct stable_cursor_hook : df::viewscreen_dwarfmodest
+{
+ typedef df::viewscreen_dwarfmodest interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input))
+ {
+ bool was_default = (ui->main.mode == df::ui_sidebar_mode::Default);
+ df::coord view = Gui::getViewportPos();
+ df::coord cursor = Gui::getCursorPos();
+
+ INTERPOSE_NEXT(feed)(input);
+
+ bool is_default = (ui->main.mode == df::ui_sidebar_mode::Default);
+ df::coord cur_cursor = Gui::getCursorPos();
+
+ if (is_default && !was_default)
+ {
+ last_view = view; last_cursor = cursor;
+ }
+ else if (!is_default && was_default &&
+ Gui::getViewportPos() == last_view &&
+ last_cursor.isValid() && cur_cursor.isValid())
+ {
+ Gui::setCursorCoords(last_cursor.x, last_cursor.y, last_cursor.z);
+
+ // Force update of ui state
+ set tmp;
+ tmp.insert(interface_key::CURSOR_DOWN_Z);
+ INTERPOSE_NEXT(feed)(&tmp);
+ tmp.clear();
+ tmp.insert(interface_key::CURSOR_UP_Z);
+ INTERPOSE_NEXT(feed)(&tmp);
+ }
+ else if (cur_cursor.isValid())
+ {
+ last_cursor = df::coord();
+ }
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(stable_cursor_hook, feed);
+
+struct patrol_duty_hook : df::squad_order_trainst
+{
+ typedef df::squad_order_trainst interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(bool, isPatrol, ())
+ {
+ return false;
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(patrol_duty_hook, isPatrol);
+
+struct readable_build_plate_hook : df::viewscreen_dwarfmodest
+{
+ typedef df::viewscreen_dwarfmodest interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(void, render, ())
+ {
+ INTERPOSE_NEXT(render)();
+
+ if (ui->main.mode == ui_sidebar_mode::Build &&
+ ui_build_selector->stage == 1 &&
+ ui_build_selector->building_type == building_type::Trap &&
+ ui_build_selector->building_subtype == trap_type::PressurePlate &&
+ ui_build_selector->plate_info.flags.bits.units)
+ {
+ auto dims = Gui::getDwarfmodeViewDims();
+ int x = dims.menu_x1;
+
+ Screen::Pen pen(' ',COLOR_WHITE);
+
+ int minv = ui_build_selector->plate_info.unit_min;
+ if ((minv % 1000) == 0)
+ Screen::paintString(pen, x+11, 14, stl_sprintf("%3dK ", minv/1000));
+
+ int maxv = ui_build_selector->plate_info.unit_max;
+ if (maxv < 200000 && (maxv % 1000) == 0)
+ Screen::paintString(pen, x+24, 14, stl_sprintf("%3dK ", maxv/1000));
+ }
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(readable_build_plate_hook, render);
+
+struct stable_temp_hook : df::item_actual {
+ typedef df::item_actual interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t rate_mult))
+ {
+ if (temperature != temp)
+ {
+ // Bug 6012 is caused by fixed-point precision mismatch jitter
+ // when an item is being pushed by two sources at N and N+1.
+ // This check suppresses it altogether.
+ if (temp == temperature+1 ||
+ (temp == temperature-1 && temperature_fraction == 0))
+ temp = temperature;
+ // When SPEC_HEAT is NONE, the original function seems to not
+ // change the temperature, yet return true, which is silly.
+ else if (getSpecHeat() == 60001)
+ temp = temperature;
+ }
+
+ return INTERPOSE_NEXT(adjustTemperature)(temp, rate_mult);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, updateContaminants, ())
+ {
+ if (contaminants)
+ {
+ // Force 1-degree difference in contaminant temperature to 0
+ for (size_t i = 0; i < contaminants->size(); i++)
+ {
+ auto obj = (*contaminants)[i];
+
+ if (abs(obj->temperature - temperature) == 1)
+ {
+ obj->temperature = temperature;
+ obj->temperature_fraction = temperature_fraction;
+ }
+ }
+ }
+
+ return INTERPOSE_NEXT(updateContaminants)();
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, adjustTemperature);
+IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, updateContaminants);
+
+static void enable_hook(color_ostream &out, VMethodInterposeLinkBase &hook, vector ¶meters)
+{
+ if (vector_get(parameters, 1) == "disable")
+ {
+ hook.remove();
+ out.print("Disabled tweak %s\n", parameters[0].c_str());
+ }
+ else
+ {
+ if (hook.apply())
+ out.print("Enabled tweak %s\n", parameters[0].c_str());
+ else
+ out.printerr("Could not activate tweak %s\n", parameters[0].c_str());
+ }
+}
+
static command_result tweak(color_ostream &out, vector ¶meters)
{
CoreSuspender suspend;
@@ -234,6 +407,29 @@ static command_result tweak(color_ostream &out, vector ¶meters)
unit->profession2 = df::profession::TRADER;
return fix_clothing_ownership(out, unit);
}
+ else if (cmd == "stable-cursor")
+ {
+ enable_hook(out, INTERPOSE_HOOK(stable_cursor_hook, feed), parameters);
+ }
+ else if (cmd == "patrol-duty")
+ {
+ enable_hook(out, INTERPOSE_HOOK(patrol_duty_hook, isPatrol), parameters);
+ }
+ else if (cmd == "readable-build-plate")
+ {
+ if (!ui_build_selector || !ui_menu_width || !ui_area_map_width)
+ {
+ out.printerr("Necessary globals not known.\n");
+ return CR_FAILURE;
+ }
+
+ enable_hook(out, INTERPOSE_HOOK(readable_build_plate_hook, render), parameters);
+ }
+ else if (cmd == "stable-temp")
+ {
+ enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, adjustTemperature), parameters);
+ enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, updateContaminants), parameters);
+ }
else
return CR_WRONG_USAGE;
diff --git a/scripts/devel/lsmem.lua b/scripts/devel/lsmem.lua
new file mode 100644
index 000000000..75586324d
--- /dev/null
+++ b/scripts/devel/lsmem.lua
@@ -0,0 +1,14 @@
+-- Prints memory ranges of the process.
+
+for _,v in ipairs(dfhack.internal.getMemRanges()) do
+ local access = { '-', '-', '-', 'p' }
+ if v.read then access[1] = 'r' end
+ if v.write then access[2] = 'w' end
+ if v.execute then access[3] = 'x' end
+ if not v.valid then
+ access[4] = '?'
+ elseif v.shared then
+ access[4] = 's'
+ end
+ print(string.format('%08x-%08x %s %s', v.start_addr, v.end_addr, table.concat(access), v.name))
+end
diff --git a/scripts/devel/migrants-now.lua b/scripts/devel/migrants-now.lua
new file mode 100644
index 000000000..8eb4b0c8f
--- /dev/null
+++ b/scripts/devel/migrants-now.lua
@@ -0,0 +1,9 @@
+-- Force a migrants event in next 10 ticks.
+
+df.global.timed_events:insert('#',{
+ new = true,
+ type = df.timed_event_type.Migrants,
+ season = df.global.cur_season,
+ season_ticks = df.global.cur_season_tick+1,
+ entity = df.historical_entity.find(df.global.ui.civ_id)
+})
diff --git a/scripts/devel/pop-screen.lua b/scripts/devel/pop-screen.lua
new file mode 100644
index 000000000..f1ee072c5
--- /dev/null
+++ b/scripts/devel/pop-screen.lua
@@ -0,0 +1,3 @@
+-- For killing bugged out gui script screens.
+
+dfhack.screen.dismiss(dfhack.gui.getCurViewscreen())
diff --git a/scripts/fix/population-cap.lua b/scripts/fix/population-cap.lua
new file mode 100644
index 000000000..a34098c57
--- /dev/null
+++ b/scripts/fix/population-cap.lua
@@ -0,0 +1,40 @@
+-- Communicates current population to mountainhomes to avoid cap overshooting.
+
+-- The reason for population cap problems is that the population value it
+-- is compared against comes from the last dwarven caravan that successfully
+-- left for mountainhomes. This script instantly updates it.
+-- Note that a migration wave can still overshoot the limit by 1-2 dwarves because
+-- of the last migrant bringing his family. Likewise, king arrival ignores cap.
+
+local args = {...}
+
+local ui = df.global.ui
+local ui_stats = ui.tasks
+local civ = df.historical_entity.find(ui.civ_id)
+
+if not civ then
+ qerror('No active fortress.')
+end
+
+local civ_stats = civ.activity_stats
+
+if not civ_stats then
+ if args[1] ~= 'force' then
+ qerror('No caravan report object; use "fix/population-cap force" to create one')
+ end
+ print('Creating an empty statistics structure...')
+ civ.activity_stats = {
+ new = true,
+ created_weapons = { resize = #ui_stats.created_weapons },
+ known_creatures1 = { resize = #ui_stats.known_creatures1 },
+ known_creatures = { resize = #ui_stats.known_creatures },
+ known_plants1 = { resize = #ui_stats.known_plants1 },
+ known_plants = { resize = #ui_stats.known_plants },
+ }
+ civ_stats = civ.activity_stats
+end
+
+-- Use max to keep at least some of the original caravan communication idea
+civ_stats.population = math.max(civ_stats.population, ui_stats.population)
+
+print('Home civ notified about current population.')
diff --git a/scripts/gui/liquids.lua b/scripts/gui/liquids.lua
new file mode 100644
index 000000000..89f08b7cf
--- /dev/null
+++ b/scripts/gui/liquids.lua
@@ -0,0 +1,302 @@
+-- Interface front-end for liquids plugin.
+
+local utils = require 'utils'
+local gui = require 'gui'
+local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
+
+local liquids = require('plugins.liquids')
+
+local sel_rect = df.global.selection_rect
+
+local brushes = {
+ { tag = 'range', caption = 'Rectangle', range = true },
+ { tag = 'block', caption = '16x16 block' },
+ { tag = 'column', caption = 'Column' },
+ { tag = 'flood', caption = 'Flood' },
+}
+
+local paints = {
+ { tag = 'water', caption = 'Water', liquid = true, flow = true, key = 'w' },
+ { tag = 'magma', caption = 'Magma', liquid = true, flow = true, key = 'l' },
+ { tag = 'obsidian', caption = 'Obsidian Wall' },
+ { tag = 'obsidian_floor', caption = 'Obsidian Floor' },
+ { tag = 'riversource', caption = 'River Source' },
+ { tag = 'flowbits', caption = 'Flow Updates', flow = true },
+ { tag = 'wclean', caption = 'Clean Salt/Stagnant' },
+}
+
+local flowbits = {
+ { tag = '+', caption = 'Enable Updates' },
+ { tag = '-', caption = 'Disable Updates' },
+ { tag = '.', caption = 'Keep Updates' },
+}
+
+local setmode = {
+ { tag = '.', caption = 'Set Exactly' },
+ { tag = '+', caption = 'Only Increase' },
+ { tag = '-', caption = 'Only Decrease' },
+}
+
+local permaflows = {
+ { tag = '.', caption = "Keep Permaflow" },
+ { tag = '-', caption = 'Remove Permaflow' },
+ { tag = 'N', caption = 'Set Permaflow N' },
+ { tag = 'S', caption = 'Set Permaflow S' },
+ { tag = 'E', caption = 'Set Permaflow E' },
+ { tag = 'W', caption = 'Set Permaflow W' },
+ { tag = 'NE', caption = 'Set Permaflow NE' },
+ { tag = 'NW', caption = 'Set Permaflow NW' },
+ { tag = 'SE', caption = 'Set Permaflow SE' },
+ { tag = 'SW', caption = 'Set Permaflow SW' },
+}
+
+Toggle = defclass(Toggle)
+
+function Toggle:init(items)
+ self:init_fields{
+ items = items,
+ selected = 1
+ }
+ return self
+end
+
+function Toggle:get()
+ return self.items[self.selected]
+end
+
+function Toggle:render(dc)
+ local item = self:get()
+ if item then
+ dc:string(item.caption)
+ if item.key then
+ dc:string(" ("):string(item.key, COLOR_LIGHTGREEN):string(")")
+ end
+ else
+ dc:string('NONE', COLOR_RED)
+ end
+end
+
+function Toggle:step(delta)
+ if #self.items > 1 then
+ delta = delta or 1
+ self.selected = 1 + (self.selected + delta - 1) % #self.items
+ end
+end
+
+LiquidsUI = defclass(LiquidsUI, guidm.MenuOverlay)
+
+LiquidsUI.focus_path = 'liquids'
+
+function LiquidsUI:init()
+ self:init_fields{
+ brush = mkinstance(Toggle):init(brushes),
+ paint = mkinstance(Toggle):init(paints),
+ flow = mkinstance(Toggle):init(flowbits),
+ set = mkinstance(Toggle):init(setmode),
+ permaflow = mkinstance(Toggle):init(permaflows),
+ amount = 7,
+ }
+ guidm.MenuOverlay.init(self)
+ return self
+end
+
+function LiquidsUI:onDestroy()
+ guidm.clearSelection()
+end
+
+function render_liquid(dc, block, x, y)
+ local dsgn = block.designation[x%16][y%16]
+
+ if dsgn.flow_size > 0 then
+ if dsgn.liquid_type == df.tile_liquid.Magma then
+ dc:pen(COLOR_RED):string("Magma")
+ else
+ dc:pen(COLOR_BLUE)
+ if dsgn.water_stagnant then dc:string("Stagnant ") end
+ if dsgn.water_salt then dc:string("Salty ") end
+ dc:string("Water")
+ end
+ dc:string(" ["..dsgn.flow_size.."/7]")
+ else
+ dc:string('No Liquid')
+ end
+end
+
+local permaflow_abbr = {
+ north = 'N', south = 'S', east = 'E', west = 'W',
+ northeast = 'NE', northwest = 'NW', southeast = 'SE', southwest = 'SW'
+}
+
+function render_flow_state(dc, block, x, y)
+ local flow = block.liquid_flow[x%16][y%16]
+
+ if block.flags.update_liquid then
+ dc:string("Updating", COLOR_GREEN)
+ else
+ dc:string("Static")
+ end
+ dc:string(", ")
+ if flow.perm_flow_dir ~= 0 then
+ local tag = df.tile_liquid_flow_dir[flow.perm_flow_dir]
+ dc:string("Permaflow "..(permaflow_abbr[tag] or tag), COLOR_CYAN)
+ elseif flow.temp_flow_timer > 0 then
+ dc:string("Flowing "..flow.temp_flow_timer, COLOR_GREEN)
+ else
+ dc:string("No Flow")
+ end
+end
+
+function LiquidsUI:onRenderBody(dc)
+ dc:clear():seek(1,1):string("Paint Liquids Cheat", COLOR_WHITE)
+
+ local cursor = guidm.getCursorPos()
+ local block = dfhack.maps.getTileBlock(cursor)
+
+ if block then
+ local x, y = pos2xyz(cursor)
+ local tile = block.tiletype[x%16][y%16]
+
+ dc:seek(2,3):string(df.tiletype.attrs[tile].caption, COLOR_CYAN)
+ dc:newline(2):pen(COLOR_DARKGREY)
+ render_liquid(dc, block, x, y)
+ dc:newline(2):pen(COLOR_DARKGREY)
+ render_flow_state(dc, block, x, y)
+ else
+ dc:seek(2,3):string("No map data", COLOR_RED):advance(0,2)
+ end
+
+ dc:newline():pen(COLOR_GREY)
+
+ dc:newline(1):string("b", COLOR_LIGHTGREEN):string(": ")
+ self.brush:render(dc)
+ dc:newline(1):string("p", COLOR_LIGHTGREEN):string(": ")
+ self.paint:render(dc)
+
+ local paint = self.paint:get()
+
+ dc:newline()
+ if paint.liquid then
+ dc:newline(1):string("Amount: "..self.amount)
+ dc:advance(1):string("("):string("-+", COLOR_LIGHTGREEN):string(")")
+ dc:newline(3):string("s", COLOR_LIGHTGREEN):string(": ")
+ self.set:render(dc)
+ else
+ dc:advance(0,2)
+ end
+
+ dc:newline()
+ if paint.flow then
+ dc:newline(1):string("f", COLOR_LIGHTGREEN):string(": ")
+ self.flow:render(dc)
+ dc:newline(1):string("r", COLOR_LIGHTGREEN):string(": ")
+ self.permaflow:render(dc)
+ else
+ dc:advance(0,2)
+ end
+
+ dc:newline():newline(1):pen(COLOR_WHITE)
+ dc:string("Esc", COLOR_LIGHTGREEN):string(": Back, ")
+ dc:string("Enter", COLOR_LIGHTGREEN):string(": Paint")
+end
+
+function ensure_blocks(cursor, size, cb)
+ local cx,cy,cz = pos2xyz(cursor)
+ local all = true
+ for x=1,size.x or 1,16 do
+ for y=1,size.y or 1,16 do
+ for z=1,size.z do
+ if not dfhack.maps.getTileBlock(cx+x-1, cy+y-1, cz+z-1) then
+ all = false
+ end
+ end
+ end
+ end
+ if all then
+ cb()
+ return
+ end
+ dlg.showYesNoPrompt(
+ 'Instantiate Blocks',
+ 'Not all map blocks are allocated - instantiate?\n\nWarning: new untested feature.',
+ COLOR_YELLOW,
+ function()
+ for x=1,size.x or 1,16 do
+ for y=1,size.y or 1,16 do
+ for z=1,size.z do
+ dfhack.maps.ensureTileBlock(cx+x-1, cy+y-1, cz+z-1)
+ end
+ end
+ end
+ cb()
+ end,
+ function()
+ cb()
+ end
+ )
+end
+
+function LiquidsUI:onInput(keys)
+ local paint = self.paint:get()
+ local liquid = paint.liquid
+ if keys.CUSTOM_B then
+ self.brush:step()
+ elseif keys.CUSTOM_P then
+ self.paint:step()
+ elseif liquid and keys.SECONDSCROLL_UP then
+ self.amount = math.max(0, self.amount-1)
+ elseif liquid and keys.SECONDSCROLL_DOWN then
+ self.amount = math.min(7, self.amount+1)
+ elseif liquid and keys.CUSTOM_S then
+ self.set:step()
+ elseif paint.flow and keys.CUSTOM_F then
+ self.flow:step()
+ elseif paint.flow and keys.CUSTOM_R then
+ self.permaflow:step()
+ elseif keys.LEAVESCREEN then
+ if guidm.getSelection() then
+ guidm.clearSelection()
+ return
+ end
+ self:dismiss()
+ self:sendInputToParent('CURSOR_DOWN_Z')
+ self:sendInputToParent('CURSOR_UP_Z')
+ elseif keys.SELECT then
+ local cursor = guidm.getCursorPos()
+ local sp = guidm.getSelection()
+ local size = nil
+ if self.brush:get().range then
+ if not sp then
+ guidm.setSelectionStart(cursor)
+ return
+ else
+ guidm.clearSelection()
+ cursor, size = guidm.getSelectionRange(cursor, sp)
+ end
+ else
+ guidm.clearSelection()
+ end
+ local cb = curry(
+ liquids.paint,
+ cursor,
+ self.brush:get().tag, self.paint:get().tag,
+ self.amount, size,
+ self.set:get().tag, self.flow:get().tag,
+ self.permaflow:get().tag
+ )
+ ensure_blocks(cursor, size, cb)
+ elseif self:propagateMoveKeys(keys) then
+ return
+ elseif keys.D_LOOK_ARENA_WATER then
+ self.paint.selected = 1
+ elseif keys.D_LOOK_ARENA_MAGMA then
+ self.paint.selected = 2
+ end
+end
+
+if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/LookAround') then
+ qerror("This script requires the main dwarfmode view in 'k' mode")
+end
+
+local list = mkinstance(LiquidsUI):init()
+list:show()
diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua
index 6b4b4042b..c14bfcbe9 100644
--- a/scripts/gui/mechanisms.lua
+++ b/scripts/gui/mechanisms.lua
@@ -122,7 +122,7 @@ function MechanismList:onInput(keys)
end
end
-if dfhack.gui.getCurFocus() ~= 'dwarfmode/QueryBuilding/Some' then
+if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then
qerror("This script requires the main dwarfmode view in 'q' mode")
end
diff --git a/scripts/gui/power-meter.lua b/scripts/gui/power-meter.lua
new file mode 100644
index 000000000..8baf43e7e
--- /dev/null
+++ b/scripts/gui/power-meter.lua
@@ -0,0 +1,116 @@
+-- Interface front-end for power-meter plugin.
+
+local utils = require 'utils'
+local gui = require 'gui'
+local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
+
+local plugin = require('plugins.power-meter')
+local bselector = df.global.ui_build_selector
+
+PowerMeter = defclass(PowerMeter, guidm.MenuOverlay)
+
+PowerMeter.focus_path = 'power-meter'
+
+function PowerMeter:init()
+ self:init_fields{
+ min_power = 0, max_power = -1, invert = false,
+ }
+ guidm.MenuOverlay.init(self)
+ return self
+end
+
+function PowerMeter:onShow()
+ guidm.MenuOverlay.onShow(self)
+
+ -- Send an event to update the errors
+ bselector.plate_info.flags.whole = 0
+ self:sendInputToParent('BUILDING_TRIGGER_ENABLE_WATER')
+end
+
+function PowerMeter:onRenderBody(dc)
+ dc:fill(0,0,dc.width-1,13,gui.CLEAR_PEN)
+ dc:seek(1,1):pen(COLOR_WHITE)
+ dc:string("Power Meter"):newline():newline(1)
+ dc:string("Placement"):newline():newline(1)
+
+ dc:string("Excess power range:")
+
+ dc:newline(3):string("as", COLOR_LIGHTGREEN)
+ dc:string(": Min ")
+ if self.min_power <= 0 then
+ dc:string("(any)")
+ else
+ dc:string(''..self.min_power)
+ end
+
+ dc:newline(3):string("zx", COLOR_LIGHTGREEN)
+ dc:string(": Max ")
+ if self.max_power < 0 then
+ dc:string("(any)")
+ else
+ dc:string(''..self.max_power)
+ end
+ dc:newline():newline(1)
+
+ dc:string("i",COLOR_LIGHTGREEN):string(": ")
+ if self.invert then
+ dc:string("Inverted")
+ else
+ dc:string("Not inverted")
+ end
+end
+
+function PowerMeter:onInput(keys)
+ if keys.CUSTOM_I then
+ self.invert = not self.invert
+ elseif keys.BUILDING_TRIGGER_MIN_WATER_UP then
+ self.min_power = self.min_power + 10
+ elseif keys.BUILDING_TRIGGER_MIN_WATER_DOWN then
+ self.min_power = math.max(0, self.min_power - 10)
+ elseif keys.BUILDING_TRIGGER_MAX_WATER_UP then
+ if self.max_power < 0 then
+ self.max_power = 0
+ else
+ self.max_power = self.max_power + 10
+ end
+ elseif keys.BUILDING_TRIGGER_MAX_WATER_DOWN then
+ self.max_power = math.max(-1, self.max_power - 10)
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+ self:sendInputToParent('LEAVESCREEN')
+ elseif keys.SELECT then
+ if #bselector.errors == 0 then
+ if not plugin.makePowerMeter(
+ bselector.plate_info,
+ self.min_power, self.max_power, self.invert
+ )
+ then
+ dlg.showMessage(
+ 'Power Meter',
+ 'Could not initialize.', COLOR_LIGHTRED
+ )
+
+ self:dismiss()
+ self:sendInputToParent('LEAVESCREEN')
+ return
+ end
+
+ self:sendInputToParent('SELECT')
+ if bselector.stage ~= 1 then
+ self:dismiss()
+ end
+ end
+ elseif self:propagateMoveKeys(keys) then
+ return
+ end
+end
+
+if dfhack.gui.getCurFocus() ~= 'dwarfmode/Build/Position/Trap'
+or bselector.building_subtype ~= df.trap_type.PressurePlate
+then
+ qerror("This script requires the main dwarfmode view in build pressure plate mode")
+end
+
+local list = mkinstance(PowerMeter):init()
+list:show()
diff --git a/scripts/gui/rename.lua b/scripts/gui/rename.lua
new file mode 100644
index 000000000..a457a0bfd
--- /dev/null
+++ b/scripts/gui/rename.lua
@@ -0,0 +1,63 @@
+-- Rename various objects via gui.
+
+local gui = require 'gui'
+local dlg = require 'gui.dialogs'
+local plugin = require 'plugins.rename'
+
+local mode = ...
+local focus = dfhack.gui.getCurFocus()
+
+local function verify_mode(expected)
+ if mode ~= nil and mode ~= expected then
+ qerror('Invalid UI state for mode '..mode)
+ end
+end
+
+if string.match(focus, '^dwarfmode/QueryBuilding/Some') then
+ verify_mode('building')
+
+ local building = df.global.world.selected_building
+ if plugin.canRenameBuilding(building) then
+ dlg.showInputPrompt(
+ 'Rename Building',
+ 'Enter a new name for the building:', COLOR_GREEN,
+ building.name,
+ curry(plugin.renameBuilding, building)
+ )
+ else
+ dlg.showMessage(
+ 'Rename Building',
+ 'Cannot rename this type of building.', COLOR_LIGHTRED
+ )
+ end
+elseif dfhack.gui.getSelectedUnit(true) then
+ local unit = dfhack.gui.getSelectedUnit(true)
+
+ if mode == 'unit-profession' then
+ dlg.showInputPrompt(
+ 'Rename Unit',
+ 'Enter a new profession for the unit:', COLOR_GREEN,
+ unit.custom_profession,
+ function(newval)
+ unit.custom_profession = newval
+ end
+ )
+ else
+ verify_mode('unit')
+
+ local vname = dfhack.units.getVisibleName(unit)
+ local vnick = ''
+ if vname and vname.has_name then
+ vnick = vname.nickname
+ end
+
+ dlg.showInputPrompt(
+ 'Rename Unit',
+ 'Enter a new nickname for the unit:', COLOR_GREEN,
+ vnick,
+ curry(dfhack.units.setNickname, unit)
+ )
+ end
+elseif mode then
+ verify_mode(nil)
+end
diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua
new file mode 100644
index 000000000..bba6bce89
--- /dev/null
+++ b/scripts/gui/siege-engine.lua
@@ -0,0 +1,308 @@
+-- Front-end for the siege engine plugin.
+
+local utils = require 'utils'
+local gui = require 'gui'
+local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
+
+local plugin = require 'plugins.siege-engine'
+local wmap = df.global.world.map
+
+-- Globals kept between script calls
+last_target_min = last_target_min or nil
+last_target_max = last_target_max or nil
+
+SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay)
+
+SiegeEngine.focus_path = 'siege-engine'
+
+function SiegeEngine:init(building)
+ self:init_fields{
+ building = building,
+ center = utils.getBuildingCenter(building),
+ links = {}, selected = 1,
+ }
+ guidm.MenuOverlay.init(self)
+ self.mode_main = {
+ render = self:callback 'onRenderBody_main',
+ input = self:callback 'onInput_main',
+ }
+ self.mode_aim = {
+ render = self:callback 'onRenderBody_aim',
+ input = self:callback 'onInput_aim',
+ }
+ return self
+end
+
+function SiegeEngine:onShow()
+ guidm.MenuOverlay.onShow(self)
+
+ self.old_cursor = guidm.getCursorPos()
+ self.old_viewport = self:getViewport()
+
+ self.mode = self.mode_main
+ self:showCursor(false)
+end
+
+function SiegeEngine:onDestroy()
+ self:selectBuilding(self.building, self.old_cursor, self.old_viewport, 10)
+end
+
+function SiegeEngine:showCursor(enable)
+ local cursor = guidm.getCursorPos()
+ if cursor and not enable then
+ self.cursor = cursor
+ self.target_select_first = nil
+ guidm.clearCursorPos()
+ elseif not cursor and enable then
+ local view = self:getViewport()
+ cursor = self.cursor
+ if not cursor or not view:isVisible(cursor) then
+ cursor = view:getCenter()
+ end
+ self.cursor = nil
+ guidm.setCursorPos(cursor)
+ end
+end
+
+function SiegeEngine:centerViewOn(pos)
+ local cursor = guidm.getCursorPos()
+ if cursor then
+ guidm.setCursorPos(pos)
+ else
+ self.cursor = pos
+ end
+ self:getViewport():centerOn(pos):set()
+end
+
+function SiegeEngine:zoomToTarget()
+ local target_min, target_max = plugin.getTargetArea(self.building)
+ if target_min then
+ local cx = math.floor((target_min.x + target_max.x)/2)
+ local cy = math.floor((target_min.y + target_max.y)/2)
+ local cz = math.floor((target_min.z + target_max.z)/2)
+ for z = cz,target_max.z do
+ if plugin.getTileStatus(self.building, xyz2pos(cx,cy,z)) ~= 'blocked' then
+ cz = z
+ break
+ end
+ end
+ self:centerViewOn(xyz2pos(cx,cy,cz))
+ end
+end
+
+function paint_target_grid(dc, view, origin, p1, p2)
+ local r1, sz, r2 = guidm.getSelectionRange(p1, p2)
+
+ if view.z < r1.z or view.z > r2.z then
+ return
+ end
+
+ local p1 = view:tileToScreen(r1)
+ local p2 = view:tileToScreen(r2)
+ local org = view:tileToScreen(origin)
+ dc:pen{ fg = COLOR_CYAN, bg = COLOR_CYAN, ch = '+', bold = true }
+
+ -- Frame
+ dc:fill(p1.x,p1.y,p1.x,p2.y)
+ dc:fill(p1.x,p1.y,p2.x,p1.y)
+ dc:fill(p2.x,p1.y,p2.x,p2.y)
+ dc:fill(p1.x,p2.y,p2.x,p2.y)
+
+ -- Grid
+ local gxmin = org.x+10*math.ceil((p1.x-org.x)/10)
+ local gxmax = org.x+10*math.floor((p2.x-org.x)/10)
+ local gymin = org.y+10*math.ceil((p1.y-org.y)/10)
+ local gymax = org.y+10*math.floor((p2.y-org.y)/10)
+ for x = gxmin,gxmax,10 do
+ for y = gymin,gymax,10 do
+ dc:fill(p1.x,y,p2.x,y)
+ dc:fill(x,p1.y,x,p2.y)
+ end
+ end
+end
+
+function SiegeEngine:renderTargetView(target_min, target_max)
+ local view = self:getViewport()
+ local map = self.df_layout.map
+ local map_dc = gui.Painter.new(map)
+
+ plugin.paintAimScreen(
+ self.building, view:getPos(),
+ xy2pos(map.x1, map.y1), view:getSize()
+ )
+
+ if target_min and math.floor(dfhack.getTickCount()/500) % 2 == 0 then
+ paint_target_grid(map_dc, view, self.center, target_min, target_max)
+ end
+
+ local cursor = guidm.getCursorPos()
+ if cursor then
+ local cx, cy, cz = pos2xyz(view:tileToScreen(cursor))
+ if cz == 0 then
+ map_dc:seek(cx,cy):char('X', COLOR_YELLOW)
+ end
+ end
+end
+
+function SiegeEngine:onRenderBody_main(dc)
+ dc:newline(1):pen(COLOR_WHITE):string("Target: ")
+
+ local target_min, target_max = plugin.getTargetArea(self.building)
+ if target_min then
+ dc:string(
+ (target_max.x-target_min.x+1).."x"..
+ (target_max.y-target_min.y+1).."x"..
+ (target_max.z-target_min.z+1).." Rect"
+ )
+ else
+ dc:string("None (default)")
+ end
+
+ dc:newline(3):string("r",COLOR_LIGHTGREEN):string(": Rectangle")
+ if last_target_min then
+ dc:string(", "):string("p",COLOR_LIGHTGREEN):string(": Paste")
+ end
+ dc:newline(3)
+ if target_min then
+ dc:string("x",COLOR_LIGHTGREEN):string(": Clear, ")
+ dc:string("z",COLOR_LIGHTGREEN):string(": Zoom")
+ end
+
+ if self.target_select_first then
+ self:renderTargetView(self.target_select_first, guidm.getCursorPos())
+ else
+ self:renderTargetView(target_min, target_max)
+ end
+end
+
+function SiegeEngine:setTargetArea(p1, p2)
+ self.target_select_first = nil
+
+ if not plugin.setTargetArea(self.building, p1, p2) then
+ dlg.showMessage(
+ 'Set Target Area',
+ 'Could not set the target area', COLOR_LIGHTRED
+ )
+ else
+ last_target_min = p1
+ last_target_max = p2
+ end
+end
+
+function SiegeEngine:onInput_main(keys)
+ if keys.CUSTOM_R then
+ self:showCursor(true)
+ self.target_select_first = nil
+ self.mode = self.mode_aim
+ elseif keys.CUSTOM_P and last_target_min then
+ self:setTargetArea(last_target_min, last_target_max)
+ elseif keys.CUSTOM_Z then
+ self:zoomToTarget()
+ elseif keys.CUSTOM_X then
+ plugin.clearTargetArea(self.building)
+ elseif self:simulateViewScroll(keys) then
+ self.cursor = nil
+ else
+ return false
+ end
+ return true
+end
+
+local status_table = {
+ ok = { pen = COLOR_GREEN, msg = "Target accessible" },
+ out_of_range = { pen = COLOR_CYAN, msg = "Target out of range" },
+ blocked = { pen = COLOR_RED, msg = "Target obstructed" },
+}
+
+function SiegeEngine:onRenderBody_aim(dc)
+ local cursor = guidm.getCursorPos()
+ local first = self.target_select_first
+
+ dc:newline(1):string('Select target rectangle'):newline()
+
+ local info = status_table[plugin.getTileStatus(self.building, cursor)]
+ if info then
+ dc:newline(2):string(info.msg, info.pen)
+ else
+ dc:newline(2):string('ERROR', COLOR_RED)
+ end
+
+ dc:newline():newline(1):string("Enter",COLOR_LIGHTGREEN)
+ if first then
+ dc:string(": Finish rectangle")
+ else
+ dc:string(": Start rectangle")
+ end
+ dc:newline()
+
+ local target_min, target_max = plugin.getTargetArea(self.building)
+ if target_min then
+ dc:newline(1):string("z",COLOR_LIGHTGREEN):string(": Zoom to current target")
+ end
+
+ if first then
+ self:renderTargetView(first, cursor)
+ else
+ local target_min, target_max = plugin.getTargetArea(self.building)
+ self:renderTargetView(target_min, target_max)
+ end
+end
+
+function SiegeEngine:onInput_aim(keys)
+ if keys.SELECT then
+ local cursor = guidm.getCursorPos()
+ if self.target_select_first then
+ self:setTargetArea(self.target_select_first, cursor)
+
+ self.mode = self.mode_main
+ self:showCursor(false)
+ else
+ self.target_select_first = cursor
+ end
+ elseif keys.CUSTOM_Z then
+ self:zoomToTarget()
+ elseif keys.LEAVESCREEN then
+ self.mode = self.mode_main
+ self:showCursor(false)
+ elseif self:simulateCursorMovement(keys) then
+ self.cursor = nil
+ else
+ return false
+ end
+ return true
+end
+
+function SiegeEngine:onRenderBody(dc)
+ dc:clear()
+ dc:seek(1,1):pen(COLOR_WHITE):string(utils.getBuildingName(self.building)):newline()
+
+ self.mode.render(dc)
+
+ dc:seek(1, math.max(dc:localY(), 21)):pen(COLOR_WHITE)
+ dc:string("ESC", COLOR_LIGHTGREEN):string(": Back, ")
+ dc:string("c", COLOR_LIGHTGREEN):string(": Recenter")
+end
+
+function SiegeEngine:onInput(keys)
+ if self.mode.input(keys) then
+ --
+ elseif keys.CUSTOM_C then
+ self:centerViewOn(self.center)
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+ end
+end
+
+if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/SiegeEngine') then
+ qerror("This script requires a siege engine selected in 'q' mode")
+end
+
+local building = df.global.world.selected_building
+
+if not df.building_siegeenginest:is_instance(building) then
+ qerror("A siege engine must be selected")
+end
+
+local list = mkinstance(SiegeEngine):init(df.global.world.selected_building)
+list:show()