-
+
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/depends/protobuf/CMakeLists.txt b/depends/protobuf/CMakeLists.txt
index 24c4b275a..5034f00f4 100644
--- a/depends/protobuf/CMakeLists.txt
+++ b/depends/protobuf/CMakeLists.txt
@@ -7,10 +7,10 @@ IF(CMAKE_COMPILER_IS_GNUCC)
STRING(REGEX MATCHALL "[0-9]+" GCC_VERSION_COMPONENTS ${GCC_VERSION})
LIST(GET GCC_VERSION_COMPONENTS 0 GCC_MAJOR)
LIST(GET GCC_VERSION_COMPONENTS 1 GCC_MINOR)
- IF(GCC_MAJOR LESS 4 OR (GCC_MAJOR EQUAL 4 AND GCC_MINOR LESS 2))
+ #IF(GCC_MAJOR LESS 4 OR (GCC_MAJOR EQUAL 4 AND GCC_MINOR LESS 2))
#GCC is too old
- SET(STL_HASH_OLD_GCC 1)
- ENDIF()
+ # SET(STL_HASH_OLD_GCC 1)
+ #ENDIF()
#SET(CMAKE_CXX_FLAGS "-std=c++0x")
SET(HAVE_HASH_MAP 0)
diff --git a/dfhack.init-example b/dfhack.init-example
index 39c0e61df..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 #
##############################
@@ -52,6 +56,9 @@ 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 #
###################
diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt
index 109a97e7c..536f4d34d 100644
--- a/library/CMakeLists.txt
+++ b/library/CMakeLists.txt
@@ -286,6 +286,10 @@ SET_TARGET_PROPERTIES(dfhack PROPERTIES LINK_INTERFACE_LIBRARIES "")
TARGET_LINK_LIBRARIES(dfhack-client protobuf-lite clsocket)
TARGET_LINK_LIBRARIES(dfhack-run dfhack-client)
+if(APPLE)
+ add_custom_command(TARGET dfhack-run COMMAND ${dfhack_SOURCE_DIR}/package/darwin/fix-libs.sh WORKING_DIRECTORY ../ COMMENT "Fixing library dependencies...")
+endif()
+
IF(UNIX)
if (APPLE)
install(PROGRAMS ${dfhack_SOURCE_DIR}/package/darwin/dfhack
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 4e57b113f..f69fa7a1b 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -79,6 +79,8 @@ distribution.
#include "df/building_civzonest.h"
#include "df/region_map_entry.h"
#include "df/flow_info.h"
+#include "df/unit_misc_trait.h"
+#include "df/proj_itemst.h"
#include
#include
@@ -728,6 +730,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(); }
@@ -739,6 +742,7 @@ static const LuaWrapper::FunctionReg dfhack_module[] = {
WRAP(getOSType),
WRAP(getDFVersion),
WRAP(getDFPath),
+ WRAP(getTickCount),
WRAP(getHackPath),
WRAP(isWorldLoaded),
WRAP(isMapLoaded),
@@ -811,12 +815,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),
@@ -874,6 +886,12 @@ static bool items_moveToInventory
return Items::moveToInventory(mc, item, unit, mode, body_part);
}
+static df::proj_itemst *items_makeProjectile(df::item *item)
+{
+ MapExtras::MapCache mc;
+ return Items::makeProjectile(mc, item);
+}
+
static const LuaWrapper::FunctionReg dfhack_items_module[] = {
WRAPM(Items, getGeneralRef),
WRAPM(Items, getSpecificRef),
@@ -885,6 +903,7 @@ static const LuaWrapper::FunctionReg dfhack_items_module[] = {
WRAPN(moveToContainer, items_moveToContainer),
WRAPN(moveToBuilding, items_moveToBuilding),
WRAPN(moveToInventory, items_moveToInventory),
+ WRAPN(makeProjectile, items_makeProjectile),
{ NULL, NULL }
};
@@ -933,6 +952,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);
@@ -949,6 +975,7 @@ static int maps_getTileBiomeRgn(lua_State *L)
static const luaL_Reg dfhack_maps_funcs[] = {
{ "isValidTilePos", maps_isValidTilePos },
{ "getTileBlock", maps_getTileBlock },
+ { "ensureTileBlock", maps_ensureTileBlock },
{ "getRegionBiome", maps_getRegionBiome },
{ "getTileBiomeRgn", maps_getTileBiomeRgn },
{ NULL, NULL }
@@ -1144,6 +1171,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;
@@ -1248,6 +1314,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/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/RemoteClient.cpp b/library/RemoteClient.cpp
index 4d30988c6..09861ad5f 100644
--- a/library/RemoteClient.cpp
+++ b/library/RemoteClient.cpp
@@ -394,7 +394,7 @@ command_result RemoteFunctionBase::execute(color_ostream &out,
//out.print("Received %d:%d\n", header.id, header.size);
- if (header.id == RPC_REPLY_FAIL)
+ if ((DFHack::DFHackReplyCode)header.id == RPC_REPLY_FAIL)
return header.size == CR_OK ? CR_FAILURE : command_result(header.size);
if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE)
diff --git a/library/RemoteServer.cpp b/library/RemoteServer.cpp
index 53428f2bd..06a9f859c 100644
--- a/library/RemoteServer.cpp
+++ b/library/RemoteServer.cpp
@@ -250,7 +250,7 @@ void ServerConnection::threadFn()
break;
}
- if (header.id == RPC_REQUEST_QUIT)
+ if ((DFHack::DFHackReplyCode)header.id == RPC_REQUEST_QUIT)
break;
if (header.size < 0 || header.size > RPCMessageHeader::MAX_MESSAGE_SIZE)
diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp
index 95c495e93..b371d60fa 100644
--- a/library/RemoteTools.cpp
+++ b/library/RemoteTools.cpp
@@ -287,7 +287,7 @@ void DFHack::describeUnit(BasicUnitInfo *info, df::unit *unit,
if (mask && mask->profession())
{
- if (unit->profession >= 0)
+ if (unit->profession >= (df::profession)0)
info->set_profession(unit->profession);
if (!unit->custom_profession.empty())
info->set_custom_profession(unit->custom_profession);
diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h
index 591a0c3ff..61d5dec41 100644
--- a/library/include/DataDefs.h
+++ b/library/include/DataDefs.h
@@ -518,7 +518,7 @@ namespace DFHack {
template
inline const char *enum_item_raw_key(T val) {
typedef df::enum_traits traits;
- return traits::is_valid(val) ? traits::key_table[val - traits::first_item_value] : NULL;
+ return traits::is_valid(val) ? traits::key_table[(short)val - traits::first_item_value] : NULL;
}
/**
diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h
index 6b1afb88b..3330e23e7 100644
--- a/library/include/LuaTools.h
+++ b/library/include/LuaTools.h
@@ -287,6 +287,11 @@ namespace DFHack {namespace Lua {
PushDFObject(state, ptr);
}
+ template inline void SetField(lua_State *L, T val, int idx, const char *name) {
+ if (idx < 0) idx = lua_absindex(L, idx);
+ Push(L, val); lua_setfield(L, idx, name);
+ }
+
template
void PushVector(lua_State *state, const T &pvec, bool addn = false)
{
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/RemoteTools.h b/library/include/RemoteTools.h
index 65884badc..e87e8026b 100644
--- a/library/include/RemoteTools.h
+++ b/library/include/RemoteTools.h
@@ -88,7 +88,7 @@ namespace DFHack
{
typedef df::enum_traits traits;
int base = traits::first_item;
- int size = traits::last_item - base + 1;
+ int size = (int)traits::last_item - base + 1;
describeEnum(pf, base, size, traits::key_table);
}
diff --git a/library/include/modules/Items.h b/library/include/modules/Items.h
index 4236f068a..7493d22fc 100644
--- a/library/include/modules/Items.h
+++ b/library/include/modules/Items.h
@@ -44,6 +44,7 @@ distribution.
namespace df
{
struct itemdef;
+ struct proj_itemst;
}
namespace MapExtras {
@@ -155,5 +156,8 @@ DFHACK_EXPORT bool moveToContainer(MapExtras::MapCache &mc, df::item *item, df::
DFHACK_EXPORT bool moveToBuilding(MapExtras::MapCache &mc, df::item *item, df::building_actual *building,int16_t use_mode);
DFHACK_EXPORT bool moveToInventory(MapExtras::MapCache &mc, df::item *item, df::unit *unit,
df::unit_inventory_item::T_mode mode = df::unit_inventory_item::Carried, int body_part = -1);
+
+/// Detaches the items from its current location and turns it into a projectile
+DFHACK_EXPORT df::proj_itemst *makeProjectile(MapExtras::MapCache &mc, df::item *item);
}
}
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 984cf16cf..869b21580 100644
--- a/library/include/modules/Maps.h
+++ b/library/include/modules/Maps.h
@@ -241,9 +241,11 @@ inline bool isValidTilePos(df::coord pos) { return isValidTilePos(pos.x, pos.y,
*/
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);
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 9e189ea13..f9b6ab6d2 100644
--- a/library/lua/gui.lua
+++ b/library/lua/gui.lua
@@ -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
@@ -353,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 1f7ae1b03..661e15591 100644
--- a/library/lua/gui/dwarfmode.lua
+++ b/library/lua/gui/dwarfmode.lua
@@ -46,7 +46,7 @@ function getPanelLayout()
end
function getCursorPos()
- if g_cursor ~= -30000 then
+ if g_cursor.x ~= -30000 then
return copyall(g_cursor)
end
end
@@ -136,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)),
@@ -159,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),
@@ -207,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
@@ -237,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: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)
+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)
@@ -282,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/Gui.cpp b/library/modules/Gui.cpp
index 1ea4bf687..91df14eaf 100644
--- a/library/modules/Gui.cpp
+++ b/library/modules/Gui.cpp
@@ -173,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 &&
@@ -189,6 +188,8 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
focus += unit ? "/Unit" : "/None";
}
}
+ else
+ focus += "/" + enum_item_key(selected->getType());
if (jobs)
{
@@ -211,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";
diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp
index dc64143c9..751797f06 100644
--- a/library/modules/Items.cpp
+++ b/library/modules/Items.cpp
@@ -72,8 +72,11 @@ using namespace std;
#include "df/general_ref_contains_itemst.h"
#include "df/general_ref_contained_in_itemst.h"
#include "df/general_ref_building_holderst.h"
+#include "df/general_ref_projectile.h"
#include "df/viewscreen_itemst.h"
#include "df/vermin.h"
+#include "df/proj_itemst.h"
+#include "df/proj_list_link.h"
#include "df/unit_inventory_item.h"
#include "df/body_part_raw.h"
@@ -88,6 +91,7 @@ using namespace df::enums;
using df::global::world;
using df::global::ui;
using df::global::ui_selected_unit;
+using df::global::proj_next_id;
#define ITEMDEF_VECTORS \
ITEM(WEAPON, weapons, itemdef_weaponst) \
@@ -866,3 +870,44 @@ bool DFHack::Items::moveToInventory(
return true;
}
+
+df::proj_itemst *Items::makeProjectile(MapExtras::MapCache &mc, df::item *item)
+{
+ CHECK_NULL_POINTER(item);
+
+ if (!world || !proj_next_id)
+ return NULL;
+
+ auto pos = getPosition(item);
+ if (!pos.isValid())
+ return NULL;
+
+ auto ref = df::allocate();
+ if (!ref)
+ return NULL;
+
+ if (!detachItem(mc, item))
+ {
+ delete ref;
+ return NULL;
+ }
+
+ item->pos = pos;
+ item->flags.bits.in_job = true;
+
+ auto proj = new df::proj_itemst();
+ proj->link = new df::proj_list_link();
+ proj->link->item = proj;
+ proj->id = (*proj_next_id)++;
+
+ proj->origin_pos = proj->target_pos = pos;
+ proj->cur_pos = proj->prev_pos = pos;
+ proj->item = item;
+
+ ref->projectile_id = proj->id;
+ item->itemrefs.push_back(ref);
+
+ linked_list_append(&world->proj_list, proj->link);
+
+ return proj;
+}
diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp
index 54b4eb27e..b74a4b73f 100644
--- a/library/modules/Job.cpp
+++ b/library/modules/Job.cpp
@@ -181,7 +181,7 @@ void DFHack::Job::printItemDetails(color_ostream &out, df::job_item *item, int i
out << " reaction class: " << item->reaction_class << endl;
if (!item->has_material_reaction_product.empty())
out << " reaction product: " << item->has_material_reaction_product << endl;
- if (item->has_tool_use >= 0)
+ if (item->has_tool_use >= (df::tool_uses)0)
out << " tool use: " << ENUM_KEY_STR(tool_uses, item->has_tool_use) << endl;
}
diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp
index 305f1296d..f1f40f19c 100644
--- a/library/modules/Maps.cpp
+++ b/library/modules/Maps.cpp
@@ -157,6 +157,39 @@ df::map_block *Maps::getTileBlock (int32_t x, int32_t y, int32_t z)
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);
@@ -274,7 +307,7 @@ df::feature_init *Maps::getLocalInitFeature(df::coord2d rgn_pos, int32_t index)
df::coord2d bigregion = rgn_pos / 16;
// bigregion is 16x16 regions. for each bigregion in X dimension:
- auto fptr = data->unk_204[bigregion.x][bigregion.y].features;
+ auto fptr = data->feature_map[bigregion.x][bigregion.y].features;
if (!fptr)
return NULL;
@@ -513,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;
@@ -537,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 874dabc3d..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)
@@ -838,7 +1311,7 @@ std::string DFHack::Units::getCasteProfessionName(int race, int casteid, df::pro
{
std::string prof, race_prefix;
- if (pid < 0 || !is_valid_enum_item(pid))
+ if (pid < (df::profession)0 || !is_valid_enum_item(pid))
return "";
bool use_race_prefix = (race >= 0 && race != df::global::ui->race_id);
diff --git a/library/modules/World.cpp b/library/modules/World.cpp
index 393e7cbfe..67b8c1236 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);
}
@@ -300,6 +300,8 @@ PersistentDataItem World::GetPersistentData(const std::string &key, bool *added)
void World::GetPersistentData(std::vector *vec, const std::string &key, bool prefix)
{
+ vec->clear();
+
if (!BuildPersistentCache())
return;
@@ -343,8 +345,10 @@ bool World::DeletePersistentData(const PersistentDataItem &item)
auto eqrange = d->persistent_index.equal_range(item.key_value);
- for (auto it = eqrange.first; it != eqrange.second; ++it)
+ for (auto it2 = eqrange.first; it2 != eqrange.second; )
{
+ auto it = it2; ++it2;
+
if (it->second != -item.id)
continue;
diff --git a/library/modules/kitchen.cpp b/library/modules/kitchen.cpp
index 4300d63df..aa235780d 100644
--- a/library/modules/kitchen.cpp
+++ b/library/modules/kitchen.cpp
@@ -114,7 +114,7 @@ void Kitchen::fillWatchMap(std::map& watchMap)
watchMap.clear();
for(std::size_t i = 0; i < size(); ++i)
{
- if(ui->kitchen.item_subtypes[i] == limitType && ui->kitchen.item_subtypes[i] == limitSubtype && ui->kitchen.exc_types[i] == limitExclusion)
+ if(ui->kitchen.item_subtypes[i] == (short)limitType && ui->kitchen.item_subtypes[i] == (short)limitSubtype && ui->kitchen.exc_types[i] == limitExclusion)
{
watchMap[ui->kitchen.mat_indices[i]] = (unsigned int) ui->kitchen.mat_types[i];
}
diff --git a/library/xml b/library/xml
index df8178a98..2bc8fbdf7 160000
--- a/library/xml
+++ b/library/xml
@@ -1 +1 @@
-Subproject commit df8178a989373ec7868d9195d82ae5f85145ef81
+Subproject commit 2bc8fbdf71143398817d31e06e169a01cce37c50
diff --git a/package/darwin/dfhack-run b/package/darwin/dfhack-run
index 865c8bd21..cc69db964 100755
--- a/package/darwin/dfhack-run
+++ b/package/darwin/dfhack-run
@@ -3,7 +3,6 @@
DF_DIR=$(dirname "$0")
cd "${DF_DIR}"
-export DYLD_LIBRARY_PATH=${PWD}/hack:${PWD}/libs
-export DYLD_FRAMEWORK_PATH=${PWD}/hack${PWD}/libs
+export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"./stonesense/deplibs":"./hack"
exec hack/dfhack-run "$@"
diff --git a/package/darwin/fix-libs.sh b/package/darwin/fix-libs.sh
new file mode 100755
index 000000000..cff98b6a6
--- /dev/null
+++ b/package/darwin/fix-libs.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+BUILD_DIR=`pwd`
+
+echo "Fixing library dependencies in $BUILD_DIR/library"
+
+install_name_tool -change $BUILD_DIR/library/libdfhack.1.0.0.dylib @executable_path/hack/libdfhack.1.0.0.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change $BUILD_DIR/library/libdfhack-client.dylib @executable_path/hack/libdfhack-client.dylib library/libdfhack-client.dylib
+install_name_tool -change $BUILD_DIR/library/libdfhack-client.dylib @executable_path/hack/libdfhack-client.dylib library/dfhack-run
+install_name_tool -change $BUILD_DIR/depends/protobuf/libprotobuf-lite.dylib @executable_path/hack/libprotobuf-lite.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change $BUILD_DIR/depends/protobuf/libprotobuf-lite.dylib @executable_path/hack/libprotobuf-lite.dylib library/libdfhack-client.dylib
+install_name_tool -change $BUILD_DIR/depends/protobuf/libprotobuf-lite.dylib @executable_path/hack/libprotobuf-lite.dylib library/dfhack-run
+install_name_tool -change $BUILD_DIR/depends/lua/liblua.dylib @executable_path/hack/liblua.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change @executable_path/../Frameworks/SDL.framework/Versions/A/SDL @executable_path/libs/SDL.framework/Versions/A/SDL library/libdfhack.1.0.0.dylib
+install_name_tool -change /usr/local/lib/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change /opt/local/lib/i386/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change /opt/local/lib/i386/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/libdfhack-client.dylib
+install_name_tool -change /opt/local/lib/i386/libstdc++.6.dylib @executable_path/libs/libstdc++.6.dylib library/dfhack-run
+install_name_tool -change /opt/local/lib/i386/libgcc_s.1.dylib @executable_path/libs/libgcc_s.1.dylib library/libdfhack.1.0.0.dylib
+install_name_tool -change /opt/local/lib/i386/libgcc_s.1.dylib @executable_path/libs/libgcc_s.1.dylib library/libdfhack-client.dylib
+install_name_tool -change /opt/local/lib/i386/libgcc_s.1.dylib @executable_path/libs/libgcc_s.1.dylib library/dfhack-run
\ No newline at end of file
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index a2e520178..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)
@@ -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/autolabor.cpp b/plugins/autolabor.cpp
index c3a2b313e..c39b126c9 100644
--- a/plugins/autolabor.cpp
+++ b/plugins/autolabor.cpp
@@ -9,6 +9,7 @@
#include
#include
+#include "modules/Units.h"
#include "modules/World.h"
// DF data structure definition headers
@@ -358,11 +359,11 @@ static const dwarf_state dwarf_states[] = {
OTHER /* DrinkBlood */,
OTHER /* ReportCrime */,
OTHER /* ExecuteCriminal */,
- BUSY /* TrainAnimal */,
- BUSY /* CarveTrack */,
- BUSY /* PushTrackVehicle */,
- BUSY /* PlaceTrackVehicle */,
- BUSY /* StoreItemInVehicle */
+ BUSY /* TrainAnimal */,
+ BUSY /* CarveTrack */,
+ BUSY /* PushTrackVehicle */,
+ BUSY /* PlaceTrackVehicle */,
+ BUSY /* StoreItemInVehicle */
};
struct labor_info
@@ -397,108 +398,108 @@ static int hauler_pct = 33;
static std::vector labor_infos;
static const struct labor_default default_labor_infos[] = {
- /* MINE */ {AUTOMATIC, true, 2, 200, 0},
- /* HAUL_STONE */ {HAULERS, false, 1, 200, 0},
- /* HAUL_WOOD */ {HAULERS, false, 1, 200, 0},
- /* HAUL_BODY */ {HAULERS, false, 1, 200, 0},
- /* HAUL_FOOD */ {HAULERS, false, 1, 200, 0},
- /* HAUL_REFUSE */ {HAULERS, false, 1, 200, 0},
- /* HAUL_ITEM */ {HAULERS, false, 1, 200, 0},
- /* HAUL_FURNITURE */ {HAULERS, false, 1, 200, 0},
- /* HAUL_ANIMAL */ {HAULERS, false, 1, 200, 0},
- /* CLEAN */ {HAULERS, false, 1, 200, 0},
- /* CUTWOOD */ {AUTOMATIC, true, 1, 200, 0},
- /* CARPENTER */ {AUTOMATIC, false, 1, 200, 0},
- /* DETAIL */ {AUTOMATIC, false, 1, 200, 0},
- /* MASON */ {AUTOMATIC, false, 1, 200, 0},
- /* ARCHITECT */ {AUTOMATIC, false, 1, 200, 0},
- /* ANIMALTRAIN */ {AUTOMATIC, false, 1, 200, 0},
- /* ANIMALCARE */ {AUTOMATIC, false, 1, 200, 0},
- /* DIAGNOSE */ {AUTOMATIC, false, 1, 200, 0},
- /* SURGERY */ {AUTOMATIC, false, 1, 200, 0},
- /* BONE_SETTING */ {AUTOMATIC, false, 1, 200, 0},
- /* SUTURING */ {AUTOMATIC, false, 1, 200, 0},
- /* DRESSING_WOUNDS */ {AUTOMATIC, false, 1, 200, 0},
- /* FEED_WATER_CIVILIANS */ {AUTOMATIC, false, 200, 200, 0},
- /* RECOVER_WOUNDED */ {HAULERS, false, 1, 200, 0},
- /* BUTCHER */ {AUTOMATIC, false, 1, 200, 0},
- /* TRAPPER */ {AUTOMATIC, false, 1, 200, 0},
- /* DISSECT_VERMIN */ {AUTOMATIC, false, 1, 200, 0},
- /* LEATHER */ {AUTOMATIC, false, 1, 200, 0},
- /* TANNER */ {AUTOMATIC, false, 1, 200, 0},
- /* BREWER */ {AUTOMATIC, false, 1, 200, 0},
- /* ALCHEMIST */ {AUTOMATIC, false, 1, 200, 0},
- /* SOAP_MAKER */ {AUTOMATIC, false, 1, 200, 0},
- /* WEAVER */ {AUTOMATIC, false, 1, 200, 0},
- /* CLOTHESMAKER */ {AUTOMATIC, false, 1, 200, 0},
- /* MILLER */ {AUTOMATIC, false, 1, 200, 0},
- /* PROCESS_PLANT */ {AUTOMATIC, false, 1, 200, 0},
- /* MAKE_CHEESE */ {AUTOMATIC, false, 1, 200, 0},
- /* MILK */ {AUTOMATIC, false, 1, 200, 0},
- /* COOK */ {AUTOMATIC, false, 1, 200, 0},
- /* PLANT */ {AUTOMATIC, false, 1, 200, 0},
- /* HERBALIST */ {AUTOMATIC, false, 1, 200, 0},
- /* FISH */ {AUTOMATIC, false, 1, 1, 0},
- /* CLEAN_FISH */ {AUTOMATIC, false, 1, 200, 0},
- /* DISSECT_FISH */ {AUTOMATIC, false, 1, 200, 0},
- /* HUNT */ {AUTOMATIC, true, 1, 1, 0},
- /* SMELT */ {AUTOMATIC, false, 1, 200, 0},
- /* FORGE_WEAPON */ {AUTOMATIC, false, 1, 200, 0},
- /* FORGE_ARMOR */ {AUTOMATIC, false, 1, 200, 0},
- /* FORGE_FURNITURE */ {AUTOMATIC, false, 1, 200, 0},
- /* METAL_CRAFT */ {AUTOMATIC, false, 1, 200, 0},
- /* CUT_GEM */ {AUTOMATIC, false, 1, 200, 0},
- /* ENCRUST_GEM */ {AUTOMATIC, false, 1, 200, 0},
- /* WOOD_CRAFT */ {AUTOMATIC, false, 1, 200, 0},
- /* STONE_CRAFT */ {AUTOMATIC, false, 1, 200, 0},
- /* BONE_CARVE */ {AUTOMATIC, false, 1, 200, 0},
- /* GLASSMAKER */ {AUTOMATIC, false, 1, 200, 0},
- /* EXTRACT_STRAND */ {AUTOMATIC, false, 1, 200, 0},
- /* SIEGECRAFT */ {AUTOMATIC, false, 1, 200, 0},
- /* SIEGEOPERATE */ {AUTOMATIC, false, 1, 200, 0},
- /* BOWYER */ {AUTOMATIC, false, 1, 200, 0},
- /* MECHANIC */ {AUTOMATIC, false, 1, 200, 0},
- /* POTASH_MAKING */ {AUTOMATIC, false, 1, 200, 0},
- /* LYE_MAKING */ {AUTOMATIC, false, 1, 200, 0},
- /* DYER */ {AUTOMATIC, false, 1, 200, 0},
- /* BURN_WOOD */ {AUTOMATIC, false, 1, 200, 0},
- /* OPERATE_PUMP */ {AUTOMATIC, false, 1, 200, 0},
- /* SHEARER */ {AUTOMATIC, false, 1, 200, 0},
- /* SPINNER */ {AUTOMATIC, false, 1, 200, 0},
- /* POTTERY */ {AUTOMATIC, false, 1, 200, 0},
- /* GLAZING */ {AUTOMATIC, false, 1, 200, 0},
- /* PRESSING */ {AUTOMATIC, false, 1, 200, 0},
- /* BEEKEEPING */ {AUTOMATIC, false, 1, 1, 0}, // reduce risk of stuck beekeepers (see http://www.bay12games.com/dwarves/mantisbt/view.php?id=3981)
- /* WAX_WORKING */ {AUTOMATIC, false, 1, 200, 0},
+ /* MINE */ {AUTOMATIC, true, 2, 200, 0},
+ /* HAUL_STONE */ {HAULERS, false, 1, 200, 0},
+ /* HAUL_WOOD */ {HAULERS, false, 1, 200, 0},
+ /* HAUL_BODY */ {HAULERS, false, 1, 200, 0},
+ /* HAUL_FOOD */ {HAULERS, false, 1, 200, 0},
+ /* HAUL_REFUSE */ {HAULERS, false, 1, 200, 0},
+ /* HAUL_ITEM */ {HAULERS, false, 1, 200, 0},
+ /* HAUL_FURNITURE */ {HAULERS, false, 1, 200, 0},
+ /* HAUL_ANIMAL */ {HAULERS, false, 1, 200, 0},
+ /* CLEAN */ {HAULERS, false, 1, 200, 0},
+ /* CUTWOOD */ {AUTOMATIC, true, 1, 200, 0},
+ /* CARPENTER */ {AUTOMATIC, false, 1, 200, 0},
+ /* DETAIL */ {AUTOMATIC, false, 1, 200, 0},
+ /* MASON */ {AUTOMATIC, false, 1, 200, 0},
+ /* ARCHITECT */ {AUTOMATIC, false, 1, 200, 0},
+ /* ANIMALTRAIN */ {AUTOMATIC, false, 1, 200, 0},
+ /* ANIMALCARE */ {AUTOMATIC, false, 1, 200, 0},
+ /* DIAGNOSE */ {AUTOMATIC, false, 1, 200, 0},
+ /* SURGERY */ {AUTOMATIC, false, 1, 200, 0},
+ /* BONE_SETTING */ {AUTOMATIC, false, 1, 200, 0},
+ /* SUTURING */ {AUTOMATIC, false, 1, 200, 0},
+ /* DRESSING_WOUNDS */ {AUTOMATIC, false, 1, 200, 0},
+ /* FEED_WATER_CIVILIANS */ {AUTOMATIC, false, 200, 200, 0},
+ /* RECOVER_WOUNDED */ {HAULERS, false, 1, 200, 0},
+ /* BUTCHER */ {AUTOMATIC, false, 1, 200, 0},
+ /* TRAPPER */ {AUTOMATIC, false, 1, 200, 0},
+ /* DISSECT_VERMIN */ {AUTOMATIC, false, 1, 200, 0},
+ /* LEATHER */ {AUTOMATIC, false, 1, 200, 0},
+ /* TANNER */ {AUTOMATIC, false, 1, 200, 0},
+ /* BREWER */ {AUTOMATIC, false, 1, 200, 0},
+ /* ALCHEMIST */ {AUTOMATIC, false, 1, 200, 0},
+ /* SOAP_MAKER */ {AUTOMATIC, false, 1, 200, 0},
+ /* WEAVER */ {AUTOMATIC, false, 1, 200, 0},
+ /* CLOTHESMAKER */ {AUTOMATIC, false, 1, 200, 0},
+ /* MILLER */ {AUTOMATIC, false, 1, 200, 0},
+ /* PROCESS_PLANT */ {AUTOMATIC, false, 1, 200, 0},
+ /* MAKE_CHEESE */ {AUTOMATIC, false, 1, 200, 0},
+ /* MILK */ {AUTOMATIC, false, 1, 200, 0},
+ /* COOK */ {AUTOMATIC, false, 1, 200, 0},
+ /* PLANT */ {AUTOMATIC, false, 1, 200, 0},
+ /* HERBALIST */ {AUTOMATIC, false, 1, 200, 0},
+ /* FISH */ {AUTOMATIC, false, 1, 1, 0},
+ /* CLEAN_FISH */ {AUTOMATIC, false, 1, 200, 0},
+ /* DISSECT_FISH */ {AUTOMATIC, false, 1, 200, 0},
+ /* HUNT */ {AUTOMATIC, true, 1, 1, 0},
+ /* SMELT */ {AUTOMATIC, false, 1, 200, 0},
+ /* FORGE_WEAPON */ {AUTOMATIC, false, 1, 200, 0},
+ /* FORGE_ARMOR */ {AUTOMATIC, false, 1, 200, 0},
+ /* FORGE_FURNITURE */ {AUTOMATIC, false, 1, 200, 0},
+ /* METAL_CRAFT */ {AUTOMATIC, false, 1, 200, 0},
+ /* CUT_GEM */ {AUTOMATIC, false, 1, 200, 0},
+ /* ENCRUST_GEM */ {AUTOMATIC, false, 1, 200, 0},
+ /* WOOD_CRAFT */ {AUTOMATIC, false, 1, 200, 0},
+ /* STONE_CRAFT */ {AUTOMATIC, false, 1, 200, 0},
+ /* BONE_CARVE */ {AUTOMATIC, false, 1, 200, 0},
+ /* GLASSMAKER */ {AUTOMATIC, false, 1, 200, 0},
+ /* EXTRACT_STRAND */ {AUTOMATIC, false, 1, 200, 0},
+ /* SIEGECRAFT */ {AUTOMATIC, false, 1, 200, 0},
+ /* SIEGEOPERATE */ {AUTOMATIC, false, 1, 200, 0},
+ /* BOWYER */ {AUTOMATIC, false, 1, 200, 0},
+ /* MECHANIC */ {AUTOMATIC, false, 1, 200, 0},
+ /* POTASH_MAKING */ {AUTOMATIC, false, 1, 200, 0},
+ /* LYE_MAKING */ {AUTOMATIC, false, 1, 200, 0},
+ /* DYER */ {AUTOMATIC, false, 1, 200, 0},
+ /* BURN_WOOD */ {AUTOMATIC, false, 1, 200, 0},
+ /* OPERATE_PUMP */ {AUTOMATIC, false, 1, 200, 0},
+ /* SHEARER */ {AUTOMATIC, false, 1, 200, 0},
+ /* SPINNER */ {AUTOMATIC, false, 1, 200, 0},
+ /* POTTERY */ {AUTOMATIC, false, 1, 200, 0},
+ /* GLAZING */ {AUTOMATIC, false, 1, 200, 0},
+ /* PRESSING */ {AUTOMATIC, false, 1, 200, 0},
+ /* BEEKEEPING */ {AUTOMATIC, false, 1, 1, 0}, // reduce risk of stuck beekeepers (see http://www.bay12games.com/dwarves/mantisbt/view.php?id=3981)
+ /* WAX_WORKING */ {AUTOMATIC, false, 1, 200, 0},
/* PUSH_HAUL_VEHICLES */ {HAULERS, false, 1, 200, 0}
};
static const int responsibility_penalties[] = {
- 0, /* LAW_MAKING */
- 0, /* LAW_ENFORCEMENT */
- 3000, /* RECEIVE_DIPLOMATS */
- 0, /* MEET_WORKERS */
- 1000, /* MANAGE_PRODUCTION */
- 3000, /* TRADE */
- 1000, /* ACCOUNTING */
- 0, /* ESTABLISH_COLONY_TRADE_AGREEMENTS */
- 0, /* MAKE_INTRODUCTIONS */
- 0, /* MAKE_PEACE_AGREEMENTS */
- 0, /* MAKE_TOPIC_AGREEMENTS */
- 0, /* COLLECT_TAXES */
- 0, /* ESCORT_TAX_COLLECTOR */
- 0, /* EXECUTIONS */
- 0, /* TAME_EXOTICS */
- 0, /* RELIGION */
- 0, /* ATTACK_ENEMIES */
- 0, /* PATROL_TERRITORY */
- 0, /* MILITARY_GOALS */
- 0, /* MILITARY_STRATEGY */
- 0, /* UPGRADE_SQUAD_EQUIPMENT */
- 0, /* EQUIPMENT_MANIFESTS */
- 0, /* SORT_AMMUNITION */
- 0, /* BUILD_MORALE */
- 5000 /* HEALTH_MANAGEMENT */
+ 0, /* LAW_MAKING */
+ 0, /* LAW_ENFORCEMENT */
+ 3000, /* RECEIVE_DIPLOMATS */
+ 0, /* MEET_WORKERS */
+ 1000, /* MANAGE_PRODUCTION */
+ 3000, /* TRADE */
+ 1000, /* ACCOUNTING */
+ 0, /* ESTABLISH_COLONY_TRADE_AGREEMENTS */
+ 0, /* MAKE_INTRODUCTIONS */
+ 0, /* MAKE_PEACE_AGREEMENTS */
+ 0, /* MAKE_TOPIC_AGREEMENTS */
+ 0, /* COLLECT_TAXES */
+ 0, /* ESCORT_TAX_COLLECTOR */
+ 0, /* EXECUTIONS */
+ 0, /* TAME_EXOTICS */
+ 0, /* RELIGION */
+ 0, /* ATTACK_ENEMIES */
+ 0, /* PATROL_TERRITORY */
+ 0, /* MILITARY_GOALS */
+ 0, /* MILITARY_STRATEGY */
+ 0, /* UPGRADE_SQUAD_EQUIPMENT */
+ 0, /* EQUIPMENT_MANIFESTS */
+ 0, /* SORT_AMMUNITION */
+ 0, /* BUILD_MORALE */
+ 5000 /* HEALTH_MANAGEMENT */
};
struct dwarf_info_t
@@ -537,7 +538,7 @@ static void cleanup_state()
labor_infos.clear();
}
-static void reset_labor(df::enums::unit_labor::unit_labor labor)
+static void reset_labor(df::unit_labor labor)
{
labor_infos[labor].set_minimum_dwarfs(default_labor_infos[labor].minimum_dwarfs);
labor_infos[labor].set_maximum_dwarfs(default_labor_infos[labor].maximum_dwarfs);
@@ -576,7 +577,7 @@ static void init_state()
for (auto p = items.begin(); p != items.end(); p++)
{
string key = p->key();
- df::enums::unit_labor::unit_labor labor = (df::enums::unit_labor::unit_labor) atoi(key.substr(strlen("autolabor/labors/")).c_str());
+ df::unit_labor labor = (df::unit_labor) atoi(key.substr(strlen("autolabor/labors/")).c_str());
if (labor >= 0 && labor <= labor_infos.size())
{
labor_infos[labor].config = *p;
@@ -597,7 +598,7 @@ static void init_state()
labor_infos[i].is_exclusive = default_labor_infos[i].is_exclusive;
labor_infos[i].active_dwarfs = 0;
- reset_labor((df::enums::unit_labor::unit_labor) i);
+ reset_labor((df::unit_labor) i);
}
generate_labor_to_skill_map();
@@ -611,12 +612,12 @@ static void generate_labor_to_skill_map()
// Generate labor -> skill mapping
for (int i = 0; i <= ENUM_LAST_ITEM(unit_labor); i++)
- labor_to_skill[i] = df::enums::job_skill::NONE;
+ labor_to_skill[i] = job_skill::NONE;
FOR_ENUM_ITEMS(job_skill, skill)
{
int labor = ENUM_ATTR(job_skill, labor, skill);
- if (labor != df::enums::unit_labor::NONE)
+ if (labor != unit_labor::NONE)
{
/*
assert(labor >= 0);
@@ -779,7 +780,7 @@ static void assign_labor(unit_labor::unit_labor labor,
int value = dwarf_info[dwarf].mastery_penalty;
- if (skill != df::enums::job_skill::NONE)
+ if (skill != job_skill::NONE)
{
int skill_level = 0;
int skill_experience = 0;
@@ -843,9 +844,9 @@ static void assign_labor(unit_labor::unit_labor labor,
int max_dwarfs = labor_infos[labor].maximum_dwarfs();
// Special - don't assign hunt without a butchers, or fish without a fishery
- if (df::enums::unit_labor::HUNT == labor && !has_butchers)
+ if (unit_labor::HUNT == labor && !has_butchers)
min_dwarfs = max_dwarfs = 0;
- if (df::enums::unit_labor::FISH == labor && !has_fishery)
+ if (unit_labor::FISH == labor && !has_fishery)
min_dwarfs = max_dwarfs = 0;
bool want_idle_dwarf = true;
@@ -956,15 +957,15 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
{
df::building *build = world->buildings.all[i];
auto type = build->getType();
- if (df::enums::building_type::Workshop == type)
+ if (building_type::Workshop == type)
{
- auto subType = build->getSubtype();
- if (df::enums::workshop_type::Butchers == subType)
+ df::workshop_type subType = (df::workshop_type)build->getSubtype();
+ if (workshop_type::Butchers == subType)
has_butchers = true;
- if (df::enums::workshop_type::Fishery == subType)
+ if (workshop_type::Fishery == subType)
has_fishery = true;
}
- else if (df::enums::building_type::TradeDepot == type)
+ else if (building_type::TradeDepot == type)
{
df::building_tradedepotst* depot = (df::building_tradedepotst*) build;
trader_requested = depot->trade_flags.bits.trader_requested;
@@ -978,11 +979,10 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
}
}
- for (int i = 0; i < world->units.all.size(); ++i)
+ for (int i = 0; i < world->units.active.size(); ++i)
{
- df::unit* cre = world->units.all[i];
- if (cre->race == race && cre->civ_id == civ && !cre->flags1.bits.marauder && !cre->flags1.bits.diplomat && !cre->flags1.bits.merchant &&
- !cre->flags1.bits.dead && !cre->flags1.bits.forest)
+ df::unit* cre = world->units.active[i];
+ if (Units::isCitizen(cre))
{
if (cre->burrows.size() > 0)
continue; // dwarfs assigned to burrows are skipped entirely
@@ -1003,9 +1003,6 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
{
dwarf_info[dwarf].single_labor = -1;
-// assert(dwarfs[dwarf]->status.souls.size() > 0);
-// assert fails can cause DF to crash, so don't do that
-
if (dwarfs[dwarf]->status.souls.size() <= 0)
continue;
@@ -1076,7 +1073,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
// Track total & highest skill among normal/medical skills. (We don't care about personal or social skills.)
- if (skill_class != df::enums::job_skill_class::Normal && skill_class != df::enums::job_skill_class::Medical)
+ if (skill_class != job_skill_class::Normal && skill_class != job_skill_class::Medical)
continue;
if (dwarf_info[dwarf].highest_skill < skill_level)
@@ -1093,16 +1090,11 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
dwarf_info[dwarf].mastery_penalty -= 10 * dwarf_info[dwarf].total_skill;
dwarf_info[dwarf].mastery_penalty -= dwarf_info[dwarf].noble_penalty;
- for (int labor = ENUM_FIRST_ITEM(unit_labor); labor <= ENUM_LAST_ITEM(unit_labor); labor++)
+ FOR_ENUM_ITEMS(unit_labor, labor)
{
- if (labor == df::enums::unit_labor::NONE)
+ if (labor == unit_labor::NONE)
continue;
- /*
- assert(labor >= 0);
- assert(labor < ARRAY_COUNT(labor_infos));
- */
-
if (labor_infos[labor].is_exclusive && dwarfs[dwarf]->status.labors[labor])
dwarf_info[dwarf].mastery_penalty -= 100;
}
@@ -1120,15 +1112,13 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
for (auto p = dwarfs[dwarf]->status.misc_traits.begin(); p < dwarfs[dwarf]->status.misc_traits.end(); p++)
{
- // 7 / 0x7 = Newly arrived migrant, will not work yet
- // 17 / 0x11 = On break
- if ((*p)->id == 0x07 || (*p)->id == 0x11)
+ if ((*p)->id == misc_trait_type::Migrant || (*p)->id == misc_trait_type::OnBreak)
is_on_break = true;
}
- if (dwarfs[dwarf]->profession == df::enums::profession::BABY ||
- dwarfs[dwarf]->profession == df::enums::profession::CHILD ||
- dwarfs[dwarf]->profession == df::enums::profession::DRUNK)
+ if (dwarfs[dwarf]->profession == profession::BABY ||
+ dwarfs[dwarf]->profession == profession::CHILD ||
+ dwarfs[dwarf]->profession == profession::DRUNK)
{
dwarf_info[dwarf].state = CHILD;
}
@@ -1146,18 +1136,13 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
else
{
int job = dwarfs[dwarf]->job.current_job->job_type;
-
- /*
- assert(job >= 0);
- assert(job < ARRAY_COUNT(dwarf_states));
- */
- if (job >= 0 && job < ARRAY_COUNT(dwarf_states))
- dwarf_info[dwarf].state = dwarf_states[job];
- else
- {
- out.print("Dwarf %i \"%s\" has unknown job %i\n", dwarf, dwarfs[dwarf]->name.first_name.c_str(), job);
- dwarf_info[dwarf].state = OTHER;
- }
+ if (job >= 0 && job < ARRAY_COUNT(dwarf_states))
+ dwarf_info[dwarf].state = dwarf_states[job];
+ else
+ {
+ out.print("Dwarf %i \"%s\" has unknown job %i\n", dwarf, dwarfs[dwarf]->name.first_name.c_str(), job);
+ dwarf_info[dwarf].state = OTHER;
+ }
}
state_count[dwarf_info[dwarf].state]++;
@@ -1170,14 +1155,9 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
FOR_ENUM_ITEMS(unit_labor, labor)
{
- if (labor == df::enums::unit_labor::NONE)
+ if (labor == unit_labor::NONE)
continue;
- /*
- assert(labor >= 0);
- assert(labor < ARRAY_COUNT(labor_infos));
- */
-
labor_infos[labor].active_dwarfs = 0;
labors.push_back(labor);
@@ -1217,11 +1197,6 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
{
auto labor = *lp;
- /*
- assert(labor >= 0);
- assert(labor < ARRAY_COUNT(labor_infos));
- */
-
assign_labor(labor, n_dwarfs, dwarf_info, trader_requested, dwarfs, has_butchers, has_fishery, out);
}
@@ -1241,7 +1216,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
{
FOR_ENUM_ITEMS(unit_labor, labor)
{
- if (labor == df::enums::unit_labor::NONE)
+ if (labor == unit_labor::NONE)
continue;
if (labor_infos[labor].mode() != HAULERS)
continue;
@@ -1264,14 +1239,9 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
FOR_ENUM_ITEMS(unit_labor, labor)
{
- if (labor == df::enums::unit_labor::NONE)
+ if (labor == unit_labor::NONE)
continue;
- /*
- assert(labor >= 0);
- assert(labor < ARRAY_COUNT(labor_infos));
- */
-
if (labor_infos[labor].mode() != HAULERS)
continue;
@@ -1311,7 +1281,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out )
return CR_OK;
}
-void print_labor (df::enums::unit_labor::unit_labor labor, color_ostream &out)
+void print_labor (df::unit_labor labor, color_ostream &out)
{
string labor_name = ENUM_KEY_STR(unit_labor, labor);
out << labor_name << ": ";
@@ -1358,7 +1328,6 @@ command_result autolabor (color_ostream &out, std::vector & parame
return CR_OK;
}
-
else if (parameters.size() == 2 && parameters[0] == "haulpct")
{
if (!enable_autolabor)
@@ -1371,15 +1340,15 @@ command_result autolabor (color_ostream &out, std::vector & parame
hauler_pct = pct;
return CR_OK;
}
- else if (parameters.size() == 2 || parameters.size() == 3) {
-
+ else if (parameters.size() == 2 || parameters.size() == 3)
+ {
if (!enable_autolabor)
{
out << "Error: The plugin is not enabled." << endl;
return CR_FAILURE;
}
- df::enums::unit_labor::unit_labor labor = df::enums::unit_labor::NONE;
+ df::unit_labor labor = unit_labor::NONE;
FOR_ENUM_ITEMS(unit_labor, test_labor)
{
@@ -1387,7 +1356,7 @@ command_result autolabor (color_ostream &out, std::vector & parame
labor = test_labor;
}
- if (labor == df::enums::unit_labor::NONE)
+ if (labor == unit_labor::NONE)
{
out.printerr("Could not find labor %s.\n", parameters[0].c_str());
return CR_WRONG_USAGE;
@@ -1430,7 +1399,8 @@ command_result autolabor (color_ostream &out, std::vector & parame
return CR_OK;
}
- else if (parameters.size() == 1 && parameters[0] == "reset-all") {
+ else if (parameters.size() == 1 && parameters[0] == "reset-all")
+ {
if (!enable_autolabor)
{
out << "Error: The plugin is not enabled." << endl;
@@ -1439,12 +1409,13 @@ command_result autolabor (color_ostream &out, std::vector & parame
for (int i = 0; i < labor_infos.size(); i++)
{
- reset_labor((df::enums::unit_labor::unit_labor) i);
+ reset_labor((df::unit_labor) i);
}
out << "All labors reset." << endl;
return CR_OK;
}
- else if (parameters.size() == 1 && parameters[0] == "list" || parameters[0] == "status") {
+ else if (parameters.size() == 1 && parameters[0] == "list" || parameters[0] == "status")
+ {
if (!enable_autolabor)
{
out << "Error: The plugin is not enabled." << endl;
@@ -1467,7 +1438,7 @@ command_result autolabor (color_ostream &out, std::vector & parame
{
FOR_ENUM_ITEMS(unit_labor, labor)
{
- if (labor == df::enums::unit_labor::NONE)
+ if (labor == unit_labor::NONE)
continue;
print_labor(labor, out);
@@ -1571,7 +1542,7 @@ static int stockcheck(color_ostream &out, vector & parameters)
{
df::building *build = world->buildings.all[i];
auto type = build->getType();
- if (df::enums::building_type::Stockpile == type)
+ if (building_type::Stockpile == type)
{
df::building_stockpilest *sp = virtual_cast(build);
StockpileInfo *spi = new StockpileInfo(sp);
@@ -1580,7 +1551,7 @@ static int stockcheck(color_ostream &out, vector & parameters)
}
- std::vector &items = world->items.other[df::enums::items_other_id::ANY_FREE];
+ std::vector &items = world->items.other[items_other_id::ANY_FREE];
// Precompute a bitmask with the bad flags
df::item_flags bad_flags;
@@ -1602,13 +1573,13 @@ static int stockcheck(color_ostream &out, vector & parameters)
// we really only care about MEAT, FISH, FISH_RAW, PLANT, CHEESE, FOOD, and EGG
df::item_type typ = item->getType();
- if (typ != df::enums::item_type::MEAT &&
- typ != df::enums::item_type::FISH &&
- typ != df::enums::item_type::FISH_RAW &&
- typ != df::enums::item_type::PLANT &&
- typ != df::enums::item_type::CHEESE &&
- typ != df::enums::item_type::FOOD &&
- typ != df::enums::item_type::EGG)
+ if (typ != item_type::MEAT &&
+ typ != item_type::FISH &&
+ typ != item_type::FISH_RAW &&
+ typ != item_type::PLANT &&
+ typ != item_type::CHEESE &&
+ typ != item_type::FOOD &&
+ typ != item_type::EGG)
continue;
df::item *container = 0;
@@ -1673,11 +1644,11 @@ static int stockcheck(color_ostream &out, vector & parameters)
if (building) {
df::building_type btype = building->getType();
- if (btype == df::enums::building_type::TradeDepot ||
- btype == df::enums::building_type::Wagon)
+ if (btype == building_type::TradeDepot ||
+ btype == building_type::Wagon)
continue; // items in trade depot or the embark wagon do not rot
- if (typ == df::enums::item_type::EGG && btype ==df::enums::building_type::NestBox)
+ if (typ == item_type::EGG && btype ==building_type::NestBox)
continue; // eggs in nest box do not rot
}
diff --git a/plugins/cleaners.cpp b/plugins/cleaners.cpp
index de204f611..c0301de7b 100644
--- a/plugins/cleaners.cpp
+++ b/plugins/cleaners.cpp
@@ -50,12 +50,12 @@ command_result cleanmap (color_ostream &out, bool snow, bool mud)
// filter snow
if(!snow
&& spatter->mat_type == builtin_mats::WATER
- && spatter->mat_state == matter_state::Powder)
+ && spatter->mat_state == (short)matter_state::Powder)
continue;
// filter mud
if(!mud
&& spatter->mat_type == builtin_mats::MUD
- && spatter->mat_state == matter_state::Solid)
+ && spatter->mat_state == (short)matter_state::Solid)
continue;
delete evt;
diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp
index c1521b8de..cd01fd616 100644
--- a/plugins/cleanowned.cpp
+++ b/plugins/cleanowned.cpp
@@ -116,7 +116,7 @@ command_result df_cleanowned (color_ostream &out, vector & parameters)
}
else if (item->flags.bits.on_ground)
{
- int32_t type = item->getType();
+ df::item_type type = item->getType();
if(type == item_type::MEAT ||
type == item_type::FISH ||
type == item_type::VERMIN ||
diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt
index f126ae53b..39e8f7b60 100644
--- a/plugins/devel/CMakeLists.txt
+++ b/plugins/devel/CMakeLists.txt
@@ -18,7 +18,7 @@ DFHACK_PLUGIN(stripcaged stripcaged.cpp)
DFHACK_PLUGIN(rprobe rprobe.cpp)
DFHACK_PLUGIN(nestboxes nestboxes.cpp)
DFHACK_PLUGIN(vshook vshook.cpp)
-DFHACK_PLUGIN(steam-engine steam-engine.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/siege-engine.cpp b/plugins/devel/siege-engine.cpp
new file mode 100644
index 000000000..a41bfe5f7
--- /dev/null
+++ b/plugins/devel/siege-engine.cpp
@@ -0,0 +1,1649 @@
+#include "Core.h"
+#include
+#include
+#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 "df/job.h"
+#include "df/job_item.h"
+#include "df/item.h"
+#include "df/items_other_id.h"
+#include "df/building_stockpilest.h"
+#include "df/stockpile_links.h"
+#include "df/workshop_profile.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)));
+}
+
+inline void normalize(float &x, float &y, float &z)
+{
+ float dist = sqrtf(x*x + y*y + z*z);
+ if (dist == 0.0f) return;
+ x /= dist; y /= dist; z /= dist;
+}
+
+static void random_direction(float &x, float &y, float &z)
+{
+ float a, b, d;
+ for (;;) {
+ a = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f;
+ b = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f;
+ d = a*a + b*b;
+ if (d < 1.0f)
+ break;
+ }
+
+ float sq = sqrtf(1-d);
+ x = 2.0f*a*sq;
+ y = 2.0f*b*sq;
+ z = 1.0f - 2.0f*d;
+}
+
+/*
+ * Configuration object
+ */
+
+static bool enable_plugin();
+
+struct EngineInfo {
+ int id;
+ df::building_siegeenginest *bld;
+
+ df::coord center;
+ coord_range building_rect;
+
+ bool is_catapult;
+ int proj_speed, hit_delay;
+ std::pair fire_range;
+
+ coord_range target;
+
+ df::job_item_vector_id ammo_vector_id;
+ df::item_type ammo_item_type;
+
+ int operator_id, operator_frame;
+
+ std::set stockpiles;
+ df::stockpile_links links;
+ df::workshop_profile profile;
+
+ 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; }
+
+ bool isInRange(int dist) {
+ return dist >= fire_range.first && dist <= fire_range.second;
+ }
+};
+
+static std::map engines;
+static std::map coord_engines;
+
+static EngineInfo *find_engine(df::building *bld, bool create = false)
+{
+ auto ebld = strict_virtual_cast(bld);
+ if (!ebld)
+ return NULL;
+
+ auto &obj = engines[bld];
+
+ if (obj)
+ {
+ obj->bld = ebld;
+ return obj;
+ }
+
+ if (!create)
+ return NULL;
+
+ obj = new EngineInfo();
+
+ obj->id = bld->id;
+ obj->bld = ebld;
+ obj->center = df::coord(bld->centerx, bld->centery, bld->z);
+ obj->building_rect = coord_range(
+ df::coord(bld->x1, bld->y1, bld->z),
+ df::coord(bld->x2, bld->y2, bld->z)
+ );
+ obj->is_catapult = (ebld->type == siegeengine_type::Catapult);
+ obj->proj_speed = 2;
+ obj->hit_delay = 3;
+ obj->fire_range = get_engine_range(ebld);
+
+ obj->ammo_vector_id = job_item_vector_id::BOULDER;
+ obj->ammo_item_type = item_type::BOULDER;
+
+ obj->operator_id = obj->operator_frame = -1;
+
+ coord_engines[obj->center] = bld;
+ return obj;
+}
+
+static EngineInfo *find_engine(lua_State *L, int idx, bool create = false, bool silent = false)
+{
+ auto bld = Lua::CheckDFObject(L, idx);
+
+ auto engine = find_engine(bld, create);
+ if (!engine && !silent)
+ luaL_error(L, "no such engine");
+
+ return engine;
+}
+
+static EngineInfo *find_engine(df::coord pos)
+{
+ auto engine = find_engine(coord_engines[pos]);
+
+ if (engine)
+ {
+ auto bld0 = df::building::find(engine->id);
+ auto bld = strict_virtual_cast(bld0);
+ if (!bld)
+ return NULL;
+
+ engine->bld = bld;
+ }
+
+ return engine;
+}
+
+/*
+ * Configuration management
+ */
+
+static void clear_engines()
+{
+ for (auto it = engines.begin(); it != engines.end(); ++it)
+ delete it->second;
+ 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));
+ }
+
+ pworld->GetPersistentData(&vec, "siege-engine/ammo/", 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->ammo_vector_id = (df::job_item_vector_id)it->ival(1);
+ engine->ammo_item_type = (df::item_type)it->ival(2);
+ }
+
+ pworld->GetPersistentData(&vec, "siege-engine/stockpiles/", true);
+ for (auto it = vec.begin(); it != vec.end(); ++it)
+ {
+ auto engine = find_engine(df::building::find(it->ival(0)), true);
+ if (!engine)
+ continue;
+ auto pile = df::building::find(it->ival(1));
+ if (!pile || pile->getType() != building_type::Stockpile)
+ {
+ pworld->DeletePersistentData(*it);
+ continue;;
+ }
+
+ engine->stockpiles.insert(it->ival(1));
+ }
+
+ pworld->GetPersistentData(&vec, "siege-engine/profiles/", 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->profile.min_level = it->ival(1);
+ engine->profile.max_level = it->ival(2);
+ }
+
+ pworld->GetPersistentData(&vec, "siege-engine/profile-workers/", true);
+ for (auto it = vec.begin(); it != vec.end(); ++it)
+ {
+ auto engine = find_engine(df::building::find(it->ival(0)), true);
+ if (!engine)
+ continue;
+ auto unit = df::unit::find(it->ival(1));
+ if (!unit || !Units::isCitizen(unit))
+ {
+ pworld->DeletePersistentData(*it);
+ continue;
+ }
+ engine->profile.permitted_workers.push_back(it->ival(1));
+ }
+}
+
+static int getTargetArea(lua_State *L)
+{
+ auto engine = find_engine(L, 1, false, true);
+
+ 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;
+}
+
+static int getAmmoItem(lua_State *L)
+{
+ auto engine = find_engine(L, 1, false, true);
+ if (!engine)
+ Lua::Push(L, item_type::BOULDER);
+ else
+ Lua::Push(L, engine->ammo_item_type);
+ return 1;
+}
+
+static int setAmmoItem(lua_State *L)
+{
+ if (!enable_plugin())
+ return 0;
+
+ auto engine = find_engine(L, 1, true);
+ auto item_type = (df::item_type)luaL_optint(L, 2, item_type::BOULDER);
+ if (!is_valid_enum_item(item_type))
+ luaL_argerror(L, 2, "invalid item type");
+
+ auto pworld = Core::getInstance().getWorld();
+ auto key = stl_sprintf("siege-engine/ammo/%d", engine->id);
+ auto entry = pworld->GetPersistentData(key, NULL);
+ if (!entry.isValid())
+ return 0;
+
+ engine->ammo_vector_id = job_item_vector_id::ANY_FREE;
+ engine->ammo_item_type = item_type;
+
+ FOR_ENUM_ITEMS(job_item_vector_id, id)
+ {
+ auto other = ENUM_ATTR(job_item_vector_id, other, id);
+ auto type = ENUM_ATTR(items_other_id, item, other);
+ if (type == item_type)
+ {
+ engine->ammo_vector_id = id;
+ break;
+ }
+ }
+
+ entry.ival(0) = engine->id;
+ entry.ival(1) = engine->ammo_vector_id;
+ entry.ival(2) = engine->ammo_item_type;
+
+ lua_pushboolean(L, true);
+ return 1;
+}
+
+static void forgetStockpileLink(EngineInfo *engine, int pile_id)
+{
+ engine->stockpiles.erase(pile_id);
+
+ auto pworld = Core::getInstance().getWorld();
+ auto key = stl_sprintf("siege-engine/stockpiles/%d/%d", engine->id, pile_id);
+ pworld->DeletePersistentData(pworld->GetPersistentData(key));
+}
+
+static void update_stockpile_links(EngineInfo *engine)
+{
+ engine->links.take_from_pile.clear();
+
+ for (auto it = engine->stockpiles.begin(); it != engine->stockpiles.end(); )
+ {
+ int id = *it; ++it;
+ auto pile = df::building::find(id);
+
+ if (!pile || pile->getType() != building_type::Stockpile)
+ forgetStockpileLink(engine, id);
+ else
+ // The vector is sorted, but we are iterating through a sorted set
+ engine->links.take_from_pile.push_back(pile);
+ }
+}
+
+static int getStockpileLinks(lua_State *L)
+{
+ auto engine = find_engine(L, 1, false, true);
+ if (!engine || engine->stockpiles.empty())
+ return 0;
+
+ update_stockpile_links(engine);
+
+ auto &links = engine->links.take_from_pile;
+ lua_createtable(L, links.size(), 0);
+
+ for (size_t i = 0; i < links.size(); i++)
+ {
+ Lua::Push(L, links[i]);
+ lua_rawseti(L, -2, i+1);
+ }
+
+ return 1;
+}
+
+static bool isLinkedToPile(df::building_siegeenginest *bld, df::building_stockpilest *pile)
+{
+ CHECK_NULL_POINTER(bld);
+ CHECK_NULL_POINTER(pile);
+
+ auto engine = find_engine(bld);
+
+ return engine && engine->stockpiles.count(pile->id);
+}
+
+static bool addStockpileLink(df::building_siegeenginest *bld, df::building_stockpilest *pile)
+{
+ CHECK_NULL_POINTER(bld);
+ CHECK_NULL_POINTER(pile);
+
+ if (!enable_plugin())
+ return false;
+
+ auto pworld = Core::getInstance().getWorld();
+ auto key = stl_sprintf("siege-engine/stockpiles/%d/%d", bld->id, pile->id);
+ auto entry = pworld->GetPersistentData(key, NULL);
+ if (!entry.isValid())
+ return false;
+
+ auto engine = find_engine(bld, true);
+
+ entry.ival(0) = bld->id;
+ entry.ival(1) = pile->id;
+
+ engine->stockpiles.insert(pile->id);
+ return true;
+}
+
+static bool removeStockpileLink(df::building_siegeenginest *bld, df::building_stockpilest *pile)
+{
+ CHECK_NULL_POINTER(bld);
+ CHECK_NULL_POINTER(pile);
+
+ if (auto engine = find_engine(bld))
+ {
+ forgetStockpileLink(engine, pile->id);
+ return true;
+ }
+
+ return false;
+}
+
+static df::workshop_profile *saveWorkshopProfile(df::building_siegeenginest *bld)
+{
+ CHECK_NULL_POINTER(bld);
+
+ if (!enable_plugin())
+ return NULL;
+
+ // Save skill limits
+ auto pworld = Core::getInstance().getWorld();
+ auto key = stl_sprintf("siege-engine/profiles/%d", bld->id);
+ auto entry = pworld->GetPersistentData(key, NULL);
+ if (!entry.isValid())
+ return NULL;
+
+ auto engine = find_engine(bld, true);
+
+ entry.ival(0) = engine->id;
+ entry.ival(1) = engine->profile.min_level;
+ entry.ival(2) = engine->profile.max_level;
+
+ // Save worker list
+ std::vector vec;
+ auto &workers = engine->profile.permitted_workers;
+
+ key = stl_sprintf("siege-engine/profile-workers/%d", bld->id);
+ pworld->GetPersistentData(&vec, key, true);
+
+ for (auto it = vec.begin(); it != vec.end(); ++it)
+ {
+ if (linear_index(workers, it->ival(1)) < 0)
+ pworld->DeletePersistentData(*it);
+ }
+
+ for (size_t i = 0; i < workers.size(); i++)
+ {
+ key = stl_sprintf("siege-engine/profile-workers/%d/%d", bld->id, workers[i]);
+ entry = pworld->GetPersistentData(key, NULL);
+ if (!entry.isValid())
+ continue;
+ entry.ival(0) = engine->id;
+ entry.ival(1) = workers[i];
+ }
+
+ return &engine->profile;
+}
+
+static int getOperatorSkill(df::building_siegeenginest *bld, bool force = false)
+{
+ CHECK_NULL_POINTER(bld);
+
+ auto engine = find_engine(bld);
+ if (!engine)
+ return 0;
+
+ if (engine->operator_id != -1 &&
+ (world->frame_counter - engine->operator_frame) <= 5)
+ {
+ auto op_unit = df::unit::find(engine->operator_id);
+ if (op_unit)
+ return Units::getEffectiveSkill(op_unit, job_skill::SIEGEOPERATE);
+ }
+
+ if (force)
+ {
+ color_ostream_proxy out(Core::getInstance().getConsole());
+ out.print("Forced siege operator search\n");
+
+ auto &active = world->units.active;
+ for (size_t i = 0; i < active.size(); i++)
+ if (active[i]->pos == engine->center && Units::isCitizen(active[i]))
+ return Units::getEffectiveSkill(active[i], job_skill::SIEGEOPERATE);
+ }
+
+ return 0;
+}
+
+/*
+ * Trajectory raytracing
+ */
+
+struct ProjectilePath {
+ static const int DEFAULT_FUDGE = 31;
+
+ 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), fudge_factor(1)
+ {
+ fudge_delta = df::coord(0,0,0);
+ calc_line();
+ }
+
+ ProjectilePath(df::coord origin, df::coord goal, df::coord delta, int factor) :
+ origin(origin), goal(goal), fudge_delta(delta), fudge_factor(factor)
+ {
+ calc_line();
+ }
+
+ ProjectilePath(df::coord origin, df::coord goal, float zdelta, int factor = DEFAULT_FUDGE) :
+ origin(origin), goal(goal), fudge_factor(factor)
+ {
+ fudge_delta = df::coord(0,0,int(factor * zdelta));
+ calc_line();
+ }
+
+ void calc_line()
+ {
+ speed = goal - origin;
+ speed.x *= fudge_factor;
+ speed.y *= fudge_factor;
+ speed.z *= fudge_factor;
+ speed = speed + fudge_delta;
+ target = origin + speed;
+ 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 ProjectilePath decode_path(lua_State *L, int idx, df::coord origin)
+{
+ idx = lua_absindex(L, idx);
+
+ Lua::StackUnwinder frame(L);
+ df::coord goal;
+
+ lua_getfield(L, idx, "target");
+ Lua::CheckDFAssign(L, &goal, frame[1]);
+
+ lua_getfield(L, idx, "delta");
+
+ if (!lua_isnil(L, frame[2]))
+ {
+ lua_getfield(L, idx, "factor");
+ int factor = luaL_optnumber(L, frame[3], ProjectilePath::DEFAULT_FUDGE);
+
+ if (lua_isnumber(L, frame[2]))
+ return ProjectilePath(origin, goal, lua_tonumber(L, frame[2]), factor);
+
+ df::coord delta;
+ Lua::CheckDFAssign(L, &delta, frame[2]);
+
+ return ProjectilePath(origin, goal, delta, factor);
+ }
+
+ return ProjectilePath(origin, goal);
+}
+
+static int projPosAtStep(lua_State *L)
+{
+ auto engine = find_engine(L, 1);
+ auto path = decode_path(L, 2, engine->center);
+ int step = luaL_checkint(L, 3);
+ Lua::Push(L, path[step]);
+ return 1;
+}
+
+static bool isPassableTile(df::coord pos)
+{
+ auto ptile = Maps::getTileType(pos);
+
+ return !ptile || FlowPassable(*ptile);
+}
+
+static bool isTargetableTile(df::coord pos)
+{
+ auto ptile = Maps::getTileType(pos);
+
+ return ptile && FlowPassable(*ptile) && !isOpenTerrain(*ptile);
+}
+
+static bool isTreeTile(df::coord pos)
+{
+ auto ptile = Maps::getTileType(pos);
+
+ return ptile && tileShape(*ptile) == tiletype_shape::TREE;
+}
+
+static bool adjustToTarget(EngineInfo *engine, df::coord *pos)
+{
+ if (isTargetableTile(*pos))
+ return true;
+
+ for (df::coord fudge = *pos;
+ fudge.z <= engine->target.second.z; fudge.z++)
+ {
+ if (!isTargetableTile(fudge))
+ continue;
+ *pos = fudge;
+ return true;
+ }
+
+ for (df::coord fudge = *pos;
+ fudge.z >= engine->target.first.z; fudge.z--)
+ {
+ if (!isTargetableTile(fudge))
+ continue;
+ *pos = fudge;
+ return true;
+ }
+
+ return false;
+}
+
+static int adjustToTarget(lua_State *L)
+{
+ auto engine = find_engine(L, 1, true);
+ df::coord pos;
+ Lua::CheckDFAssign(L, &pos, 2);
+ bool ok = adjustToTarget(engine, &pos);
+ Lua::Push(L, pos);
+ Lua::Push(L, ok);
+ return 2;
+}
+
+static const char* const hit_type_names[] = {
+ "wall", "floor", "ceiling", "map_edge", "tree"
+};
+
+struct PathMetrics {
+ enum CollisionType {
+ Impassable,
+ Floor,
+ Ceiling,
+ MapEdge,
+ Tree
+ } hit_type;
+
+ int collision_step, collision_z_step;
+ int goal_step, goal_z_step, goal_distance;
+
+ bool hits() const { return collision_step > goal_step; }
+
+ PathMetrics(const ProjectilePath &path)
+ {
+ compute(path);
+ }
+
+ void compute(const ProjectilePath &path)
+ {
+ collision_step = goal_step = goal_z_step = 1000000;
+ collision_z_step = 0;
+
+ goal_distance = point_distance(path.origin - path.goal);
+
+ int step = 0;
+ df::coord prev_pos = path.origin;
+
+ for (;;) {
+ df::coord cur_pos = path[++step];
+ if (cur_pos == prev_pos)
+ break;
+
+ 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))
+ {
+ if (isTreeTile(cur_pos))
+ {
+ // The projectile code has a bug where it will
+ // hit a tree on the same tick as a Z level change.
+ if (cur_pos.z != prev_pos.z)
+ {
+ hit_type = Tree;
+ break;
+ }
+ }
+ else
+ {
+ 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;
+ }
+
+ collision_z_step = step;
+ }
+
+ prev_pos = cur_pos;
+ }
+
+ collision_step = step;
+ }
+};
+
+enum TargetTileStatus {
+ TARGET_OK, TARGET_RANGE, TARGET_BLOCKED, TARGET_SEMIBLOCKED
+};
+static const char* const target_tile_type_names[] = {
+ "ok", "out_of_range", "blocked", "semi_blocked"
+};
+
+static TargetTileStatus calcTileStatus(EngineInfo *engine, const PathMetrics &raytrace)
+{
+ if (raytrace.hits())
+ {
+ if (engine->isInRange(raytrace.goal_step))
+ return TARGET_OK;
+ else
+ return TARGET_RANGE;
+ }
+ else
+ return TARGET_BLOCKED;
+}
+
+static int projPathMetrics(lua_State *L)
+{
+ auto engine = find_engine(L, 1);
+ auto path = decode_path(L, 2, engine->center);
+
+ PathMetrics info(path);
+
+ lua_createtable(L, 0, 7);
+ Lua::SetField(L, hit_type_names[info.hit_type], -1, "hit_type");
+ Lua::SetField(L, info.collision_step, -1, "collision_step");
+ Lua::SetField(L, info.collision_z_step, -1, "collision_z_step");
+ Lua::SetField(L, info.goal_distance, -1, "goal_distance");
+ if (info.goal_step < info.collision_step)
+ Lua::SetField(L, info.goal_step, -1, "goal_step");
+ if (info.goal_z_step < info.collision_step)
+ Lua::SetField(L, info.goal_z_step, -1, "goal_z_step");
+ Lua::SetField(L, target_tile_type_names[calcTileStatus(engine, info)], -1, "status");
+ return 1;
+}
+
+static TargetTileStatus calcTileStatus(EngineInfo *engine, df::coord target, float zdelta)
+{
+ ProjectilePath path(engine->center, target, zdelta);
+ PathMetrics raytrace(path);
+ return calcTileStatus(engine, raytrace);
+}
+
+static TargetTileStatus calcTileStatus(EngineInfo *engine, df::coord target)
+{
+ auto status = calcTileStatus(engine, target, 0.0f);
+
+ if (status == TARGET_BLOCKED)
+ {
+ if (calcTileStatus(engine, target, 0.5f) < TARGET_BLOCKED)
+ return TARGET_SEMIBLOCKED;
+
+ if (calcTileStatus(engine, target, -0.5f) < TARGET_BLOCKED)
+ return TARGET_SEMIBLOCKED;
+ }
+
+ return status;
+}
+
+static std::string getTileStatus(df::building_siegeenginest *bld, df::coord tile_pos)
+{
+ auto engine = find_engine(bld, true);
+ if (!engine)
+ return "invalid";
+
+ return target_tile_type_names[calcTileStatus(engine, tile_pos)];
+}
+
+static void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::coord2d ltop, df::coord2d size)
+{
+ auto engine = find_engine(bld, true);
+ CHECK_NULL_POINTER(engine);
+
+ 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(engine->building_rect, tile_pos))
+ continue;
+
+ Pen cur_tile = Screen::readTile(ltop.x+x, ltop.y+y);
+ if (!cur_tile.valid())
+ continue;
+
+ int color;
+
+ switch (calcTileStatus(engine, tile_pos))
+ {
+ case TARGET_OK:
+ color = COLOR_GREEN;
+ break;
+ case TARGET_RANGE:
+ color = COLOR_CYAN;
+ break;
+ case TARGET_BLOCKED:
+ color = COLOR_RED;
+ break;
+ case TARGET_SEMIBLOCKED:
+ color = COLOR_BROWN;
+ break;
+ }
+
+ 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 = 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)
+ {
+ if (unit->flags1.bits.rider)
+ {
+ auto mount = df::unit::find(unit->relations.rider_mount_id);
+
+ if (mount)
+ {
+ path = get(mount)->path;
+ return;
+ }
+ }
+
+ 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) && engine->isInRange(info.dist))
+ 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 bool canTargetUnit(df::unit *unit)
+{
+ CHECK_NULL_POINTER(unit);
+
+ if (unit->flags1.bits.dead ||
+ unit->flags3.bits.ghostly ||
+ unit->flags1.bits.caged ||
+ unit->flags1.bits.hidden_in_ambush)
+ return false;
+
+ return true;
+}
+
+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 (!canTargetUnit(unit))
+ continue;
+
+ UnitPath::get(unit)->findHits(engine, hits, bias);
+ }
+}
+
+static int proposeUnitHits(lua_State *L)
+{
+ auto engine = find_engine(L, 1);
+ float bias = luaL_optnumber(L, 2, 0);
+
+ 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::SetField(L, hit.path->unit, -1, "unit");
+ Lua::SetField(L, hit.pos, -1, "pos");
+ Lua::SetField(L, hit.dist, -1, "dist");
+ Lua::SetField(L, hit.time, -1, "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;
+}
+
+/*
+ * Projectile hook
+ */
+
+struct projectile_hook : df::proj_itemst {
+ typedef df::proj_itemst interpose_base;
+
+ void aimAtPoint(EngineInfo *engine, const ProjectilePath &path)
+ {
+ 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]);
+
+ // Find valid hit point for catapult stones
+ if (flags.bits.high_flying)
+ {
+ if (raytrace.hits())
+ fall_threshold = raytrace.goal_step;
+ else
+ fall_threshold = (raytrace.collision_z_step+raytrace.collision_step-1)/2;
+
+ while (fall_threshold < raytrace.collision_step-1)
+ {
+ if (isTargetableTile(path[fall_threshold]))
+ break;
+
+ fall_threshold++;
+ }
+ }
+
+ fall_threshold = std::max(fall_threshold, engine->fire_range.first);
+ fall_threshold = std::min(fall_threshold, engine->fire_range.second);
+ }
+
+ void aimAtArea(EngineInfo *engine)
+ {
+ df::coord target, last_passable;
+ df::coord tbase = engine->target.first;
+ df::coord tsize = 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 (adjustToTarget(engine, &target))
+ last_passable = target;
+ else
+ continue;
+
+ ProjectilePath path(engine->center, target, engine->is_catapult ? 0.5f : 0.0f);
+ PathMetrics raytrace(path);
+
+ if (raytrace.hits() && engine->isInRange(raytrace.goal_step))
+ {
+ aimAtPoint(engine, path);
+ return;
+ }
+ }
+
+ if (!last_passable.isValid())
+ last_passable = target;
+
+ aimAtPoint(engine, ProjectilePath(engine->center, last_passable));
+ }
+
+ static int safeAimProjectile(lua_State *L)
+ {
+ color_ostream &out = *Lua::GetOutput(L);
+ auto proj = (projectile_hook*)lua_touserdata(L, 1);
+ auto engine = (EngineInfo*)lua_touserdata(L, 2);
+ int skill = lua_tointeger(L, 3);
+
+ if (!Lua::PushModulePublic(out, L, "plugins.siege-engine", "doAimProjectile"))
+ luaL_error(L, "Projectile aiming AI not available");
+
+ Lua::PushDFObject(L, engine->bld);
+ Lua::Push(L, proj->item);
+ Lua::Push(L, engine->target.first);
+ Lua::Push(L, engine->target.second);
+ Lua::Push(L, skill);
+
+ lua_call(L, 5, 1);
+
+ if (lua_isnil(L, -1))
+ proj->aimAtArea(engine);
+ else
+ proj->aimAtPoint(engine, decode_path(L, -1, engine->center));
+
+ return 0;
+ }
+
+ void doCheckMovement()
+ {
+ if (flags.bits.parabolic || distance_flown != 0 ||
+ fall_counter != fall_delay || item == NULL)
+ return;
+
+ auto engine = find_engine(origin_pos);
+ if (!engine || !engine->hasTarget())
+ return;
+
+ auto L = Lua::Core::State;
+ CoreSuspendClaimer suspend;
+ color_ostream_proxy out(Core::getInstance().getConsole());
+
+ int skill = getOperatorSkill(engine->bld, true);
+
+ lua_pushcfunction(L, safeAimProjectile);
+ lua_pushlightuserdata(L, this);
+ lua_pushlightuserdata(L, engine);
+ lua_pushinteger(L, skill);
+
+ if (!Lua::Core::SafeCall(out, 3, 0))
+ aimAtArea(engine);
+
+ switch (item->getType())
+ {
+ case item_type::CAGE:
+ flags.bits.bouncing = false;
+ break;
+ case item_type::BIN:
+ case item_type::BARREL:
+ flags.bits.bouncing = false;
+ break;
+ default:
+ break;
+ }
+ }
+
+ void doLaunchContents()
+ {
+ // Translate cartoon flight speed to parabolic
+ float speed = 100000.0f / (fall_delay + 1);
+ int min_zspeed = (fall_delay+1)*4900;
+
+ // Flight direction vector
+ df::coord dist = target_pos - origin_pos;
+ float vx = dist.x, vy = dist.y, vz = fabs(dist.z);
+ normalize(vx, vy, vz);
+
+ int start_z = 0;
+
+ // Start at tile top, if hit a wall
+ ProjectilePath path(origin_pos, target_pos);
+ auto next_pos = path[distance_flown+1];
+ if (next_pos.z == cur_pos.z && !isPassableTile(next_pos))
+ start_z = 49000;
+
+ MapExtras::MapCache mc;
+ std::vector contents;
+ Items::getContainedItems(item, &contents);
+
+ for (size_t i = 0; i < contents.size(); i++)
+ {
+ auto child = contents[i];
+ auto proj = Items::makeProjectile(mc, child);
+ if (!proj) continue;
+
+ proj->flags.bits.no_impact_destroy = true;
+ //proj->flags.bits.bouncing = true;
+ proj->flags.bits.piercing = true;
+ proj->flags.bits.parabolic = true;
+ proj->flags.bits.unk9 = true;
+ proj->flags.bits.no_collide = true;
+
+ proj->pos_z = start_z;
+
+ float sx, sy, sz;
+ random_direction(sx, sy, sz);
+ sx += vx*0.7; sy += vy*0.7; sz += vz*0.7;
+ if (sz < 0) sz = -sz;
+ normalize(sx, sy, sz);
+
+ proj->speed_x = int(speed * sx);
+ proj->speed_y = int(speed * sy);
+ proj->speed_z = int(speed * sz);
+ }
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, checkMovement, ())
+ {
+ if (flags.bits.high_flying || flags.bits.piercing)
+ doCheckMovement();
+
+ return INTERPOSE_NEXT(checkMovement)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, checkImpact, (bool no_damage_floor))
+ {
+ if (!flags.bits.has_hit_ground && !flags.bits.parabolic &&
+ flags.bits.high_flying && !flags.bits.bouncing &&
+ !flags.bits.no_impact_destroy && target_pos != origin_pos &&
+ item && item->flags.bits.container)
+ {
+ doLaunchContents();
+ }
+
+ return INTERPOSE_NEXT(checkImpact)(no_damage_floor);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(projectile_hook, checkMovement);
+IMPLEMENT_VMETHOD_INTERPOSE(projectile_hook, checkImpact);
+
+/*
+ * Building hook
+ */
+
+struct building_hook : df::building_siegeenginest {
+ typedef df::building_siegeenginest interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(df::workshop_profile*, getWorkshopProfile, ())
+ {
+ if (auto engine = find_engine(this))
+ return &engine->profile;
+
+ return INTERPOSE_NEXT(getWorkshopProfile)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(df::stockpile_links*, getStockpileLinks, ())
+ {
+ if (auto engine = find_engine(this))
+ {
+ update_stockpile_links(engine);
+ return &engine->links;
+ }
+
+ return INTERPOSE_NEXT(getStockpileLinks)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, updateAction, ())
+ {
+ INTERPOSE_NEXT(updateAction)();
+
+ if (jobs.empty())
+ return;
+
+ if (auto engine = find_engine(this))
+ {
+ auto job = jobs[0];
+ bool save_op = false;
+
+ switch (job->job_type)
+ {
+ case job_type::LoadCatapult:
+ if (!job->job_items.empty())
+ {
+ auto item = job->job_items[0];
+ item->item_type = engine->ammo_item_type;
+ item->vector_id = engine->ammo_vector_id;
+
+ switch (item->item_type)
+ {
+ case item_type::NONE:
+ case item_type::BOULDER:
+ case item_type::BLOCKS:
+ item->mat_type = 0;
+ break;
+
+ case item_type::BIN:
+ case item_type::BARREL:
+ item->mat_type = -1;
+ // A hack to make it take objects assigned to stockpiles.
+ // Since reaction_id is not set, the actual value is not used.
+ item->contains.resize(1);
+ break;
+
+ default:
+ item->mat_type = -1;
+ break;
+ }
+ }
+ // fallthrough
+
+ case job_type::LoadBallista:
+ case job_type::FireCatapult:
+ case job_type::FireBallista:
+ if (auto worker = Job::getWorker(job))
+ {
+ engine->operator_id = worker->id;
+ engine->operator_frame = world->frame_counter;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(building_hook, getWorkshopProfile);
+IMPLEMENT_VMETHOD_INTERPOSE(building_hook, getStockpileLinks);
+IMPLEMENT_VMETHOD_INTERPOSE(building_hook, updateAction);
+
+/*
+ * Initialization
+ */
+
+DFHACK_PLUGIN_LUA_FUNCTIONS {
+ DFHACK_LUA_FUNCTION(clearTargetArea),
+ DFHACK_LUA_FUNCTION(setTargetArea),
+ DFHACK_LUA_FUNCTION(isLinkedToPile),
+ DFHACK_LUA_FUNCTION(addStockpileLink),
+ DFHACK_LUA_FUNCTION(removeStockpileLink),
+ DFHACK_LUA_FUNCTION(saveWorkshopProfile),
+ DFHACK_LUA_FUNCTION(getTileStatus),
+ DFHACK_LUA_FUNCTION(paintAimScreen),
+ DFHACK_LUA_FUNCTION(canTargetUnit),
+ DFHACK_LUA_FUNCTION(isPassableTile),
+ DFHACK_LUA_FUNCTION(isTreeTile),
+ DFHACK_LUA_FUNCTION(isTargetableTile),
+ DFHACK_LUA_END
+};
+
+DFHACK_PLUGIN_LUA_COMMANDS {
+ DFHACK_LUA_COMMAND(getTargetArea),
+ DFHACK_LUA_COMMAND(getAmmoItem),
+ DFHACK_LUA_COMMAND(setAmmoItem),
+ DFHACK_LUA_COMMAND(getStockpileLinks),
+ DFHACK_LUA_COMMAND(projPosAtStep),
+ DFHACK_LUA_COMMAND(projPathMetrics),
+ DFHACK_LUA_COMMAND(adjustToTarget),
+ 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);
+ INTERPOSE_HOOK(projectile_hook, checkImpact).apply(enable);
+
+ INTERPOSE_HOOK(building_hook, getWorkshopProfile).apply(enable);
+ INTERPOSE_HOOK(building_hook, getStockpileLinks).apply(enable);
+ INTERPOSE_HOOK(building_hook, updateAction).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(color_ostream &out)
+{
+ 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(out);
+ return CR_OK;
+}
diff --git a/plugins/jobutils.cpp b/plugins/jobutils.cpp
index 24ad4170e..dbfe26b90 100644
--- a/plugins/jobutils.cpp
+++ b/plugins/jobutils.cpp
@@ -372,7 +372,7 @@ static command_result job_cmd(color_ostream &out, vector & parameters)
out << "Job item updated." << endl;
- if (item->item_type < 0 && minfo.isValid())
+ if (item->item_type < (df::item_type)0 && minfo.isValid())
out.printerr("WARNING: Due to a probable bug, creature & plant material subtype\n"
" is ignored unless the item type is also specified.\n");
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..89c47659d
--- /dev/null
+++ b/plugins/lua/siege-engine.lua
@@ -0,0 +1,45 @@
+local _ENV = mkmodule('plugins.siege-engine')
+
+--[[
+
+ Native functions:
+
+ * getTargetArea(building) -> point1, point2
+ * clearTargetArea(building)
+ * setTargetArea(building, point1, point2) -> true/false
+
+--]]
+
+Z_STEP_COUNT = 15
+Z_STEP = 1/31
+
+function findShotHeight(engine, target)
+ local path = { target = target, delta = 0.0 }
+
+ if projPathMetrics(engine, path).goal_step then
+ return path
+ end
+
+ for i = 1,Z_STEP_COUNT do
+ path.delta = i*Z_STEP
+ if projPathMetrics(engine, path).goal_step then
+ return path
+ end
+
+ path.delta = -i*Z_STEP
+ if projPathMetrics(engine, path).goal_step then
+ return path
+ end
+ end
+end
+
+function doAimProjectile(engine, item, target_min, target_max, skill)
+ print(item, df.skill_rating[skill])
+ local targets = proposeUnitHits(engine)
+ if #targets > 0 then
+ local rnd = math.random(#targets)
+ return findShotHeight(engine, targets[rnd].pos)
+ end
+end
+
+return _ENV
\ No newline at end of file
diff --git a/plugins/manipulator.cpp b/plugins/manipulator.cpp
index 1a90d2eea..f40969655 100644
--- a/plugins/manipulator.cpp
+++ b/plugins/manipulator.cpp
@@ -252,6 +252,7 @@ struct UnitInfo
enum altsort_mode {
ALTSORT_NAME,
ALTSORT_PROFESSION,
+ ALTSORT_HAPPINESS,
ALTSORT_MAX
};
@@ -275,6 +276,14 @@ bool sortByProfession (const UnitInfo *d1, const UnitInfo *d2)
return (d1->profession < d2->profession);
}
+bool sortByHappiness (const UnitInfo *d1, const UnitInfo *d2)
+{
+ if (descending)
+ return (d1->unit->status.happiness > d2->unit->status.happiness);
+ else
+ return (d1->unit->status.happiness < d2->unit->status.happiness);
+}
+
bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2)
{
if (sort_skill != job_skill::NONE)
@@ -310,6 +319,14 @@ bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2)
return sortByName(d1, d2);
}
+enum display_columns {
+ DISP_COLUMN_HAPPINESS,
+ DISP_COLUMN_NAME,
+ DISP_COLUMN_PROFESSION,
+ DISP_COLUMN_LABORS,
+ DISP_COLUMN_MAX,
+};
+
class viewscreen_unitlaborsst : public dfhack_viewscreen {
public:
void feed(set *events);
@@ -328,10 +345,11 @@ protected:
vector units;
altsort_mode altsort;
- int first_row, sel_row;
+ int first_row, sel_row, num_rows;
int first_column, sel_column;
- int height, name_width, prof_width, labors_width;
+ int col_widths[DISP_COLUMN_MAX];
+ int col_offsets[DISP_COLUMN_MAX];
void calcSize ();
};
@@ -374,34 +392,52 @@ viewscreen_unitlaborsst::viewscreen_unitlaborsst(vector &src)
void viewscreen_unitlaborsst::calcSize()
{
- height = gps->dimy - 10;
- if (height > units.size())
- height = units.size();
-
- name_width = prof_width = labors_width = 0;
- for (int i = 4; i < gps->dimx; i++)
+ num_rows = gps->dimy - 10;
+ if (num_rows > units.size())
+ num_rows = units.size();
+
+ int num_columns = gps->dimx - DISP_COLUMN_MAX - 1;
+ for (int i = 0; i < DISP_COLUMN_MAX; i++)
+ col_widths[i] = 0;
+ while (num_columns > 0)
{
- // 20% for Name, 20% for Profession, 60% for Labors
- switch ((i - 4) % 5)
+ num_columns--;
+ // need at least 4 digits for happiness
+ if (col_widths[DISP_COLUMN_HAPPINESS] < 4)
+ {
+ col_widths[DISP_COLUMN_HAPPINESS]++;
+ continue;
+ }
+ // of remaining, 20% for Name, 20% for Profession, 60% for Labors
+ switch (num_columns % 5)
{
case 0: case 2: case 4:
- labors_width++;
+ col_widths[DISP_COLUMN_LABORS]++;
break;
case 1:
- name_width++;
+ col_widths[DISP_COLUMN_NAME]++;
break;
case 3:
- prof_width++;
+ col_widths[DISP_COLUMN_PROFESSION]++;
break;
}
}
- while (labors_width > NUM_COLUMNS)
+
+ while (col_widths[DISP_COLUMN_LABORS] > NUM_COLUMNS)
+ {
+ col_widths[DISP_COLUMN_LABORS]--;
+ if (col_widths[DISP_COLUMN_LABORS] & 1)
+ col_widths[DISP_COLUMN_NAME]++;
+ else
+ col_widths[DISP_COLUMN_PROFESSION]++;
+ }
+
+ for (int i = 0; i < DISP_COLUMN_MAX; i++)
{
- if (labors_width & 1)
- name_width++;
+ if (i == 0)
+ col_offsets[i] = 1;
else
- prof_width++;
- labors_width--;
+ col_offsets[i] = col_offsets[i - 1] + col_widths[i - 1] + 1;
}
// don't adjust scroll position immediately after the window opened
@@ -409,20 +445,20 @@ void viewscreen_unitlaborsst::calcSize()
return;
// if the window grows vertically, scroll upward to eliminate blank rows from the bottom
- if (first_row > units.size() - height)
- first_row = units.size() - height;
+ if (first_row > units.size() - num_rows)
+ first_row = units.size() - num_rows;
// if it shrinks vertically, scroll downward to keep the cursor visible
- if (first_row < sel_row - height + 1)
- first_row = sel_row - height + 1;
+ if (first_row < sel_row - num_rows + 1)
+ first_row = sel_row - num_rows + 1;
// if the window grows horizontally, scroll to the left to eliminate blank columns from the right
- if (first_column > NUM_COLUMNS - labors_width)
- first_column = NUM_COLUMNS - labors_width;
+ if (first_column > NUM_COLUMNS - col_widths[DISP_COLUMN_LABORS])
+ first_column = NUM_COLUMNS - col_widths[DISP_COLUMN_LABORS];
// if it shrinks horizontally, scroll to the right to keep the cursor visible
- if (first_column < sel_column - labors_width + 1)
- first_column = sel_column - labors_width + 1;
+ if (first_column < sel_column - col_widths[DISP_COLUMN_LABORS] + 1)
+ first_column = sel_column - col_widths[DISP_COLUMN_LABORS] + 1;
}
void viewscreen_unitlaborsst::feed(set *events)
@@ -453,8 +489,8 @@ void viewscreen_unitlaborsst::feed(set *events)
if (sel_row < first_row)
first_row = sel_row;
- if (first_row < sel_row - height + 1)
- first_row = sel_row - height + 1;
+ if (first_row < sel_row - num_rows + 1)
+ first_row = sel_row - num_rows + 1;
if (events->count(interface_key::CURSOR_LEFT) || events->count(interface_key::CURSOR_UPLEFT) || events->count(interface_key::CURSOR_DOWNLEFT))
sel_column--;
@@ -489,8 +525,8 @@ void viewscreen_unitlaborsst::feed(set *events)
if (sel_column < first_column)
first_column = sel_column;
- if (first_column < sel_column - labors_width + 1)
- first_column = sel_column - labors_width + 1;
+ if (first_column < sel_column - col_widths[DISP_COLUMN_LABORS] + 1)
+ first_column = sel_column - col_widths[DISP_COLUMN_LABORS] + 1;
UnitInfo *cur = units[sel_row];
if (events->count(interface_key::SELECT) && (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE))
@@ -556,6 +592,9 @@ void viewscreen_unitlaborsst::feed(set *events)
case ALTSORT_PROFESSION:
std::sort(units.begin(), units.end(), sortByProfession);
break;
+ case ALTSORT_HAPPINESS:
+ std::sort(units.begin(), units.end(), sortByHappiness);
+ break;
}
}
if (events->count(interface_key::CHANGETAB))
@@ -566,6 +605,9 @@ void viewscreen_unitlaborsst::feed(set *events)
altsort = ALTSORT_PROFESSION;
break;
case ALTSORT_PROFESSION:
+ altsort = ALTSORT_HAPPINESS;
+ break;
+ case ALTSORT_HAPPINESS:
altsort = ALTSORT_NAME;
break;
}
@@ -603,9 +645,9 @@ void viewscreen_unitlaborsst::render()
dfhack_viewscreen::render();
Screen::clear();
- Screen::drawBorder(" Manage Labors ");
+ Screen::drawBorder(" Dwarf Manipulator - Manage Labors ");
- for (int col = 0; col < labors_width; col++)
+ for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++)
{
int col_offset = col + first_column;
if (col_offset >= NUM_COLUMNS)
@@ -620,21 +662,21 @@ void viewscreen_unitlaborsst::render()
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);
+ Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 1);
+ Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 2);
df::profession profession = columns[col_offset].profession;
- if (profession != profession::NONE)
+ if ((profession != profession::NONE) && (ui->race_id != -1))
{
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);
+ col_offsets[DISP_COLUMN_LABORS] + col, 3);
}
}
- for (int row = 0; row < height; row++)
+ for (int row = 0; row < num_rows; row++)
{
int row_offset = row + first_row;
if (row_offset >= units.size())
@@ -643,6 +685,26 @@ void viewscreen_unitlaborsst::render()
UnitInfo *cur = units[row_offset];
df::unit *unit = cur->unit;
int8_t fg = 15, bg = 0;
+
+ int happy = cur->unit->status.happiness;
+ string happiness = stl_sprintf("%4i", happy);
+ if (happy == 0) // miserable
+ fg = 13; // 5:1
+ else if (happy <= 25) // very unhappy
+ fg = 12; // 4:1
+ else if (happy <= 50) // unhappy
+ fg = 4; // 4:0
+ else if (happy < 75) // fine
+ fg = 14; // 6:1
+ else if (happy < 125) // quite content
+ fg = 6; // 6:0
+ else if (happy < 150) // happy
+ fg = 2; // 2:0
+ else // ecstatic
+ fg = 10; // 2:1
+ Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_HAPPINESS], 4 + row, happiness);
+
+ fg = 15;
if (row_offset == sel_row)
{
fg = 0;
@@ -650,18 +712,18 @@ void viewscreen_unitlaborsst::render()
}
string name = cur->name;
- name.resize(name_width);
- Screen::paintString(Screen::Pen(' ', fg, bg), 1, 4 + row, name);
+ name.resize(col_widths[DISP_COLUMN_NAME]);
+ Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_NAME], 4 + row, name);
string profession = cur->profession;
- profession.resize(prof_width);
+ profession.resize(col_widths[DISP_COLUMN_PROFESSION]);
fg = cur->color;
bg = 0;
- Screen::paintString(Screen::Pen(' ', fg, bg), 1 + name_width + 1, 4 + row, profession);
+ Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_PROFESSION], 4 + row, profession);
// Print unit's skills and labor assignments
- for (int col = 0; col < labors_width; col++)
+ for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++)
{
int col_offset = col + first_column;
fg = 15;
@@ -693,7 +755,7 @@ void viewscreen_unitlaborsst::render()
}
else
bg = 4;
- Screen::paintTile(Screen::Pen(c, fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 4 + row);
+ Screen::paintTile(Screen::Pen(c, fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 4 + row);
}
}
@@ -703,17 +765,17 @@ void viewscreen_unitlaborsst::render()
{
df::unit *unit = cur->unit;
int x = 1;
- Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->transname);
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->transname);
x += cur->transname.length();
if (cur->transname.length())
{
- Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ", ");
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ", ");
x += 2;
}
- Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->profession);
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->profession);
x += cur->profession.length();
- Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ": ");
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ": ");
x += 2;
string str;
@@ -740,7 +802,7 @@ 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), x, 3 + height + 2, str);
+ Screen::paintString(Screen::Pen(' ', 9, 0), x, 3 + num_rows + 2, str);
canToggle = (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE);
}
@@ -779,6 +841,9 @@ void viewscreen_unitlaborsst::render()
case ALTSORT_PROFESSION:
OutputString(15, x, gps->dimy - 2, "Profession");
break;
+ case ALTSORT_HAPPINESS:
+ OutputString(15, x, gps->dimy - 2, "Happiness");
+ break;
default:
OutputString(15, x, gps->dimy - 2, "Unknown");
break;
@@ -810,7 +875,7 @@ struct unitlist_hook : df::viewscreen_unitlistst
{
int x = 2;
OutputString(12, x, gps->dimy - 2, "l"); // UNITVIEW_PRF_PROF key
- OutputString(15, x, gps->dimy - 2, ": Manage labors");
+ OutputString(15, x, gps->dimy - 2, ": Manage labors (DFHack)");
}
}
};
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/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/devel/building_steam_engine.txt b/plugins/raw/building_steam_engine.txt
similarity index 100%
rename from plugins/devel/building_steam_engine.txt
rename to plugins/raw/building_steam_engine.txt
diff --git a/plugins/devel/item_trapcomp_steam_engine.txt b/plugins/raw/item_trapcomp_steam_engine.txt
similarity index 81%
rename from plugins/devel/item_trapcomp_steam_engine.txt
rename to plugins/raw/item_trapcomp_steam_engine.txt
index bae6f5b22..c35f6ef45 100644
--- a/plugins/devel/item_trapcomp_steam_engine.txt
+++ b/plugins/raw/item_trapcomp_steam_engine.txt
@@ -4,8 +4,8 @@ item_trapcomp_steam_engine
[ITEM_TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON]
[NAME:piston:pistons]
-[ADJECTIVE:huge]
-[SIZE:1600]
+[ADJECTIVE:heavy]
+[SIZE:1800]
[HITS:1]
[MATERIAL_SIZE:6]
[METAL]
diff --git a/plugins/devel/reaction_steam_engine.txt b/plugins/raw/reaction_steam_engine.txt
similarity index 100%
rename from plugins/devel/reaction_steam_engine.txt
rename to plugins/raw/reaction_steam_engine.txt
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/devel/steam-engine.cpp b/plugins/steam-engine.cpp
similarity index 88%
rename from plugins/devel/steam-engine.cpp
rename to plugins/steam-engine.cpp
index 5c344f788..cacfc6e16 100644
--- a/plugins/devel/steam-engine.cpp
+++ b/plugins/steam-engine.cpp
@@ -34,6 +34,82 @@
#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;
@@ -465,9 +541,11 @@ struct workshop_hook : df::building_workshopst {
power_rate = 0.0f;
}
// waste rate: 1-10% depending on piston assembly quality
- float waste = 0.1f - 0.015f * get_component_quality(1);
+ 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;
+ ticks *= (waste + 0.9f*power_rate)*power_level*efficiency_coeff;
// end result
return std::max(1, int(ticks));
}
diff --git a/plugins/stonesense b/plugins/stonesense
index 2a62ba5ed..37a823541 160000
--- a/plugins/stonesense
+++ b/plugins/stonesense
@@ -1 +1 @@
-Subproject commit 2a62ba5ed2607f4dbf0473e77502d4e19c19678e
+Subproject commit 37a823541538023b9f3d0d1e8039cf32851de68d
diff --git a/plugins/tiletypes.cpp b/plugins/tiletypes.cpp
index 190bda7cd..6af94f2ee 100644
--- a/plugins/tiletypes.cpp
+++ b/plugins/tiletypes.cpp
@@ -767,7 +767,7 @@ command_result executePaintJob(color_ostream &out)
}
// Remove liquid from walls, etc
- if (type != -1 && !DFHack::FlowPassable(type))
+ if (type != (df::tiletype)-1 && !DFHack::FlowPassable(type))
{
des.bits.flow_size = 0;
//des.bits.liquid_type = DFHack::liquid_water;
diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp
index 639e7c56a..98258682e 100644
--- a/plugins/workflow.cpp
+++ b/plugins/workflow.cpp
@@ -828,7 +828,7 @@ static void compute_custom_job(ProtectedJob *pj, df::job *job)
using namespace df::enums::reaction_product_item_flags;
VIRTUAL_CAST_VAR(prod, df::reaction_product_itemst, r->products[i]);
- if (!prod || (prod->item_type < 0 && !prod->flags.is_set(CRAFTS)))
+ if (!prod || (prod->item_type < (df::item_type)0 && !prod->flags.is_set(CRAFTS)))
continue;
MaterialInfo mat(prod);
diff --git a/plugins/zone.cpp b/plugins/zone.cpp
index fc89fecc1..c496f49b6 100644
--- a/plugins/zone.cpp
+++ b/plugins/zone.cpp
@@ -792,7 +792,7 @@ void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false)
bool isActivityZone(df::building * building)
{
if( building->getType() == building_type::Civzone
- && building->getSubtype() == civzone_type::ActivityZone)
+ && building->getSubtype() == (short)civzone_type::ActivityZone)
return true;
else
return false;
@@ -1603,7 +1603,7 @@ void zoneInfo(color_ostream & out, df::building* building, bool verbose)
if(building->getType()!= building_type::Civzone)
return;
- if(building->getSubtype() != civzone_type::ActivityZone)
+ if(building->getSubtype() != (short)civzone_type::ActivityZone)
return;
string name;
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/gui/liquids.lua b/scripts/gui/liquids.lua
index 869cac908..89f08b7cf 100644
--- a/scripts/gui/liquids.lua
+++ b/scripts/gui/liquids.lua
@@ -3,6 +3,7 @@
local utils = require 'utils'
local gui = require 'gui'
local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
local liquids = require('plugins.liquids')
@@ -199,6 +200,42 @@ function LiquidsUI:onRenderBody(dc)
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
@@ -239,13 +276,15 @@ function LiquidsUI:onInput(keys)
else
guidm.clearSelection()
end
- liquids.paint(
+ 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
diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua
index 4468e1dcb..c14bfcbe9 100644
--- a/scripts/gui/mechanisms.lua
+++ b/scripts/gui/mechanisms.lua
@@ -122,7 +122,7 @@ function MechanismList:onInput(keys)
end
end
-if not string.find(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..47043cbb1
--- /dev/null
+++ b/scripts/gui/siege-engine.lua
@@ -0,0 +1,490 @@
+-- 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
+
+local LEGENDARY = df.skill_rating.Legendary
+
+-- Globals kept between script calls
+last_target_min = last_target_min or nil
+last_target_max = last_target_max or nil
+
+local item_choices = {
+ { caption = 'boulders (default)', item_type = df.item_type.BOULDER },
+ { caption = 'blocks', item_type = df.item_type.BLOCKS },
+ { caption = 'weapons', item_type = df.item_type.WEAPON },
+ { caption = 'trap components', item_type = df.item_type.TRAPCOMP },
+ { caption = 'bins', item_type = df.item_type.BIN },
+ { caption = 'barrels', item_type = df.item_type.BARREL },
+ { caption = 'anything', item_type = -1 },
+}
+
+local item_choice_idx = {}
+for i,v in ipairs(item_choices) do
+ item_choice_idx[v.item_type] = i
+end
+
+SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay)
+
+SiegeEngine.focus_path = 'siege-engine'
+
+function SiegeEngine:init(building)
+ self:init_fields{
+ building = building,
+ center = utils.getBuildingCenter(building),
+ selected_pile = 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',
+ }
+ self.mode_pile = {
+ render = self:callback 'onRenderBody_pile',
+ input = self:callback 'onInput_pile',
+ }
+ 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()
+ if self.save_profile then
+ plugin.saveWorkshopProfile(self.building)
+ end
+ if not self.no_select_building then
+ self:selectBuilding(self.building, self.old_cursor, self.old_viewport, 10)
+ end
+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)
+ local pos = plugin.adjustToTarget(self.building, xyz2pos(cx,cy,cz))
+ self:centerViewOn(pos)
+ 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:scrollPiles(delta)
+ local links = plugin.getStockpileLinks(self.building)
+ if links then
+ self.selected_pile = 1+(self.selected_pile+delta-1) % #links
+ return links[self.selected_pile]
+ end
+end
+
+function SiegeEngine:renderStockpiles(dc, links, nlines)
+ local idx = (self.selected_pile-1) % #links
+ local page = math.floor(idx/nlines)
+ for i = page*nlines,math.min(#links,(page+1)*nlines)-1 do
+ local color = COLOR_BROWN
+ if i == idx then
+ color = COLOR_YELLOW
+ end
+ dc:newline(2):string(utils.getBuildingName(links[i+1]), color)
+ 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
+
+ dc:newline():newline(1)
+ if self.building.type == df.siegeengine_type.Ballista then
+ dc:string("Uses ballista arrows")
+ else
+ local item = plugin.getAmmoItem(self.building)
+ dc:string("u",COLOR_LIGHTGREEN):string(": Use ")
+ if item_choice_idx[item] then
+ dc:string(item_choices[item_choice_idx[item]].caption)
+ else
+ dc:string(df.item_type[item])
+ end
+ end
+
+ dc:newline():newline(1)
+ dc:string("t",COLOR_LIGHTGREEN):string(": Take from stockpile"):newline(3)
+ local links = plugin.getStockpileLinks(self.building)
+ local bottom = dc.height - 5
+ if links then
+ dc:string("d",COLOR_LIGHTGREEN):string(": Delete, ")
+ dc:string("o",COLOR_LIGHTGREEN):string(": Zoom"):newline()
+ self:renderStockpiles(dc, links, bottom-2-dc:localY())
+ dc:newline():newline()
+ end
+
+ local prof = self.building:getWorkshopProfile() or {}
+ dc:seek(1,math.max(dc:localY(),19)):string('ghjk',COLOR_LIGHTGREEN)dc:string(': ')
+ dc:string(df.skill_rating.attrs[prof.min_level or 0].caption):string('-')
+ dc:string(df.skill_rating.attrs[math.min(LEGENDARY,prof.max_level or 3000)].caption)
+ dc:newline():newline()
+
+ 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:setAmmoItem(choice)
+ if self.building.type == df.siegeengine_type.Ballista then
+ return
+ end
+
+ if not plugin.setAmmoItem(self.building, choice.item_type) then
+ dlg.showMessage(
+ 'Set Ammo Item',
+ 'Could not set the ammo item', COLOR_LIGHTRED
+ )
+ 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_U then
+ local item = plugin.getAmmoItem(self.building)
+ local idx = 1 + (item_choice_idx[item] or 0) % #item_choices
+ self:setAmmoItem(item_choices[idx])
+ elseif keys.CUSTOM_Z then
+ self:zoomToTarget()
+ elseif keys.CUSTOM_X then
+ plugin.clearTargetArea(self.building)
+ elseif keys.SECONDSCROLL_UP then
+ self:scrollPiles(-1)
+ elseif keys.SECONDSCROLL_DOWN then
+ self:scrollPiles(1)
+ elseif keys.CUSTOM_D then
+ local pile = self:scrollPiles(0)
+ if pile then
+ plugin.removeStockpileLink(self.building, pile)
+ end
+ elseif keys.CUSTOM_O then
+ local pile = self:scrollPiles(0)
+ if pile then
+ self:centerViewOn(utils.getBuildingCenter(pile))
+ end
+ elseif keys.CUSTOM_T then
+ self:showCursor(true)
+ self.mode = self.mode_pile
+ self:sendInputToParent('CURSOR_DOWN_Z')
+ self:sendInputToParent('CURSOR_UP_Z')
+ elseif keys.CUSTOM_G then
+ local prof = plugin.saveWorkshopProfile(self.building)
+ prof.min_level = math.max(0, prof.min_level-1)
+ plugin.saveWorkshopProfile(self.building)
+ elseif keys.CUSTOM_H then
+ local prof = plugin.saveWorkshopProfile(self.building)
+ prof.min_level = math.min(LEGENDARY, prof.min_level+1)
+ plugin.saveWorkshopProfile(self.building)
+ elseif keys.CUSTOM_J then
+ local prof = plugin.saveWorkshopProfile(self.building)
+ prof.max_level = math.max(0, math.min(LEGENDARY,prof.max_level)-1)
+ plugin.saveWorkshopProfile(self.building)
+ elseif keys.CUSTOM_K then
+ local prof = plugin.saveWorkshopProfile(self.building)
+ prof.max_level = math.min(LEGENDARY, prof.max_level+1)
+ if prof.max_level >= LEGENDARY then prof.max_level = 3000 end
+ plugin.saveWorkshopProfile(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" },
+ semi_blocked = { pen = COLOR_BROWN, msg = "Partially 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_pile(dc)
+ dc:newline(1):string('Select pile to take from'):newline():newline(2)
+
+ local sel = df.global.world.selected_building
+
+ if df.building_stockpilest:is_instance(sel) then
+ dc:string(utils.getBuildingName(sel), COLOR_GREEN):newline():newline(1)
+
+ if plugin.isLinkedToPile(self.building, sel) then
+ dc:string("Already taking from here"):newline():newline(2)
+ dc:string("d", COLOR_LIGHTGREEN):string(": Delete link")
+ else
+ dc:string("Enter",COLOR_LIGHTGREEN):string(": Take from this pile")
+ end
+ elseif sel then
+ dc:string(utils.getBuildingName(sel), COLOR_DARKGREY)
+ dc:newline():newline(1)
+ dc:string("Not a stockpile",COLOR_LIGHTRED)
+ else
+ dc:string("No building selected", COLOR_DARKGREY)
+ end
+end
+
+function SiegeEngine:onInput_pile(keys)
+ if keys.SELECT then
+ local sel = df.global.world.selected_building
+ if df.building_stockpilest:is_instance(sel)
+ and not plugin.isLinkedToPile(self.building, sel) then
+ plugin.addStockpileLink(self.building, sel)
+
+ df.global.world.selected_building = self.building
+ self.mode = self.mode_main
+ self:showCursor(false)
+ end
+ elseif keys.CUSTOM_D then
+ local sel = df.global.world.selected_building
+ if df.building_stockpilest:is_instance(sel) then
+ plugin.removeStockpileLink(self.building, sel)
+ end
+ elseif keys.LEAVESCREEN then
+ df.global.world.selected_building = self.building
+ self.mode = self.mode_main
+ self:showCursor(false)
+ elseif self:propagateMoveKeys(keys) then
+ --
+ 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()
+ elseif keys.LEAVESCREEN_ALL then
+ self:dismiss()
+ self.no_select_building = true
+ guidm.clearCursorPos()
+ df.global.ui.main.mode = df.ui_sidebar_mode.Default
+ df.global.world.selected_building = nil
+ 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()