diff --git a/Lua API.html b/Lua API.html
index 06fa5418e..125687110 100644
--- a/Lua API.html
+++ b/Lua API.html
@@ -932,6 +932,21 @@ Returns entry, did_create_new
and automatically stored in the save game, these save and retrieval
functions can just copy values in memory without doing any actual I/O.
However, currently every entry has a 180+-byte dead-weight overhead.
+It is also possible to associate one bit per map tile with an entry,
+using these two methods:
+
+entry:getTilemask(block[, create])
+Retrieves the tile bitmask associated with this entry in the given map
+block. If create is true, an empty mask is created if none exists;
+otherwise the function returns nil, which must be assumed to be the same
+as an all-zero mask.
+
+entry:deleteTilemask(block)
+Deletes the associated tile mask from the given map block.
+
+
+Note that these masks are only saved in fortress mode, and also that deleting
+the persistent entry will NOT delete the associated masks.
@@ -1286,6 +1301,18 @@ tools like liquids or tiletypes are used. It also cannot possibly
take into account anything that depends on the actual units, like
burrows, or the presence of invaders.
+
dfhack.maps.hasTileAssignment(tilemask)
+Checks if the tile_bitmask object is not nil and contains any set bits; returns true or false.
+
+
dfhack.maps.getTileAssignment(tilemask,x,y)
+Checks if the tile_bitmask object is not nil and has the relevant bit set; returns true or false.
+
+
dfhack.maps.setTileAssignment(tilemask,x,y,enable)
+Sets the relevant bit in the tile_bitmask object to the enable argument.
+
+
dfhack.maps.resetTileAssignment(tilemask[,enable])
+Sets all bits in the mask to the enable argument.
+
diff --git a/Lua API.rst b/Lua API.rst
index fbb4b7d82..7ad6aec23 100644
--- a/Lua API.rst
+++ b/Lua API.rst
@@ -646,6 +646,24 @@ and automatically stored in the save game, these save and retrieval
functions can just copy values in memory without doing any actual I/O.
However, currently every entry has a 180+-byte dead-weight overhead.
+It is also possible to associate one bit per map tile with an entry,
+using these two methods:
+
+* ``entry:getTilemask(block[, create])``
+
+ Retrieves the tile bitmask associated with this entry in the given map
+ block. If ``create`` is *true*, an empty mask is created if none exists;
+ otherwise the function returns *nil*, which must be assumed to be the same
+ as an all-zero mask.
+
+* ``entry:deleteTilemask(block)``
+
+ Deletes the associated tile mask from the given map block.
+
+Note that these masks are only saved in fortress mode, and also that deleting
+the persistent entry will **NOT** delete the associated masks.
+
+
Material info lookup
--------------------
@@ -1079,6 +1097,22 @@ Maps module
take into account anything that depends on the actual units, like
burrows, or the presence of invaders.
+* ``dfhack.maps.hasTileAssignment(tilemask)``
+
+ Checks if the tile_bitmask object is not *nil* and contains any set bits; returns *true* or *false*.
+
+* ``dfhack.maps.getTileAssignment(tilemask,x,y)``
+
+ Checks if the tile_bitmask object is not *nil* and has the relevant bit set; returns *true* or *false*.
+
+* ``dfhack.maps.setTileAssignment(tilemask,x,y,enable)``
+
+ Sets the relevant bit in the tile_bitmask object to the *enable* argument.
+
+* ``dfhack.maps.resetTileAssignment(tilemask[,enable])``
+
+ Sets all bits in the mask to the *enable* argument.
+
Burrows module
--------------
diff --git a/dfhack.init-example b/dfhack.init-example
index 8505f5acc..3ecbf4342 100644
--- a/dfhack.init-example
+++ b/dfhack.init-example
@@ -72,6 +72,9 @@ keybinding add Alt-A@dwarfmode/QueryBuilding/Some/SiegeEngine gui/siege-engine
# military weapon auto-select
keybinding add Ctrl-W@layer_military/Equip/Customize/View gui/choose-weapons
+# minecart Guide path
+keybinding add Alt-P@dwarfmode/Hauling/DefineStop/Cond/Guide gui/guide-path
+
############################
# UI and game logic tweaks #
############################
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 0df96f066..ef571bcb7 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -30,6 +30,7 @@ distribution.
#include "MemAccess.h"
#include "Core.h"
+#include "Error.h"
#include "VersionInfo.h"
#include "tinythread.h"
// must be last due to MS stupidity
@@ -311,12 +312,12 @@ static PersistentDataItem get_persistent(lua_State *state)
if (lua_istable(state, 1))
{
+ Lua::StackUnwinder frame(state);
+
if (!lua_getmetatable(state, 1) ||
!lua_rawequal(state, -1, lua_upvalueindex(1)))
luaL_argerror(state, 1, "invalid table type");
- lua_settop(state, 1);
-
return persistent_by_struct(state, 1);
}
else
@@ -446,11 +447,38 @@ static int dfhack_persistent_save(lua_State *state)
return 2;
}
+static int dfhack_persistent_getTilemask(lua_State *state)
+{
+ CoreSuspender suspend;
+
+ lua_settop(state, 3);
+ auto ref = get_persistent(state);
+ auto block = Lua::CheckDFObject(state, 2);
+ bool create = lua_toboolean(state, 3);
+
+ Lua::PushDFObject(state, World::getPersistentTilemask(ref, block, create));
+ return 1;
+}
+
+static int dfhack_persistent_deleteTilemask(lua_State *state)
+{
+ CoreSuspender suspend;
+
+ lua_settop(state, 2);
+ auto ref = get_persistent(state);
+ auto block = Lua::CheckDFObject(state, 2);
+
+ lua_pushboolean(state, World::deletePersistentTilemask(ref, block));
+ return 1;
+}
+
static const luaL_Reg dfhack_persistent_funcs[] = {
{ "get", dfhack_persistent_get },
{ "delete", dfhack_persistent_delete },
{ "get_all", dfhack_persistent_get_all },
{ "save", dfhack_persistent_save },
+ { "getTilemask", dfhack_persistent_getTilemask },
+ { "deleteTilemask", dfhack_persistent_deleteTilemask },
{ NULL, NULL }
};
@@ -937,6 +965,22 @@ static const luaL_Reg dfhack_items_funcs[] = {
/***** Maps module *****/
+static bool hasTileAssignment(df::tile_bitmask *bm) {
+ return bm && bm->has_assignments();
+}
+static bool getTileAssignment(df::tile_bitmask *bm, int x, int y) {
+ return bm && bm->getassignment(x,y);
+}
+static void setTileAssignment(df::tile_bitmask *bm, int x, int y, bool val) {
+ CHECK_NULL_POINTER(bm);
+ bm->setassignment(x,y,val);
+}
+static void resetTileAssignment(df::tile_bitmask *bm, bool val) {
+ CHECK_NULL_POINTER(bm);
+ if (val) bm->set_all();
+ else bm->clear();
+}
+
static const LuaWrapper::FunctionReg dfhack_maps_module[] = {
WRAPN(getBlock, (df::map_block* (*)(int32_t,int32_t,int32_t))Maps::getBlock),
WRAPM(Maps, enableBlockUpdates),
@@ -944,6 +988,10 @@ static const LuaWrapper::FunctionReg dfhack_maps_module[] = {
WRAPM(Maps, getLocalInitFeature),
WRAPM(Maps, canWalkBetween),
WRAPM(Maps, spawnFlow),
+ WRAPN(hasTileAssignment, hasTileAssignment),
+ WRAPN(getTileAssignment, getTileAssignment),
+ WRAPN(setTileAssignment, setTileAssignment),
+ WRAPN(resetTileAssignment, resetTileAssignment),
{ NULL, NULL }
};
diff --git a/library/VTableInterpose.cpp b/library/VTableInterpose.cpp
index 141450615..d8a07e830 100644
--- a/library/VTableInterpose.cpp
+++ b/library/VTableInterpose.cpp
@@ -281,9 +281,10 @@ VMethodInterposeLinkBase *VMethodInterposeLinkBase::get_first_interpose(virtual_
return item;
}
-void VMethodInterposeLinkBase::find_child_hosts(virtual_identity *cur, void *vmptr)
+bool VMethodInterposeLinkBase::find_child_hosts(virtual_identity *cur, void *vmptr)
{
auto &children = cur->getChildren();
+ bool found = false;
for (size_t i = 0; i < children.size(); i++)
{
@@ -298,17 +299,32 @@ void VMethodInterposeLinkBase::find_child_hosts(virtual_identity *cur, void *vmp
continue;
child_next.insert(base);
+ found = true;
}
- else
+ else if (child->vtable_ptr)
{
void *cptr = child->get_vmethod_ptr(vmethod_idx);
if (cptr != vmptr)
continue;
child_hosts.insert(child);
+ found = true;
+
find_child_hosts(child, vmptr);
}
+ else
+ {
+ // If this vtable is not known, but any of the children
+ // have the same vmethod, this one definitely does too
+ if (find_child_hosts(child, vmptr))
+ {
+ child_hosts.insert(child);
+ found = true;
+ }
+ }
}
+
+ return found;
}
void VMethodInterposeLinkBase::on_host_delete(virtual_identity *from)
diff --git a/library/include/VTableInterpose.h b/library/include/VTableInterpose.h
index b52b32270..bd3c23036 100644
--- a/library/include/VTableInterpose.h
+++ b/library/include/VTableInterpose.h
@@ -159,7 +159,7 @@ namespace DFHack
void on_host_delete(virtual_identity *host);
VMethodInterposeLinkBase *get_first_interpose(virtual_identity *id);
- void find_child_hosts(virtual_identity *cur, void *vmptr);
+ bool find_child_hosts(virtual_identity *cur, void *vmptr);
public:
VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr, int priority);
~VMethodInterposeLinkBase();
diff --git a/library/include/df/custom/tile_bitmask.methods.inc b/library/include/df/custom/tile_bitmask.methods.inc
index 18b312a08..e3414e334 100644
--- a/library/include/df/custom/tile_bitmask.methods.inc
+++ b/library/include/df/custom/tile_bitmask.methods.inc
@@ -16,7 +16,7 @@ inline bool getassignment( const df::coord2d &xy )
}
inline bool getassignment( int x, int y )
{
- return (bits[y] & (1 << x));
+ return (bits[(y&15)] & (1 << (x&15)));
}
inline void setassignment( const df::coord2d &xy, bool bit )
{
@@ -25,9 +25,9 @@ inline void setassignment( const df::coord2d &xy, bool bit )
inline void setassignment( int x, int y, bool bit )
{
if(bit)
- bits[y] |= (1 << x);
+ bits[(y&15)] |= (1 << (x&15));
else
- bits[y] &= ~(1 << x);
+ bits[(y&15)] &= ~(1 << (x&15));
}
bool has_assignments()
{
diff --git a/library/include/modules/World.h b/library/include/modules/World.h
index f4c31dcf3..a945c4e72 100644
--- a/library/include/modules/World.h
+++ b/library/include/modules/World.h
@@ -37,6 +37,12 @@ distribution.
#include "DataDefs.h"
+namespace df
+{
+ struct tile_bitmask;
+ struct map_block;
+}
+
namespace DFHack
{
typedef df::game_mode GameMode;
@@ -120,6 +126,9 @@ namespace DFHack
DFHACK_EXPORT bool DeletePersistentData(const PersistentDataItem &item);
DFHACK_EXPORT void ClearPersistentCache();
+
+ DFHACK_EXPORT df::tile_bitmask *getPersistentTilemask(const PersistentDataItem &item, df::map_block *block, bool create = false);
+ DFHACK_EXPORT bool deletePersistentTilemask(const PersistentDataItem &item, df::map_block *block);
}
}
#endif
diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp
index b211f9f2b..04a453c06 100644
--- a/library/modules/Gui.cpp
+++ b/library/modules/Gui.cpp
@@ -85,6 +85,8 @@ using namespace DFHack;
#include "df/assign_trade_status.h"
#include "df/announcement_flags.h"
#include "df/announcements.h"
+#include "df/stop_depart_condition.h"
+#include "df/route_stockpile_link.h"
using namespace df::enums;
using df::global::gview;
@@ -303,6 +305,45 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
focus += "/List";
break;
+ case Hauling:
+ if (ui->hauling.in_assign_vehicle)
+ {
+ auto vehicle = vector_get(ui->hauling.vehicles, ui->hauling.cursor_vehicle);
+ focus += "/AssignVehicle/" + std::string(vehicle ? "Some" : "None");
+ }
+ else
+ {
+ int idx = ui->hauling.cursor_top;
+ auto route = vector_get(ui->hauling.view_routes, idx);
+ auto stop = vector_get(ui->hauling.view_stops, idx);
+ std::string tag = stop ? "Stop" : (route ? "Route" : "None");
+
+ if (ui->hauling.in_name)
+ focus += "/Rename/" + tag;
+ else if (ui->hauling.in_stop)
+ {
+ int sidx = ui->hauling.cursor_stop;
+ auto cond = vector_get(ui->hauling.stop_conditions, sidx);
+ auto link = vector_get(ui->hauling.stop_links, sidx);
+
+ focus += "/DefineStop";
+
+ if (cond)
+ focus += "/Cond/" + enum_item_key(cond->mode);
+ else if (link)
+ {
+ focus += "/Link/";
+ if (link->mode.bits.give) focus += "Give";
+ if (link->mode.bits.take) focus += "Take";
+ }
+ else
+ focus += "/None";
+ }
+ else
+ focus += "/Select/" + tag;
+ }
+ break;
+
default:
break;
}
diff --git a/library/modules/World.cpp b/library/modules/World.cpp
index 65d0c7cb8..f3283c3a3 100644
--- a/library/modules/World.cpp
+++ b/library/modules/World.cpp
@@ -38,13 +38,18 @@ using namespace std;
#include "ModuleFactory.h"
#include "Core.h"
+#include "modules/Maps.h"
+
#include "MiscUtils.h"
#include "DataDefs.h"
#include "df/world.h"
#include "df/historical_figure.h"
+#include "df/map_block.h"
+#include "df/block_square_event_world_constructionst.h"
using namespace DFHack;
+using namespace df::enums;
using df::global::world;
@@ -312,3 +317,63 @@ bool World::DeletePersistentData(const PersistentDataItem &item)
return false;
}
+
+df::tile_bitmask *World::getPersistentTilemask(const PersistentDataItem &item, df::map_block *block, bool create)
+{
+ if (!block)
+ return NULL;
+
+ int id = item.raw_id();
+ if (id > -100)
+ return NULL;
+
+ for (size_t i = 0; i < block->block_events.size(); i++)
+ {
+ auto ev = block->block_events[i];
+ if (ev->getType() != block_square_event_type::world_construction)
+ continue;
+ auto wcsev = strict_virtual_cast(ev);
+ if (!wcsev || wcsev->construction_id != id)
+ continue;
+ return &wcsev->tile_bitmask;
+ }
+
+ if (!create)
+ return NULL;
+
+ auto ev = df::allocate();
+ if (!ev)
+ return NULL;
+
+ ev->construction_id = id;
+ ev->tile_bitmask.clear();
+ vector_insert_at(block->block_events, 0, (df::block_square_event*)ev);
+
+ return &ev->tile_bitmask;
+}
+
+bool World::deletePersistentTilemask(const PersistentDataItem &item, df::map_block *block)
+{
+ if (!block)
+ return false;
+ int id = item.raw_id();
+ if (id > -100)
+ return false;
+
+ bool found = false;
+ for (int i = block->block_events.size()-1; i >= 0; i--)
+ {
+ auto ev = block->block_events[i];
+ if (ev->getType() != block_square_event_type::world_construction)
+ continue;
+ auto wcsev = strict_virtual_cast(ev);
+ if (!wcsev || wcsev->construction_id != id)
+ continue;
+
+ delete wcsev;
+ vector_erase_at(block->block_events, i);
+ found = true;
+ }
+
+ return found;
+}
diff --git a/library/xml b/library/xml
index a5a8fd689..aec106cdc 160000
--- a/library/xml
+++ b/library/xml
@@ -1 +1 @@
-Subproject commit a5a8fd68947b60fcb2d1c03b4f05bdf48cfcf7a5
+Subproject commit aec106cdc32083bbcc6d6dd27d9e069f5525ea92
diff --git a/scripts/gui/guide-path.lua b/scripts/gui/guide-path.lua
new file mode 100644
index 000000000..f3e27d917
--- /dev/null
+++ b/scripts/gui/guide-path.lua
@@ -0,0 +1,201 @@
+-- Show and manipulate the path used by Guide Cart orders.
+
+local utils = require 'utils'
+local gui = require 'gui'
+local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
+
+local tile_attrs = df.tiletype.attrs
+
+function same_xyz(a,b)
+ return a and b and a.x == b.x and a.y == b.y and a.z == b.z
+end
+
+GuidePathUI = defclass(GuidePathUI, guidm.MenuOverlay)
+
+GuidePathUI.focus_path = 'guide-path'
+
+GuidePathUI.ATTRS {
+ route = DEFAULT_NIL,
+ stop = DEFAULT_NIL,
+ order = DEFAULT_NIL,
+}
+
+function GuidePathUI:init()
+ self.saved_mode = df.global.ui.main.mode
+
+ for i=0,#self.route.stops-1 do
+ if self.route.stops[i] == self.stop then
+ self.stop_idx = i
+ break
+ end
+ end
+
+ if not self.stop_idx then
+ error('Could not find stop index')
+ end
+
+ self.next_stop = self.route.stops[(self.stop_idx+1)%#self.route.stops]
+end
+
+function GuidePathUI:onShow()
+ -- with cursor, but without those ugly lines from native hauling mode
+ df.global.ui.main.mode = df.ui_sidebar_mode.Stockpiles
+end
+
+function GuidePathUI:onDestroy()
+ df.global.ui.main.mode = self.saved_mode
+end
+
+local function getTileType(cursor)
+ local block = dfhack.maps.getTileBlock(cursor)
+ if block then
+ return block.tiletype[cursor.x%16][cursor.y%16]
+ else
+ return 0
+ end
+end
+
+local function isTrackTile(tile)
+ return tile_attrs[tile].special == df.tiletype_special.TRACK
+end
+
+local function paintMapTile(dc, vp, cursor, pos, ...)
+ if not same_xyz(cursor, pos) then
+ local stile = vp:tileToScreen(pos)
+ if stile.z == 0 then
+ dc:seek(stile.x,stile.y):char(...)
+ end
+ end
+end
+
+local function get_path_point(gpath,i)
+ return xyz2pos(gpath.x[i], gpath.y[i], gpath.z[i])
+end
+
+local function blink_visible(delay)
+ return math.floor(dfhack.getTickCount()/delay) % 2 == 0
+end
+
+function GuidePathUI:renderPath(cursor)
+ local gpath = self.order.guide_path
+ local startp = self.stop.pos
+ local endp = self.next_stop.pos
+ local vp = self:getViewport()
+ local dc = gui.Painter.new(self.df_layout.map)
+ local visible = blink_visible(500)
+
+ if visible then
+ paintMapTile(dc, vp, cursor, endp, '+', COLOR_LIGHTGREEN)
+ end
+
+ local ok = nil
+ local pcnt = #gpath.x
+ if pcnt > 0 then
+ ok = true
+
+ for i = 0,pcnt-1 do
+ local pt = get_path_point(gpath, i)
+ if i == 0 and not same_xyz(startp,pt) then
+ ok = false
+ end
+ if i == pcnt-1 and not same_xyz(endp,pt) then
+ ok = false
+ end
+ local tile = getTileType(pt)
+ if not isTrackTile(tile) then
+ ok = false
+ end
+ if visible then
+ local char = '+'
+ if i < pcnt-1 then
+ local npt = get_path_point(gpath, i+1)
+ if npt.x == pt.x+1 then
+ char = 26
+ elseif npt.x == pt.x-1 then
+ char = 27
+ elseif npt.y == pt.y+1 then
+ char = 25
+ elseif npt.y == pt.y-1 then
+ char = 24
+ end
+ end
+ local color = COLOR_LIGHTGREEN
+ if not ok then color = COLOR_LIGHTRED end
+ paintMapTile(dc, vp, cursor, pt, char, color)
+ end
+ end
+ end
+
+ if blink_visible(120) then
+ paintMapTile(dc, vp, cursor, startp, 240, COLOR_LIGHTGREEN, COLOR_GREEN)
+ end
+
+ return ok
+end
+
+function GuidePathUI:onRenderBody(dc)
+ dc:clear():seek(1,1):pen(COLOR_WHITE):string("Guide Path")
+
+ dc:seek(2,3)
+
+ local cursor = guidm.getCursorPos()
+ local path_ok = self:renderPath(cursor)
+
+ if path_ok == nil then
+ dc:string('No saved path', COLOR_DARKGREY)
+ elseif path_ok then
+ dc:string('Valid path', COLOR_GREEN)
+ else
+ dc:string('Invalid path', COLOR_RED)
+ end
+
+ dc:newline():newline(1)
+ dc:key('CUSTOM_Z'):string(": Reset path, ",COLOR_GREY,nil,path_ok~=nil)
+ dc:key('CUSTOM_P'):string(": Find path",COLOR_GREY,nil,false)
+ dc:newline(1)
+ dc:key('CUSTOM_C'):string(": Zoom cur, ")
+ dc:key('CUSTOM_N'):string(": Zoom next")
+
+ dc:newline():newline(1):string('At cursor:')
+ dc:newline():newline(2)
+
+ local tile = getTileType(cursor)
+ if isTrackTile(tile) then
+ dc:string('Track '..tile_attrs[tile].direction, COLOR_GREEN)
+ else
+ dc:string('No track', COLOR_DARKGREY)
+ end
+
+ dc:newline():newline(1)
+ dc:key('LEAVESCREEN'):string(": Back")
+end
+
+function GuidePathUI:onInput(keys)
+ if keys.CUSTOM_C then
+ self:moveCursorTo(copyall(self.stop.pos))
+ elseif keys.CUSTOM_N then
+ self:moveCursorTo(copyall(self.next_stop.pos))
+ elseif keys.CUSTOM_Z then
+ local gpath = self.order.guide_path
+ gpath.x:resize(0)
+ gpath.y:resize(0)
+ gpath.z:resize(0)
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+ elseif self:propagateMoveKeys(keys) then
+ return
+ end
+end
+
+if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/Hauling/DefineStop/Cond/Guide') then
+ qerror("This script requires the main dwarfmode view in 'h' mode over a Guide order")
+end
+
+local hauling = df.global.ui.hauling
+local route = hauling.view_routes[hauling.cursor_top]
+local stop = hauling.view_stops[hauling.cursor_top]
+local order = hauling.stop_conditions[hauling.cursor_stop]
+
+local list = GuidePathUI{ route = route, stop = stop, order = order }
+list:show()