diff --git a/LUA_API.rst b/LUA_API.rst
index 4d9170d6e..c5f9a1c58 100644
--- a/LUA_API.rst
+++ b/LUA_API.rst
@@ -868,6 +868,26 @@ Units module
Returns the nemesis record of the unit if it has one, or *nil*.
+* ``dfhack.units.isHidingCurse(unit)``
+
+ Checks if the unit hides improved attributes from its curse.
+
+* ``dfhack.units.getPhysicalAttrValue(unit, attr_type)``
+* ``dfhack.units.getMentalAttrValue(unit, attr_type)``
+
+ Computes the effective attribute value, including curse effect.
+
+* ``dfhack.units.isCrazed(unit)``
+* ``dfhack.units.isOpposedToLife(unit)``
+* ``dfhack.units.hasExtravision(unit)``
+* ``dfhack.units.isBloodsucker(unit)``
+
+ Simple checks of caste attributes that can be modified by curses.
+
+* ``dfhack.units.getMiscTrait(unit, type[, create])``
+
+ Finds (or creates if requested) a misc trait object with the given id.
+
* ``dfhack.units.isDead(unit)``
The unit is completely dead and passive, or a ghost.
@@ -894,6 +914,14 @@ Units module
Returns the age of the unit in years as a floating-point value.
If ``true_age`` is true, ignores false identities.
+* ``dfhack.units.getEffectiveSkill(unit, skill)``
+
+ Computes the effective rating for the given skill, taking into account exhaustion, pain etc.
+
+* ``dfhack.units.computeMovementSpeed(unit)``
+
+ Computes number of frames * 100 it takes the unit to move in its current state of mind and body.
+
* ``dfhack.units.getNoblePositions(unit)``
Returns a list of tables describing noble position assignments, or *nil*.
@@ -971,6 +999,14 @@ Items module
Move the item to the unit inventory. Returns *false* if impossible.
+* ``dfhack.items.remove(item[, no_uncat])``
+
+ Removes the item, and marks it for garbage collection unless ``no_uncat`` is true.
+
+* ``dfhack.items.makeProjectile(item)``
+
+ Turns the item into a projectile, and returns the new object, or *nil* if impossible.
+
Maps module
-----------
diff --git a/Lua API.html b/Lua API.html
index dc9c8d73e..07f038bc4 100644
--- a/Lua API.html
+++ b/Lua API.html
@@ -1106,6 +1106,26 @@ a lua list containing them.
dfhack.units.getNemesis(unit)
Returns the nemesis record of the unit if it has one, or nil.
+dfhack.units.isHidingCurse(unit)
+Checks if the unit hides improved attributes from its curse.
+
+dfhack.units.getPhysicalAttrValue(unit, attr_type)
+
+dfhack.units.getMentalAttrValue(unit, attr_type)
+Computes the effective attribute value, including curse effect.
+
+dfhack.units.isCrazed(unit)
+
+dfhack.units.isOpposedToLife(unit)
+
+dfhack.units.hasExtravision(unit)
+
+dfhack.units.isBloodsucker(unit)
+Simple checks of caste attributes that can be modified by curses.
+
+dfhack.units.getMiscTrait(unit, type[, create])
+Finds (or creates if requested) a misc trait object with the given id.
+
dfhack.units.isDead(unit)
The unit is completely dead and passive, or a ghost.
@@ -1126,6 +1146,12 @@ same checks the game uses to decide game-over by extinction.
Returns the age of the unit in years as a floating-point value.
If true_age is true, ignores false identities.
+dfhack.units.getEffectiveSkill(unit, skill)
+Computes the effective rating for the given skill, taking into account exhaustion, pain etc.
+
+dfhack.units.computeMovementSpeed(unit)
+Computes number of frames * 100 it takes the unit to move in its current state of mind and body.
+
dfhack.units.getNoblePositions(unit)
Returns a list of tables describing noble position assignments, or nil.
Every table has fields entity, assignment and position.
@@ -1187,6 +1213,12 @@ Returns false in case of error.
dfhack.items.moveToInventory(item,unit,use_mode,body_part)
Move the item to the unit inventory. Returns false if impossible.
+dfhack.items.remove(item[, no_uncat])
+Removes the item, and marks it for garbage collection unless no_uncat is true.
+
+dfhack.items.makeProjectile(item)
+Turns the item into a projectile, and returns the new object, or nil if impossible.
+
diff --git a/NEWS b/NEWS
new file mode 100644
index 000000000..22f64d7d6
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,75 @@
+DFHack v0.34.11-r2 (UNRELEASED)
+
+ Internals:
+ - full support for Mac OS X.
+ - a plugin that adds scripting in ruby.
+ - support for interposing virtual methods in DF from C++ plugins.
+ - support for creating new interface screens from C++ and lua.
+ - added various other API functions.
+ Notable bugfixes:
+ - better terminal reset after exit on linux.
+ - seedwatch now works on reclaim.
+ - the sort plugin won't crash on cages anymore.
+ Misc improvements:
+ - autodump: can move items to any walkable tile, not just floors.
+ - stripcaged: by default keep armor, new dumparmor option.
+ - zone: allow non-domesticated birds in nestboxes.
+ - workflow: quality range in constraints.
+ - cleanplants: new command to remove rain water from plants.
+ - liquids: can paint permaflow, i.e. what makes rivers power water wheels.
+ - prospect: pre-embark prospector accounts for caves & magma sea in its estimate.
+ - rename: supports renaming stockpiles, workshops, traps, siege engines.
+ New tweaks:
+ - tweak stable-cursor: keeps exact cursor position between d/k/t/q/v etc menus.
+ - tweak patrol-duty: makes Train orders reduce patrol timer, like the binary patch does.
+ - tweak readable-build-plate: fix unreadable truncation in unit pressure plate build ui.
+ - tweak stable-temp: fixes bug 6012; may improve FPS by 50-100% on a slow item-heavy fort.
+ - tweak fast-heat: speeds up item heating & cooling, thus making stable-temp act faster.
+ - tweak fix-dimensions: fixes subtracting small amounts from stacked liquids etc.
+ - tweak advmode-contained: fixes UI bug in custom reactions with container inputs in advmode.
+ New scripts:
+ - fixnaked: removes thoughts about nakedness.
+ - setfps: set FPS cap at runtime, in case you want slow motion or speed-up.
+ - fix/population-cap: run after every migrant wave to prevent exceeding the cap.
+ - fix/stable-temp: counts items with temperature updates; does instant one-shot stable-temp.
+ New GUI scripts:
+ - gui/mechanisms: browse mechanism links of the current building.
+ - gui/room-list: browse other rooms owned by the unit when assigning one.
+ - gui/liquids: a GUI front-end for the liquids plugin.
+ - gui/rename: renaming stockpiles, workshops and units via an in-game dialog.
+ - gui/power-meter: front-end for the Power Meter plugin.
+ - gui/siege-engine: front-end for the Siege Engine plugin.
+ Autolabor plugin:
+ - can set nonidle hauler percentage.
+ - broker excluded from all labors when needed at depot.
+ - likewise, anybody with a scheduled diplomat meeting.
+ New Dwarf Manipulator plugin:
+ Open the unit list, and press 'l' to access a Dwarf Therapist like UI in the game.
+ New Steam Engine plugin:
+ Dwarven Water Reactors don't make any sense whatsoever and cause lag, so this may be
+ a replacement for those concerned by it. The plugin detects if a workshop with a
+ certain name is in the raws used by the current world, and provides the necessary
+ behavior. See hack/raw/*_steam_engine.txt for the necessary raw definitions.
+ Note: Stuff like animal treadmills might be more period, but absolutely can't be
+ done with tools dfhack has access to.
+ New Power Meter plugin:
+ When activated, implements a pressure plate modification that detects power in gear
+ boxes built on the four adjacent N/S/W/E tiles. The gui/power-meter script implements
+ the necessary build configuration UI.
+ New Siege Engine plugin:
+ When enabled and configured via gui/siege-engine, allows aiming siege engines
+ at a designated rectangular area with 360 degree fire range and across Z levels;
+ this works by rewriting the projectile trajectory immediately after it appears.
+ Also supports loading catapults with non-boulder projectiles, taking from a stockpile,
+ and restricting operator skill range like with ordinary workshops.
+ Disclaimer: not in any way to undermine the future siege update from Toady, but
+ the aiming logic of existing engines hasn't been updated since 2D, and is almost
+ useless above ground :(. Again, things like making siegers bring their own engines
+ is totally out of the scope of dfhack and can only be done by Toady.
+ New Add Spatter plugin:
+ Detects reactions with certain names in the raws, and changes them from adding
+ improvements to adding item contaminants. This allows directly covering items
+ with poisons. The added spatters are immune both to water and 'clean items'.
+ Intended to give some use to all those giant cave spider poison barrels brought
+ by the caravans.
+
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 658f6e9be..83c3641b6 100644
--- a/dfhack.init-example
+++ b/dfhack.init-example
@@ -51,7 +51,7 @@ keybinding add Shift-G "job-material GLASS_GREEN"
keybinding add Ctrl-M@dwarfmode/QueryBuilding/Some gui/mechanisms
# browse rooms of same owner
-keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list.work
+keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list
# interface for the liquids plugin
keybinding add Alt-L@dwarfmode/LookAround gui/liquids
@@ -59,12 +59,12 @@ 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
-# interface for universal game master's editor
-keybinding add Alt-Shift-E gui/gm-editor
+# siege engine control
+keybinding add Alt-A@dwarfmode/QueryBuilding/Some/SiegeEngine gui/siege-engine
-###################
-# UI logic tweaks #
-###################
+############################
+# UI and game logic tweaks #
+############################
# stabilize the cursor of dwarfmode when switching menus
tweak stable-cursor
@@ -77,3 +77,16 @@ tweak readable-build-plate
# improve FPS by squashing endless item temperature update loops
tweak stable-temp
+
+# speed up items reaching temp equilibrium with environment by
+# capping the rate to no less than 1 degree change per 500 frames
+# Note: will also cause stuff to melt faster in magma etc
+tweak fast-heat 500
+
+# stop stacked liquid/bar/thread/cloth items from lasting forever
+# if used in reactions that use only a fraction of the dimension.
+tweak fix-dimensions
+
+# make reactions requiring containers usable in advmode - the issue is
+# that the screen asks for those reagents to be selected directly
+tweak advmode-contained
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 807cbf539..d73d14cf9 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
@@ -813,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),
@@ -876,6 +886,18 @@ static bool items_moveToInventory
return Items::moveToInventory(mc, item, unit, mode, body_part);
}
+static bool items_remove(df::item *item, bool no_uncat)
+{
+ MapExtras::MapCache mc;
+ return Items::remove(mc, item, no_uncat);
+}
+
+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),
@@ -887,6 +909,8 @@ static const LuaWrapper::FunctionReg dfhack_items_module[] = {
WRAPN(moveToContainer, items_moveToContainer),
WRAPN(moveToBuilding, items_moveToBuilding),
WRAPN(moveToInventory, items_moveToInventory),
+ WRAPN(makeProjectile, items_makeProjectile),
+ WRAPN(remove, items_remove),
{ NULL, NULL }
};
@@ -1060,7 +1084,9 @@ static int buildings_getCorrectSize(lua_State *state)
return 5;
}
-static int buildings_setSize(lua_State *state)
+namespace {
+
+int buildings_setSize(lua_State *state)
{
auto bld = Lua::CheckDFObject(state, 1);
df::coord2d size(luaL_optint(state, 2, 1), luaL_optint(state, 3, 1));
@@ -1081,11 +1107,13 @@ static int buildings_setSize(lua_State *state)
return 1;
}
+}
+
static const luaL_Reg dfhack_buildings_funcs[] = {
{ "findAtTile", buildings_findAtTile },
{ "findCivzonesAt", buildings_findCivzonesAt },
{ "getCorrectSize", buildings_getCorrectSize },
- { "setSize", buildings_setSize },
+ { "setSize", &Lua::CallWithCatchWrapper },
{ NULL, NULL }
};
diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp
index 4a66470f9..1fecbab78 100644
--- a/library/Process-linux.cpp
+++ b/library/Process-linux.cpp
@@ -127,6 +127,9 @@ void Process::getMemRanges( vector & ranges )
char permissions[5]; // r/-, w/-, x/-, p/s, 0
FILE *mapFile = ::fopen("/proc/self/maps", "r");
+ if (!mapFile)
+ return;
+
size_t start, end, offset, device1, device2, node;
while (fgets(buffer, 1024, mapFile))
@@ -148,6 +151,8 @@ void Process::getMemRanges( vector & ranges )
temp.valid = true;
ranges.push_back(temp);
}
+
+ fclose(mapFile);
}
uint32_t Process::getBase()
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/VTableInterpose.cpp b/library/VTableInterpose.cpp
index 583ef5184..046425653 100644
--- a/library/VTableInterpose.cpp
+++ b/library/VTableInterpose.cpp
@@ -438,6 +438,8 @@ void VMethodInterposeLinkBase::remove()
if (next)
prev->child_next.insert(next);
+ else
+ prev->child_hosts.insert(host);
}
}
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/DataFuncs.h b/library/include/DataFuncs.h
index 52039566c..01a798e34 100644
--- a/library/include/DataFuncs.h
+++ b/library/include/DataFuncs.h
@@ -85,7 +85,7 @@ namespace df {
static const bool is_method = true; \
};
-#define INSTANTIATE_WRAPPERS(Count, FArgs, Args, Loads) \
+#define INSTANTIATE_WRAPPERS2(Count, FArgs, Args, Loads) \
template struct function_wrapper { \
static const int num_args = Count; \
static void execute(lua_State *state, int base, void (*cb) FArgs) { Loads; INVOKE_VOID(cb Args); } \
@@ -105,79 +105,103 @@ namespace df {
LOAD_CLASS(); Loads; INVOKE_RV((self->*cb) Args); } \
};
+#define INSTANTIATE_WRAPPERS(Count, FArgs, OFArgs, Args, OArgs, Loads) \
+ INSTANTIATE_WRAPPERS2(Count, FArgs, Args, Loads) \
+ INSTANTIATE_WRAPPERS2(Count, OFArgs, OArgs, LOAD_OSTREAM(out); Loads)
+
#define FW_TARGSC
#define FW_TARGS
INSTANTIATE_RETURN_TYPE(())
-INSTANTIATE_WRAPPERS(0, (), (), ;)
-INSTANTIATE_WRAPPERS(0, (OSTREAM_ARG), (out), LOAD_OSTREAM(out);)
+INSTANTIATE_WRAPPERS(0, (), (OSTREAM_ARG), (), (out), ;)
#undef FW_TARGS
#undef FW_TARGSC
#define FW_TARGSC FW_TARGS,
#define FW_TARGS class A1
INSTANTIATE_RETURN_TYPE((A1))
-INSTANTIATE_WRAPPERS(1, (A1), (vA1), LOAD_ARG(A1);)
-INSTANTIATE_WRAPPERS(1, (OSTREAM_ARG,A1), (out,vA1), LOAD_OSTREAM(out); LOAD_ARG(A1);)
+INSTANTIATE_WRAPPERS(1, (A1), (OSTREAM_ARG,A1), (vA1), (out,vA1), LOAD_ARG(A1);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2
INSTANTIATE_RETURN_TYPE((A1,A2))
-INSTANTIATE_WRAPPERS(2, (A1,A2), (vA1,vA2), LOAD_ARG(A1); LOAD_ARG(A2);)
-INSTANTIATE_WRAPPERS(2, (OSTREAM_ARG,A1,A2), (out,vA1,vA2),
- LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2);)
+INSTANTIATE_WRAPPERS(2, (A1,A2), (OSTREAM_ARG,A1,A2), (vA1,vA2), (out,vA1,vA2),
+ LOAD_ARG(A1); LOAD_ARG(A2);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3
INSTANTIATE_RETURN_TYPE((A1,A2,A3))
-INSTANTIATE_WRAPPERS(3, (A1,A2,A3), (vA1,vA2,vA3), LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);)
-INSTANTIATE_WRAPPERS(3, (OSTREAM_ARG,A1,A2,A3), (out,vA1,vA2,vA3),
- LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);)
+INSTANTIATE_WRAPPERS(3, (A1,A2,A3), (OSTREAM_ARG,A1,A2,A3), (vA1,vA2,vA3), (out,vA1,vA2,vA3),
+ LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4))
-INSTANTIATE_WRAPPERS(4, (A1,A2,A3,A4), (vA1,vA2,vA3,vA4),
+INSTANTIATE_WRAPPERS(4, (A1,A2,A3,A4), (OSTREAM_ARG,A1,A2,A3,A4),
+ (vA1,vA2,vA3,vA4), (out,vA1,vA2,vA3,vA4),
LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);)
-INSTANTIATE_WRAPPERS(4, (OSTREAM_ARG,A1,A2,A3,A4), (out,vA1,vA2,vA3,vA4),
- LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5))
-INSTANTIATE_WRAPPERS(5, (A1,A2,A3,A4,A5), (vA1,vA2,vA3,vA4,vA5),
- LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4); LOAD_ARG(A5);)
-INSTANTIATE_WRAPPERS(5, (OSTREAM_ARG,A1,A2,A3,A4,A5), (out,vA1,vA2,vA3,vA4,vA5),
- LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2);
- LOAD_ARG(A3); LOAD_ARG(A4); LOAD_ARG(A5);)
+INSTANTIATE_WRAPPERS(5, (A1,A2,A3,A4,A5), (OSTREAM_ARG,A1,A2,A3,A4,A5),
+ (vA1,vA2,vA3,vA4,vA5), (out,vA1,vA2,vA3,vA4,vA5),
+ LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
+ LOAD_ARG(A5);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6))
-INSTANTIATE_WRAPPERS(6, (A1,A2,A3,A4,A5,A6), (vA1,vA2,vA3,vA4,vA5,vA6),
- LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);
- LOAD_ARG(A4); LOAD_ARG(A5); LOAD_ARG(A6);)
-INSTANTIATE_WRAPPERS(6, (OSTREAM_ARG,A1,A2,A3,A4,A5,A6), (out,vA1,vA2,vA3,vA4,vA5,vA6),
- LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);
- LOAD_ARG(A4); LOAD_ARG(A5); LOAD_ARG(A6);)
+INSTANTIATE_WRAPPERS(6, (A1,A2,A3,A4,A5,A6), (OSTREAM_ARG,A1,A2,A3,A4,A5,A6),
+ (vA1,vA2,vA3,vA4,vA5,vA6), (out,vA1,vA2,vA3,vA4,vA5,vA6),
+ LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
+ LOAD_ARG(A5); LOAD_ARG(A6);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6, class A7
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6,A7))
-INSTANTIATE_WRAPPERS(7, (A1,A2,A3,A4,A5,A6,A7), (vA1,vA2,vA3,vA4,vA5,vA6,vA7),
- LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);
- LOAD_ARG(A4); LOAD_ARG(A5); LOAD_ARG(A6);
- LOAD_ARG(A7);)
-INSTANTIATE_WRAPPERS(7, (OSTREAM_ARG,A1,A2,A3,A4,A5,A6,A7), (out,vA1,vA2,vA3,vA4,vA5,vA6,vA7),
- LOAD_OSTREAM(out); LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3);
- LOAD_ARG(A4); LOAD_ARG(A5); LOAD_ARG(A6); LOAD_ARG(A7);)
+INSTANTIATE_WRAPPERS(7, (A1,A2,A3,A4,A5,A6,A7), (OSTREAM_ARG,A1,A2,A3,A4,A5,A6,A7),
+ (vA1,vA2,vA3,vA4,vA5,vA6,vA7), (out,vA1,vA2,vA3,vA4,vA5,vA6,vA7),
+ LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
+ LOAD_ARG(A5); LOAD_ARG(A6); LOAD_ARG(A7);)
#undef FW_TARGS
#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6, class A7, class A8
INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6,A7,A8))
+INSTANTIATE_WRAPPERS(8, (A1,A2,A3,A4,A5,A6,A7,A8), (OSTREAM_ARG,A1,A2,A3,A4,A5,A6,A7,A8),
+ (vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8), (out,vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8),
+ LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
+ LOAD_ARG(A5); LOAD_ARG(A6); LOAD_ARG(A7); LOAD_ARG(A8);)
+#undef FW_TARGS
+
+#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6, class A7, class A8, class A9
+INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6,A7,A8,A9))
+INSTANTIATE_WRAPPERS(9, (A1,A2,A3,A4,A5,A6,A7,A8,A9),
+ (OSTREAM_ARG,A1,A2,A3,A4,A5,A6,A7,A8,A9),
+ (vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8,vA9),
+ (out,vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8,vA9),
+ LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
+ LOAD_ARG(A5); LOAD_ARG(A6); LOAD_ARG(A7); LOAD_ARG(A8);
+ LOAD_ARG(A9);)
+#undef FW_TARGS
+
+#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6, class A7, class A8, class A9, class A10
+INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6,A7,A8,A9,A10))
+INSTANTIATE_WRAPPERS(10, (A1,A2,A3,A4,A5,A6,A7,A8,A9,A10),
+ (OSTREAM_ARG,A1,A2,A3,A4,A5,A6,A7,A8,A9,A10),
+ (vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8,vA9,vA10),
+ (out,vA1,vA2,vA3,vA4,vA5,vA6,vA7,vA8,vA9,vA10),
+ LOAD_ARG(A1); LOAD_ARG(A2); LOAD_ARG(A3); LOAD_ARG(A4);
+ LOAD_ARG(A5); LOAD_ARG(A6); LOAD_ARG(A7); LOAD_ARG(A8);
+ LOAD_ARG(A9); LOAD_ARG(A10);)
+#undef FW_TARGS
+
+#define FW_TARGS class A1, class A2, class A3, class A4, class A5, class A6, class A7, class A8, class A9, class A10, class A11
+INSTANTIATE_RETURN_TYPE((A1,A2,A3,A4,A5,A6,A7,A8,A9,A10,A11))
#undef FW_TARGS
#undef FW_TARGSC
#undef INSTANTIATE_WRAPPERS
+#undef INSTANTIATE_WRAPPERS2
#undef INVOKE_VOID
#undef INVOKE_RV
#undef LOAD_CLASS
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/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..81c8e1285 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,11 @@ 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);
+
+/// Makes the item removed and marked for garbage collection
+DFHACK_EXPORT bool remove(MapExtras::MapCache &mc, df::item *item, bool no_uncat = false);
+
+/// 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/Materials.h b/library/include/modules/Materials.h
index 76c89de30..fb5a6353c 100644
--- a/library/include/modules/Materials.h
+++ b/library/include/modules/Materials.h
@@ -131,6 +131,11 @@ namespace DFHack
bool findPlant(const std::string &token, const std::string &subtoken);
bool findCreature(const std::string &token, const std::string &subtoken);
+ bool findProduct(df::material *material, const std::string &name);
+ bool findProduct(const MaterialInfo &info, const std::string &name) {
+ return findProduct(info.material, name);
+ }
+
std::string getToken();
std::string toString(uint16_t temp = 10015, bool named = true);
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/class.lua b/library/lua/class.lua
new file mode 100644
index 000000000..7b142e499
--- /dev/null
+++ b/library/lua/class.lua
@@ -0,0 +1,150 @@
+-- A trivial reloadable class system
+
+local _ENV = mkmodule('class')
+
+-- Metatable template for a class
+class_obj = {} or class_obj
+
+-- Methods shared by all classes
+common_methods = {} or common_methods
+
+-- Forbidden names for class fields and methods.
+reserved_names = { super = true, ATTRS = true }
+
+-- Attribute table metatable
+attrs_meta = {} or attrs_meta
+
+-- Create or updates a class; a class has metamethods and thus own metatable.
+function defclass(class,parent)
+ class = class or {}
+
+ local meta = getmetatable(class)
+ if not meta then
+ meta = {}
+ setmetatable(class, meta)
+ end
+
+ for k,v in pairs(class_obj) do meta[k] = v end
+
+ meta.__index = parent or common_methods
+
+ local attrs = rawget(class, 'ATTRS') or {}
+ setmetatable(attrs, attrs_meta)
+
+ rawset(class, 'super', parent)
+ rawset(class, 'ATTRS', attrs)
+ rawset(class, '__index', rawget(class, '__index') or class)
+
+ return class
+end
+
+-- An instance uses the class as metatable
+function mkinstance(class,table)
+ table = table or {}
+ setmetatable(table, class)
+ return table
+end
+
+-- Patch the stubs in the global environment
+dfhack.BASE_G.defclass = _ENV.defclass
+dfhack.BASE_G.mkinstance = _ENV.mkinstance
+
+-- Just verify the name, and then set.
+function class_obj:__newindex(name,val)
+ if reserved_names[name] or common_methods[name] then
+ error('Method name '..name..' is reserved.')
+ end
+ rawset(self, name, val)
+end
+
+function attrs_meta:__call(attrs)
+ for k,v in pairs(attrs) do
+ self[k] = v
+ end
+end
+
+local function apply_attrs(obj, attrs, init_table)
+ for k,v in pairs(attrs) do
+ if v == DEFAULT_NIL then
+ v = nil
+ end
+ obj[k] = init_table[k] or v
+ end
+end
+
+local function invoke_before_rec(self, class, method, ...)
+ local meta = getmetatable(class)
+ if meta then
+ local fun = rawget(class, method)
+ if fun then
+ fun(self, ...)
+ end
+
+ invoke_before_rec(self, meta.__index, method, ...)
+ end
+end
+
+local function invoke_after_rec(self, class, method, ...)
+ local meta = getmetatable(class)
+ if meta then
+ invoke_after_rec(self, meta.__index, method, ...)
+
+ local fun = rawget(class, method)
+ if fun then
+ fun(self, ...)
+ end
+ end
+end
+
+local function init_attrs_rec(obj, class, init_table)
+ local meta = getmetatable(class)
+ if meta then
+ init_attrs_rec(obj, meta.__index, init_table)
+ apply_attrs(obj, rawget(class, 'ATTRS'), init_table)
+ end
+end
+
+-- Call metamethod constructs the object
+function class_obj:__call(init_table)
+ -- The table is assumed to be a scratch temporary.
+ -- If it is not, copy it yourself before calling.
+ init_table = init_table or {}
+
+ local obj = mkinstance(self)
+
+ -- This initialization sequence is broadly based on how the
+ -- Common Lisp initialize-instance generic function works.
+
+ -- preinit screens input arguments in subclass to superclass order
+ invoke_before_rec(obj, self, 'preinit', init_table)
+ -- initialize the instance table from init table
+ init_attrs_rec(obj, self, init_table)
+ -- init in superclass -> subclass
+ invoke_after_rec(obj, self, 'init', init_table)
+ -- postinit in superclass -> subclass
+ invoke_after_rec(obj, self, 'postinit', init_table)
+
+ return obj
+end
+
+-- Common methods for all instances:
+
+function common_methods:callback(method, ...)
+ return dfhack.curry(self[method], self, ...)
+end
+
+function common_methods:assign(data)
+ for k,v in pairs(data) do
+ self[k] = v
+ end
+end
+
+function common_methods:invoke_before(method, ...)
+ return invoke_before_rec(self, getmetatable(self), method, ...)
+end
+
+function common_methods:invoke_after(method, ...)
+ return invoke_after_rec(self, getmetatable(self), method, ...)
+end
+
+return _ENV
diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua
index e96bb0f4b..ce3be5a87 100644
--- a/library/lua/dfhack.lua
+++ b/library/lua/dfhack.lua
@@ -113,26 +113,14 @@ function rawset_default(target,source)
end
end
-function defclass(class,parent)
- class = class or {}
- rawset_default(class, { __index = class })
- if parent then
- setmetatable(class, parent)
- else
- rawset_default(class, {
- init_fields = rawset_default,
- callback = function(self, name, ...)
- return dfhack.curry(self[name], self, ...)
- end
- })
- end
- return class
+DEFAULT_NIL = DEFAULT_NIL or {} -- Unique token
+
+function defclass(...)
+ return require('class').defclass(...)
end
-function mkinstance(class,table)
- table = table or {}
- setmetatable(table, class)
- return table
+function mkinstance(...)
+ return require('class').mkinstance(...)
end
-- Misc functions
@@ -169,6 +157,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 f9b6ab6d2..6eaa98606 100644
--- a/library/lua/gui.lua
+++ b/library/lua/gui.lua
@@ -74,18 +74,23 @@ end
Painter = defclass(Painter, nil)
-function Painter.new(rect, pen)
- rect = rect or mkdims_wh(0,0,dscreen.getWindowSize())
- local self = {
- x1 = rect.x1, clip_x1 = rect.x1,
- y1 = rect.y1, clip_y1 = rect.y1,
- x2 = rect.x2, clip_x2 = rect.x2,
- y2 = rect.y2, clip_y2 = rect.y2,
+function Painter:init(args)
+ local rect = args.rect or mkdims_wh(0,0,dscreen.getWindowSize())
+ local crect = args.clip_rect or rect
+ self:assign{
+ x = rect.x1, y = rect.y1,
+ x1 = rect.x1, clip_x1 = crect.x1,
+ y1 = rect.y1, clip_y1 = crect.y1,
+ x2 = rect.x2, clip_x2 = crect.x2,
+ y2 = rect.y2, clip_y2 = crect.y2,
width = rect.x2-rect.x1+1,
height = rect.y2-rect.y1+1,
- cur_pen = to_pen(nil, pen or COLOR_GREY)
+ cur_pen = to_pen(nil, args.pen or COLOR_GREY)
}
- return mkinstance(Painter, self):seek(0,0)
+end
+
+function Painter.new(rect, pen)
+ return Painter{ rect = rect, pen = pen }
end
function Painter:isValidPos()
@@ -213,9 +218,8 @@ Screen = defclass(Screen)
Screen.text_input_mode = false
-function Screen:init()
+function Screen:postinit()
self:updateLayout()
- return self
end
Screen.isDismissed = dscreen.isDismissed
@@ -344,7 +348,12 @@ end
FramedScreen = defclass(FramedScreen, Screen)
-FramedScreen.frame_style = BOUNDARY_FRAME
+FramedScreen.ATTRS{
+ frame_style = BOUNDARY_FRAME,
+ frame_title = DEFAULT_NIL,
+ frame_width = DEFAULT_NIL,
+ frame_height = DEFAULT_NIL,
+}
local function hint_coord(gap,hint)
if hint and hint > 0 then
diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua
index c4f15c9ac..b1a96a558 100644
--- a/library/lua/gui/dialogs.lua
+++ b/library/lua/gui/dialogs.lua
@@ -10,24 +10,21 @@ 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")
+
+MessageBox.ATTRS{
+ frame_style = gui.GREY_LINE_FRAME,
+ -- new attrs
+ text = {},
+ on_accept = DEFAULT_NIL,
+ on_cancel = DEFAULT_NIL,
+ on_close = DEFAULT_NIL,
+ text_pen = DEFAULT_NIL,
+}
+
+function MessageBox:preinit(info)
+ if type(info.text) == 'string' then
+ info.text = utils.split_string(info.text, "\n")
end
- gui.FramedScreen.init(self, info)
- return self
end
function MessageBox:getWantedFrameSize()
@@ -82,9 +79,8 @@ function MessageBox:onInput(keys)
end
function showMessage(title, text, tcolor, on_close)
- mkinstance(MessageBox):init{
- text = text,
- title = title,
+ MessageBox{
+ frame_title = title,
text = text,
text_pen = tcolor,
on_close = on_close
@@ -92,8 +88,8 @@ function showMessage(title, text, tcolor, on_close)
end
function showYesNoPrompt(title, text, tcolor, on_accept, on_cancel)
- mkinstance(MessageBox):init{
- title = title,
+ MessageBox{
+ frame_title = title,
text = text,
text_pen = tcolor,
on_accept = on_accept,
@@ -105,25 +101,23 @@ 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
+InputBox.ATTRS{
+ input = '',
+ input_pen = DEFAULT_NIL,
+ on_input = DEFAULT_NIL,
+}
+
+function InputBox:preinit(info)
+ info.on_accept = nil
end
function InputBox:getWantedFrameSize()
- local mw, mh = MessageBox.getWantedFrameSize(self)
+ local mw, mh = InputBox.super.getWantedFrameSize(self)
return mw, mh+2
end
function InputBox:onRenderBody(dc)
- MessageBox.onRenderBody(self, dc)
+ InputBox.super.onRenderBody(self, dc)
dc:newline(1)
dc:pen(self.input_pen or COLOR_LIGHTCYAN)
@@ -161,8 +155,8 @@ function InputBox:onInput(keys)
end
function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width)
- mkinstance(InputBox):init{
- title = title,
+ InputBox{
+ frame_title = title,
text = text,
text_pen = tcolor,
input = input,
@@ -172,5 +166,103 @@ function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_wi
}:show()
end
+ListBox = defclass(ListBox, MessageBox)
+
+ListBox.focus_path = 'ListBox'
+
+ListBox.ATTRS{
+ selection = 0,
+ choices = {},
+ select_pen = DEFAULT_NIL,
+ on_input = DEFAULT_NIL
+}
+
+function InputBox:preinit(info)
+ info.on_accept = nil
+end
+
+function ListBox:init(info)
+ self.page_top = 0
+end
+
+function ListBox:getWantedFrameSize()
+ local mw, mh = ListBox.super.getWantedFrameSize(self)
+ return mw, mh+#self.choices
+end
+
+function ListBox:onRenderBody(dc)
+ ListBox.super.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)
+ ListBox{
+ frame_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..ba3cfbe6c 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
@@ -293,7 +353,7 @@ end
MenuOverlay = defclass(MenuOverlay, DwarfOverlay)
function MenuOverlay:updateLayout()
- DwarfOverlay.updateLayout(self)
+ MenuOverlay.super.updateLayout(self)
self.frame_rect = self.df_layout.menu
end
@@ -301,7 +361,7 @@ MenuOverlay.getWindowSize = gui.FramedScreen.getWindowSize
MenuOverlay.getMousePos = gui.FramedScreen.getMousePos
function MenuOverlay:onAboutToShow(below)
- DwarfOverlay.onAboutToShow(self,below)
+ MenuOverlay.super.onAboutToShow(self,below)
self:updateLayout()
if not self.df_layout.menu then
diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp
index 8de908734..1662f4467 100644
--- a/library/modules/Gui.cpp
+++ b/library/modules/Gui.cpp
@@ -54,7 +54,7 @@ using namespace DFHack;
#include "df/viewscreen_joblistst.h"
#include "df/viewscreen_unitlistst.h"
#include "df/viewscreen_itemst.h"
-#include "df/viewscreen_layerst.h"
+#include "df/viewscreen_layer.h"
#include "df/viewscreen_layer_workshop_profilest.h"
#include "df/viewscreen_layer_noblelistst.h"
#include "df/viewscreen_layer_overall_healthst.h"
@@ -95,7 +95,7 @@ using df::global::selection_rect;
using df::global::ui_menu_width;
using df::global::ui_area_map_width;
-static df::layer_object_listst *getLayerList(df::viewscreen_layerst *layer, int idx)
+static df::layer_object_listst *getLayerList(df::viewscreen_layer *layer, int idx)
{
return virtual_cast(vector_get(layer->layer_objects,idx));
}
@@ -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)
{
@@ -331,9 +332,9 @@ DEFINE_GET_FOCUS_STRING_HANDLER(layer_military)
focus += "/" + enum_item_key(screen->page);
int cur_list;
- if (list1->bright) cur_list = 0;
- else if (list2->bright) cur_list = 1;
- else if (list3->bright) cur_list = 2;
+ if (list1->active) cur_list = 0;
+ else if (list2->active) cur_list = 1;
+ else if (list3->active) cur_list = 2;
else return;
switch (screen->page)
@@ -419,7 +420,7 @@ DEFINE_GET_FOCUS_STRING_HANDLER(layer_assigntrade)
if (unsigned(list_idx) >= num_lists)
return;
- if (list1->bright)
+ if (list1->active)
focus += "/Groups";
else
focus += "/Items";
@@ -457,10 +458,10 @@ DEFINE_GET_FOCUS_STRING_HANDLER(layer_stockpile)
focus += "/On";
- if (list2->bright || list3->bright || screen->list_ids.empty()) {
+ if (list2->active || list3->active || screen->list_ids.empty()) {
focus += "/" + enum_item_key(screen->cur_list);
- if (list3->bright)
+ if (list3->active)
focus += (screen->item_names.empty() ? "/None" : "/Item");
}
}
@@ -843,7 +844,7 @@ static df::item *getAnyItem(df::viewscreen *top)
{
auto list1 = getLayerList(screen, 0);
auto list2 = getLayerList(screen, 1);
- if (!list1 || !list2 || !list2->bright)
+ if (!list1 || !list2 || !list2->active)
return NULL;
int list_idx = vector_get(screen->visible_lists, list1->cursor, (int16_t)-1);
diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp
index dc64143c9..b8c697a48 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) \
@@ -726,6 +730,18 @@ static bool detachItem(MapExtras::MapCache &mc, df::item *item)
item->flags.bits.in_inventory = false;
return true;
}
+ else if (item->flags.bits.removed)
+ {
+ item->flags.bits.removed = false;
+
+ if (item->flags.bits.garbage_collect)
+ {
+ item->flags.bits.garbage_collect = false;
+ item->categorize(true);
+ }
+
+ return true;
+ }
else
return false;
}
@@ -866,3 +882,64 @@ bool DFHack::Items::moveToInventory(
return true;
}
+
+bool Items::remove(MapExtras::MapCache &mc, df::item *item, bool no_uncat)
+{
+ CHECK_NULL_POINTER(item);
+
+ auto pos = getPosition(item);
+
+ if (!detachItem(mc, item))
+ return false;
+
+ if (pos.isValid())
+ item->pos = pos;
+
+ if (!no_uncat)
+ item->uncategorize();
+
+ item->flags.bits.removed = true;
+ item->flags.bits.garbage_collect = !no_uncat;
+ 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 d0401164a..f1f40f19c 100644
--- a/library/modules/Maps.cpp
+++ b/library/modules/Maps.cpp
@@ -307,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;
diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp
index 50cf21a9c..db9c9c7df 100644
--- a/library/modules/Materials.cpp
+++ b/library/modules/Materials.cpp
@@ -283,6 +283,19 @@ bool MaterialInfo::findCreature(const std::string &token, const std::string &sub
return decode(-1);
}
+bool MaterialInfo::findProduct(df::material *material, const std::string &name)
+{
+ if (!material || name.empty())
+ return decode(-1);
+
+ auto &pids = material->reaction_product.id;
+ for (size_t i = 0; i < pids.size(); i++)
+ if ((*pids[i]) == name)
+ return decode(material->reaction_product.material, i);
+
+ return decode(-1);
+}
+
std::string MaterialInfo::getToken()
{
if (isNone())
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 e14aa02a0..67b8c1236 100644
--- a/library/modules/World.cpp
+++ b/library/modules/World.cpp
@@ -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..a914f3b75 160000
--- a/library/xml
+++ b/library/xml
@@ -1 +1 @@
-Subproject commit df8178a989373ec7868d9195d82ae5f85145ef81
+Subproject commit a914f3b7558335d53c0ac93f6e7267906a33cd29
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 04da3e6c8..0b0ad0461 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -44,6 +44,12 @@ 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")
+install(DIRECTORY raw/
+ DESTINATION ${DFHACK_DATA_DESTINATION}/raw
+ FILES_MATCHING PATTERN "*.diff")
# Protobuf
FILE(GLOB PROJECT_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto)
@@ -114,6 +120,10 @@ 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)
+ DFHACK_PLUGIN(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
+ DFHACK_PLUGIN(add-spatter add-spatter.cpp)
# not yet. busy with other crud again...
#DFHACK_PLUGIN(versionosd versionosd.cpp)
endif()
diff --git a/plugins/add-spatter.cpp b/plugins/add-spatter.cpp
new file mode 100644
index 000000000..35ea11ef5
--- /dev/null
+++ b/plugins/add-spatter.cpp
@@ -0,0 +1,433 @@
+#include "Core.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include "df/item_liquid_miscst.h"
+#include "df/item_constructed.h"
+#include "df/builtin_mats.h"
+#include "df/world.h"
+#include "df/job.h"
+#include "df/job_item.h"
+#include "df/job_item_ref.h"
+#include "df/ui.h"
+#include "df/report.h"
+#include "df/reaction.h"
+#include "df/reaction_reagent_itemst.h"
+#include "df/reaction_product_item_improvementst.h"
+#include "df/reaction_product_improvement_flags.h"
+#include "df/matter_state.h"
+#include "df/contaminant.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;
+
+typedef df::reaction_product_item_improvementst improvement_product;
+
+DFHACK_PLUGIN("add-spatter");
+
+struct ReagentSource {
+ int idx;
+ df::reaction_reagent *reagent;
+
+ ReagentSource() : idx(-1), reagent(NULL) {}
+};
+
+struct MaterialSource : ReagentSource {
+ bool product;
+ std::string product_name;
+
+ int mat_type, mat_index;
+
+ MaterialSource() : product(false), mat_type(-1), mat_index(-1) {}
+};
+
+struct ProductInfo {
+ df::reaction *react;
+ improvement_product *product;
+
+ ReagentSource object;
+ MaterialSource material;
+
+ bool isValid() {
+ return object.reagent && (material.mat_type >= 0 || material.reagent);
+ }
+};
+
+struct ReactionInfo {
+ df::reaction *react;
+
+ std::vector products;
+};
+
+static std::map reactions;
+static std::map products;
+
+static ReactionInfo *find_reaction(const std::string &name)
+{
+ auto it = reactions.find(name);
+ return (it != reactions.end()) ? &it->second : NULL;
+}
+
+static bool is_add_spatter(const std::string &name)
+{
+ return name.size() > 12 && memcmp(name.data(), "SPATTER_ADD_", 12) == 0;
+}
+
+static void find_material(int *type, int *index, df::item *input, MaterialSource &mat)
+{
+ if (input && mat.reagent)
+ {
+ MaterialInfo info(input);
+
+ if (mat.product)
+ {
+ if (!info.findProduct(info, mat.product_name))
+ {
+ color_ostream_proxy out(Core::getInstance().getConsole());
+ out.printerr("Cannot find product '%s'\n", mat.product_name.c_str());
+ }
+ }
+
+ *type = info.type;
+ *index = info.index;
+ }
+ else
+ {
+ *type = mat.mat_type;
+ *index = mat.mat_index;
+ }
+}
+
+static int has_contaminant(df::item_actual *item, int type, int index)
+{
+ auto cont = item->contaminants;
+ if (!cont)
+ return 0;
+
+ int size = 0;
+
+ for (size_t i = 0; i < cont->size(); i++)
+ {
+ auto cur = (*cont)[i];
+ if (cur->mat_type == type && cur->mat_index == index)
+ size += cur->size;
+ }
+
+ return size;
+}
+
+/*
+ * Hooks
+ */
+
+typedef std::map > item_table;
+
+static void index_items(item_table &table, df::job *job, ReactionInfo *info)
+{
+ for (int i = job->items.size()-1; i >= 0; i--)
+ {
+ auto iref = job->items[i];
+ if (iref->job_item_idx < 0) continue;
+ auto iitem = job->job_items[iref->job_item_idx];
+
+ if (iitem->contains.empty())
+ {
+ table[iitem->reagent_index].push_back(iref->item);
+ }
+ else
+ {
+ std::vector contents;
+ Items::getContainedItems(iref->item, &contents);
+
+ for (int j = contents.size()-1; j >= 0; j--)
+ {
+ for (int k = iitem->contains.size()-1; k >= 0; k--)
+ {
+ int ridx = iitem->contains[k];
+ auto reag = info->react->reagents[ridx];
+
+ if (reag->matchesChild(contents[j], info->react, iitem->reaction_id))
+ table[ridx].push_back(contents[j]);
+ }
+ }
+ }
+ }
+}
+
+df::item* find_item(ReagentSource &info, item_table &table)
+{
+ if (!info.reagent)
+ return NULL;
+ if (table[info.idx].empty())
+ return NULL;
+ return table[info.idx].back();
+}
+
+struct item_hook : df::item_constructed {
+ typedef df::item_constructed interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(bool, isImprovable, (df::job *job, int16_t mat_type, int32_t mat_index))
+ {
+ ReactionInfo *info;
+
+ if (job && job->job_type == job_type::CustomReaction &&
+ (info = find_reaction(job->reaction_name)) != NULL)
+ {
+ if (!contaminants || contaminants->empty())
+ return true;
+
+ item_table table;
+ index_items(table, job, info);
+
+ for (size_t i = 0; i < info->products.size(); i++)
+ {
+ auto &product = info->products[i];
+
+ int mattype, matindex;
+ auto material = find_item(info->products[i].material, table);
+
+ find_material(&mattype, &matindex, material, product.material);
+
+ if (mattype < 0 || has_contaminant(this, mattype, matindex) >= 50)
+ return false;
+ }
+
+ return true;
+ }
+
+ return INTERPOSE_NEXT(isImprovable)(job, mat_type, mat_index);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(item_hook, isImprovable);
+
+df::item* find_item(
+ ReagentSource &info,
+ std::vector *in_reag,
+ std::vector *in_items
+) {
+ if (!info.reagent)
+ return NULL;
+ for (int i = in_items->size(); i >= 0; i--)
+ if ((*in_reag)[i] == info.reagent)
+ return (*in_items)[i];
+ return NULL;
+}
+
+struct product_hook : improvement_product {
+ typedef improvement_product interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(
+ void, produce,
+ (df::unit *unit, std::vector *out_items,
+ std::vector *in_reag,
+ std::vector *in_items,
+ int32_t quantity, int16_t skill,
+ df::historical_entity *entity, df::world_site *site)
+ ) {
+ if (auto product = products[this])
+ {
+ auto object = find_item(product->object, in_reag, in_items);
+ auto material = find_item(product->material, in_reag, in_items);
+
+ if (object && (material || !product->material.reagent))
+ {
+ using namespace df::enums::improvement_type;
+
+ int mattype, matindex;
+ find_material(&mattype, &matindex, material, product->material);
+
+ df::matter_state state = matter_state::Liquid;
+
+ switch (improvement_type)
+ {
+ case COVERED:
+ if (flags.is_set(reaction_product_improvement_flags::GLAZED))
+ state = matter_state::Solid;
+ break;
+ case BANDS:
+ state = matter_state::Paste;
+ break;
+ case SPIKES:
+ state = matter_state::Powder;
+ break;
+ default:
+ break;
+ }
+
+ int rating = unit ? Units::getEffectiveSkill(unit, df::job_skill(skill)) : 0;
+ int size = int(probability*(1.0f + 0.06f*rating)); // +90% at legendary
+
+ object->addContaminant(
+ mattype, matindex, state,
+ object->getTemperature(),
+ size, -1,
+ 0x8000 // not washed by water, and 'clean items' safe.
+ );
+ }
+
+ return;
+ }
+
+ INTERPOSE_NEXT(produce)(unit, out_items, in_reag, in_items, quantity, skill, entity, site);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(product_hook, produce);
+
+/*
+ * Scan raws for matching reactions.
+ */
+
+static void find_reagent(
+ color_ostream &out, ReagentSource &info, df::reaction *react, std::string name
+) {
+ for (size_t i = 0; i < react->reagents.size(); i++)
+ {
+ if (react->reagents[i]->code != name)
+ continue;
+
+ info.idx = i;
+ info.reagent = react->reagents[i];
+ return;
+ }
+
+ out.printerr("Invalid reagent name '%s' in '%s'\n", name.c_str(), react->code.c_str());
+}
+
+static void parse_product(
+ color_ostream &out, ProductInfo &info, df::reaction *react, improvement_product *prod
+) {
+ using namespace df::enums::reaction_product_improvement_flags;
+
+ info.react = react;
+ info.product = prod;
+
+ find_reagent(out, info.object, react, prod->target_reagent);
+
+ auto ritem = strict_virtual_cast(info.object.reagent);
+ if (ritem)
+ ritem->flags1.bits.improvable = true;
+
+ info.material.mat_type = prod->mat_type;
+ info.material.mat_index = prod->mat_index;
+
+ if (prod->flags.is_set(GET_MATERIAL_PRODUCT))
+ {
+ find_reagent(out, info.material, react, prod->get_material.reagent_code);
+
+ info.material.product = true;
+ info.material.product_name = prod->get_material.product_code;
+ }
+ else if (prod->flags.is_set(GET_MATERIAL_SAME))
+ {
+ find_reagent(out, info.material, react, prod->get_material.reagent_code);
+ }
+}
+
+static bool find_reactions(color_ostream &out)
+{
+ reactions.clear();
+ products.clear();
+
+ auto &rlist = world->raws.reactions;
+
+ for (size_t i = 0; i < rlist.size(); i++)
+ {
+ if (!is_add_spatter(rlist[i]->code))
+ continue;
+
+ reactions[rlist[i]->code].react = rlist[i];
+ }
+
+ for (auto it = reactions.begin(); it != reactions.end(); ++it)
+ {
+ auto &prod = it->second.react->products;
+ auto &out_prod = it->second.products;
+
+ for (size_t i = 0; i < prod.size(); i++)
+ {
+ auto itprod = strict_virtual_cast(prod[i]);
+ if (!itprod) continue;
+
+ out_prod.push_back(ProductInfo());
+ parse_product(out, out_prod.back(), it->second.react, itprod);
+ }
+
+ for (size_t i = 0; i < prod.size(); i++)
+ {
+ if (out_prod[i].isValid())
+ products[out_prod[i].product] = &out_prod[i];
+ }
+ }
+
+ return !products.empty();
+}
+
+static void enable_hooks(bool enable)
+{
+ INTERPOSE_HOOK(item_hook, isImprovable).apply(enable);
+ INTERPOSE_HOOK(product_hook, produce).apply(enable);
+}
+
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
+{
+ switch (event) {
+ case SC_MAP_LOADED:
+ if (find_reactions(out))
+ {
+ out.print("Detected spatter add reactions - enabling plugin.\n");
+ enable_hooks(true);
+ }
+ else
+ enable_hooks(false);
+ break;
+ case SC_MAP_UNLOADED:
+ enable_hooks(false);
+ reactions.clear();
+ products.clear();
+ break;
+ default:
+ break;
+ }
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands)
+{
+ if (Core::getInstance().isMapLoaded())
+ plugin_onstatechange(out, SC_MAP_LOADED);
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown ( color_ostream &out )
+{
+ enable_hooks(false);
+ return CR_OK;
+}
diff --git a/plugins/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..319b83c1f 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;
@@ -81,11 +81,18 @@ command_result cleanitems (color_ostream &out)
df::item_actual *item = (df::item_actual *)world->items.all[i];
if (item->contaminants && item->contaminants->size())
{
+ std::vector saved;
for (size_t j = 0; j < item->contaminants->size(); j++)
- delete item->contaminants->at(j);
+ {
+ auto obj = (*item->contaminants)[j];
+ if (obj->flags.whole & 0x8000) // DFHack-generated contaminant
+ saved.push_back(obj);
+ else
+ delete obj;
+ }
cleaned_items++;
- cleaned_total += item->contaminants->size();
- item->contaminants->clear();
+ cleaned_total += item->contaminants->size() - saved.size();
+ item->contaminants->swap(saved);
}
}
if (cleaned_total)
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/dumpmats.cpp b/plugins/devel/dumpmats.cpp
index ba888e7cf..0af1fce50 100644
--- a/plugins/devel/dumpmats.cpp
+++ b/plugins/devel/dumpmats.cpp
@@ -11,6 +11,7 @@
#include "df/matter_state.h"
#include "df/descriptor_color.h"
#include "df/item_type.h"
+#include "df/strain_type.h"
using std::string;
using std::vector;
@@ -195,47 +196,17 @@ command_result df_dumpmats (color_ostream &out, vector ¶meters)
if (mat->molar_mass != 0xFBBC7818)
out.print("\t[MOLAR_MASS:%i]\n", mat->molar_mass);
- if (mat->strength.impact_yield != 10000)
- out.print("\t[IMPACT_YIELD:%i]\n", mat->strength.impact_yield);
- if (mat->strength.impact_fracture != 10000)
- out.print("\t[IMPACT_FRACTURE:%i]\n", mat->strength.impact_fracture);
- if (mat->strength.impact_strain_at_yield != 0)
- out.print("\t[IMPACT_STRAIN_AT_YIELD:%i]\n", mat->strength.impact_strain_at_yield);
-
- if (mat->strength.compressive_yield != 10000)
- out.print("\t[COMPRESSIVE_YIELD:%i]\n", mat->strength.compressive_yield);
- if (mat->strength.compressive_fracture != 10000)
- out.print("\t[COMPRESSIVE_FRACTURE:%i]\n", mat->strength.compressive_fracture);
- if (mat->strength.compressive_strain_at_yield != 0)
- out.print("\t[COMPRESSIVE_STRAIN_AT_YIELD:%i]\n", mat->strength.compressive_strain_at_yield);
-
- if (mat->strength.tensile_yield != 10000)
- out.print("\t[TENSILE_YIELD:%i]\n", mat->strength.tensile_yield);
- if (mat->strength.tensile_fracture != 10000)
- out.print("\t[TENSILE_FRACTURE:%i]\n", mat->strength.tensile_fracture);
- if (mat->strength.tensile_strain_at_yield != 0)
- out.print("\t[TENSILE_STRAIN_AT_YIELD:%i]\n", mat->strength.tensile_strain_at_yield);
-
- if (mat->strength.torsion_yield != 10000)
- out.print("\t[TORSION_YIELD:%i]\n", mat->strength.torsion_yield);
- if (mat->strength.torsion_fracture != 10000)
- out.print("\t[TORSION_FRACTURE:%i]\n", mat->strength.torsion_fracture);
- if (mat->strength.torsion_strain_at_yield != 0)
- out.print("\t[TORSION_STRAIN_AT_YIELD:%i]\n", mat->strength.torsion_strain_at_yield);
-
- if (mat->strength.shear_yield != 10000)
- out.print("\t[SHEAR_YIELD:%i]\n", mat->strength.shear_yield);
- if (mat->strength.shear_fracture != 10000)
- out.print("\t[SHEAR_FRACTURE:%i]\n", mat->strength.shear_fracture);
- if (mat->strength.shear_strain_at_yield != 0)
- out.print("\t[SHEAR_STRAIN_AT_YIELD:%i]\n", mat->strength.shear_strain_at_yield);
-
- if (mat->strength.bending_yield != 10000)
- out.print("\t[BENDING_YIELD:%i]\n", mat->strength.bending_yield);
- if (mat->strength.bending_fracture != 10000)
- out.print("\t[BENDING_FRACTURE:%i]\n", mat->strength.bending_fracture);
- if (mat->strength.bending_strain_at_yield != 0)
- out.print("\t[BENDING_STRAIN_AT_YIELD:%i]\n", mat->strength.bending_strain_at_yield);
+ FOR_ENUM_ITEMS(strain_type, strain)
+ {
+ auto name = ENUM_KEY_STR(strain_type,strain);
+
+ if (mat->strength.yield[strain] != 10000)
+ out.print("\t[%s_YIELD:%i]\n", name.c_str(), mat->strength.yield[strain]);
+ if (mat->strength.fracture[strain] != 10000)
+ out.print("\t[%s_FRACTURE:%i]\n", name.c_str(), mat->strength.fracture[strain]);
+ if (mat->strength.strain_at_yield[strain] != 0)
+ out.print("\t[%s_STRAIN_AT_YIELD:%i]\n", name.c_str(), mat->strength.strain_at_yield[strain]);
+ }
if (mat->strength.max_edge != 0)
out.print("\t[MAX_EDGE:%i]\n", mat->strength.max_edge);
diff --git a/plugins/devel/nestboxes.cpp b/plugins/devel/nestboxes.cpp
index b3d24cd92..42c3c0660 100644
--- a/plugins/devel/nestboxes.cpp
+++ b/plugins/devel/nestboxes.cpp
@@ -31,6 +31,40 @@ static command_result nestboxes(color_ostream &out, vector & parameters
DFHACK_PLUGIN("nestboxes");
+static bool enabled = false;
+
+static void eggscan(color_ostream &out)
+{
+ CoreSuspender suspend;
+
+ for (int i = 0; i < world->buildings.all.size(); ++i)
+ {
+ df::building *build = world->buildings.all[i];
+ auto type = build->getType();
+ if (df::enums::building_type::NestBox == type)
+ {
+ bool fertile = false;
+ df::building_nest_boxst *nb = virtual_cast(build);
+ if (nb->claimed_by != -1)
+ {
+ df::unit* u = df::unit::find(nb->claimed_by);
+ if (u && u->relations.pregnancy_timer > 0)
+ fertile = true;
+ }
+ for (int j = 1; j < nb->contained_items.size(); j++)
+ {
+ df::item* item = nb->contained_items[j]->item;
+ if (item->flags.bits.forbid != fertile)
+ {
+ item->flags.bits.forbid = fertile;
+ out << item->getStackSize() << " eggs " << (fertile ? "forbidden" : "unforbidden.") << endl;
+ }
+ }
+ }
+ }
+}
+
+
DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands)
{
if (world && ui) {
@@ -49,6 +83,19 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
return CR_OK;
}
+DFhackCExport command_result plugin_onupdate(color_ostream &out)
+{
+ if (!enabled)
+ return CR_OK;
+
+ static unsigned cnt = 0;
+ if ((++cnt % 5) != 0)
+ return CR_OK;
+
+ eggscan(out);
+
+ return CR_OK;
+}
static command_result nestboxes(color_ostream &out, vector & parameters)
{
@@ -57,60 +104,16 @@ static command_result nestboxes(color_ostream &out, vector & parameters
int dump_count = 0;
int good_egg = 0;
- if (parameters.size() == 1 && parameters[0] == "clean")
- {
- clean = true;
- }
- for (int i = 0; i < world->buildings.all.size(); ++i)
- {
- df::building *build = world->buildings.all[i];
- auto type = build->getType();
- if (df::enums::building_type::NestBox == type)
- {
- bool needs_clean = false;
- df::building_nest_boxst *nb = virtual_cast(build);
- out << "Nestbox at (" << nb->x1 << "," << nb->y1 << ","<< nb->z << "): claimed-by " << nb->claimed_by << ", contained item count " << nb->contained_items.size() << " (" << nb->anon_1 << ")" << endl;
- if (nb->contained_items.size() > 1)
- needs_clean = true;
- if (nb->claimed_by != -1)
- {
- df::unit* u = df::unit::find(nb->claimed_by);
- if (u)
- {
- out << " Claimed by ";
- if (u->name.has_name)
- out << u->name.first_name << ", ";
- df::creature_raw *raw = df::global::world->raws.creatures.all[u->race];
- out << raw->creature_id
- << ", pregnancy timer " << u->relations.pregnancy_timer << endl;
- if (u->relations.pregnancy_timer > 0)
- needs_clean = false;
- }
- }
- for (int j = 1; j < nb->contained_items.size(); j++)
- {
- df::item* item = nb->contained_items[j]->item;
- if (needs_clean) {
- if (clean && !item->flags.bits.dump)
- {
- item->flags.bits.dump = 1;
- dump_count += item->getStackSize();
-
- }
- } else {
- good_egg += item->getStackSize();
- }
- }
- }
- }
-
- if (clean)
- {
- out << dump_count << " eggs dumped." << endl;
- }
- out << good_egg << " fertile eggs found." << endl;
-
-
+ if (parameters.size() == 1) {
+ if (parameters[0] == "enable")
+ enabled = true;
+ else if (parameters[0] == "disable")
+ enabled = false;
+ else
+ return CR_WRONG_USAGE;
+ } else {
+ out << "Plugin " << (enabled ? "enabled" : "disabled") << "." << endl;
+ }
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/siege-engine.lua b/plugins/lua/siege-engine.lua
new file mode 100644
index 000000000..33e120feb
--- /dev/null
+++ b/plugins/lua/siege-engine.lua
@@ -0,0 +1,229 @@
+local _ENV = mkmodule('plugins.siege-engine')
+
+--[[
+
+ Native functions:
+
+ * getTargetArea(building) -> point1, point2
+ * clearTargetArea(building)
+ * setTargetArea(building, point1, point2) -> true/false
+
+ * isLinkedToPile(building,pile) -> true/false
+ * getStockpileLinks(building) -> {pile}
+ * addStockpileLink(building,pile) -> true/false
+ * removeStockpileLink(building,pile) -> true/false
+
+ * saveWorkshopProfile(building) -> profile
+
+ * getAmmoItem(building) -> item_type
+ * setAmmoItem(building,item_type) -> true/false
+
+ * isPassableTile(pos) -> true/false
+ * isTreeTile(pos) -> true/false
+ * isTargetableTile(pos) -> true/false
+
+ * getTileStatus(building,pos) -> 'invalid/ok/out_of_range/blocked/semiblocked'
+ * paintAimScreen(building,view_pos_xyz,left_top_xy,size_xy)
+
+ * canTargetUnit(unit) -> true/false
+
+ proj_info = { target = pos, [delta = float/pos], [factor = int] }
+
+ * projPosAtStep(building,proj_info,step) -> pos
+ * projPathMetrics(building,proj_info) -> {
+ hit_type = 'wall/floor/ceiling/map_edge/tree',
+ collision_step = int,
+ collision_z_step = int,
+ goal_distance = int,
+ goal_step = int/nil,
+ goal_z_step = int/nil,
+ status = 'ok/out_of_range/blocked'
+ }
+
+ * adjustToTarget(building,pos) -> pos,ok=true/false
+
+ * traceUnitPath(unit) -> { {x=int,y=int,z=int[,from=time][,to=time]} }
+ * unitPosAtTime(unit, time) -> pos
+
+ * proposeUnitHits(building) -> { {
+ pos=pos, unit=unit, time=float, dist=int,
+ [lmargin=float,] [rmargin=float,]
+ } }
+
+ * computeNearbyWeight(building,hits,{[id/unit]=score}[,fname])
+
+]]
+
+Z_STEP_COUNT = 15
+Z_STEP = 1/31
+
+function getMetrics(engine, path)
+ path.metrics = path.metrics or projPathMetrics(engine, path)
+ return path.metrics
+end
+
+function findShotHeight(engine, target)
+ local path = { target = target, delta = 0.0 }
+
+ if getMetrics(engine, path).goal_step then
+ return path
+ end
+
+ local tpath = { target = target, delta = Z_STEP_COUNT*Z_STEP }
+
+ if getMetrics(engine, tpath).goal_step then
+ for i = 1,Z_STEP_COUNT-1 do
+ path = { target = target, delta = i*Z_STEP }
+ if getMetrics(engine, path).goal_step then
+ return path
+ end
+ end
+
+ return tpath
+ end
+
+ tpath = { target = target, delta = -Z_STEP_COUNT*Z_STEP }
+
+ if getMetrics(engine, tpath).goal_step then
+ for i = 1,Z_STEP_COUNT-1 do
+ path = { target = target, delta = -i*Z_STEP }
+ if getMetrics(engine, path).goal_step then
+ return path
+ end
+ end
+
+ return tpath
+ end
+end
+
+function findReachableTargets(engine, targets)
+ local reachable = {}
+ for _,tgt in ipairs(targets) do
+ tgt.path = findShotHeight(engine, tgt.pos)
+ if tgt.path then
+ table.insert(reachable, tgt)
+ end
+ end
+ return reachable
+end
+
+recent_targets = recent_targets or {}
+
+if dfhack.is_core_context then
+ dfhack.onStateChange[_ENV] = function(code)
+ if code == SC_MAP_LOADED then
+ recent_targets = {}
+ end
+ end
+end
+
+function saveRecent(unit)
+ local id = unit.id
+ local tgt = recent_targets
+ tgt[id] = (tgt[id] or 0) + 1
+ dfhack.timeout(3, 'days', function()
+ tgt[id] = math.max(0, tgt[id]-1)
+ end)
+end
+
+function getBaseUnitWeight(unit)
+ if dfhack.units.isCitizen(unit) then
+ return -10
+ elseif unit.flags1.diplomat or unit.flags1.merchant then
+ return -2
+ elseif unit.flags1.tame and unit.civ_id == df.global.ui.civ_id then
+ return -1
+ else
+ local rv = 1
+ if unit.flags1.marauder then rv = rv + 0.5 end
+ if unit.flags1.active_invader then rv = rv + 1 end
+ if unit.flags1.invader_origin then rv = rv + 1 end
+ if unit.flags1.invades then rv = rv + 1 end
+ if unit.flags1.hidden_ambusher then rv = rv + 1 end
+ return rv
+ end
+end
+
+function getUnitWeight(unit)
+ local base = getBaseUnitWeight(unit)
+ return base * math.pow(0.7, recent_targets[unit.id] or 0)
+end
+
+function unitWeightCache()
+ local cache = {}
+ return cache, function(unit)
+ local id = unit.id
+ cache[id] = cache[id] or getUnitWeight(unit)
+ return cache[id]
+ end
+end
+
+function scoreTargets(engine, reachable)
+ local ucache, get_weight = unitWeightCache()
+
+ for _,tgt in ipairs(reachable) do
+ tgt.score = get_weight(tgt.unit)
+ if tgt.lmargin and tgt.lmargin < 3 then
+ tgt.score = tgt.score * tgt.lmargin / 3
+ end
+ if tgt.rmargin and tgt.rmargin < 3 then
+ tgt.score = tgt.score * tgt.rmargin / 3
+ end
+ end
+
+ computeNearbyWeight(engine, reachable, ucache)
+
+ for _,tgt in ipairs(reachable) do
+ tgt.score = (tgt.score + tgt.nearby_weight*0.7) * math.pow(0.995, tgt.time/3)
+ end
+
+ table.sort(reachable, function(a,b)
+ return a.score > b.score or (a.score == b.score and a.time < b.time)
+ end)
+end
+
+function pickUniqueTargets(reachable)
+ local unique = {}
+
+ if #reachable > 0 then
+ local pos_table = {}
+ local first_score = reachable[1].score
+
+ for i,tgt in ipairs(reachable) do
+ if tgt.score < 0 or tgt.score < 0.1*first_score then
+ break
+ end
+ local x,y,z = pos2xyz(tgt.pos)
+ local key = x..':'..y..':'..z
+ if pos_table[key] then
+ table.insert(pos_table[key].units, tgt.unit)
+ else
+ table.insert(unique, tgt)
+ pos_table[key] = tgt
+ tgt.units = { tgt.unit }
+ end
+ end
+ end
+
+ return unique
+end
+
+function doAimProjectile(engine, item, target_min, target_max, skill)
+ print(item, df.skill_rating[skill])
+
+ local targets = proposeUnitHits(engine)
+ local reachable = findReachableTargets(engine, targets)
+ scoreTargets(engine, reachable)
+ local unique = pickUniqueTargets(reachable)
+
+ if #unique > 0 then
+ local cnt = math.max(math.min(#unique,5), math.min(10, math.floor(#unique/2)))
+ local rnd = math.random(cnt)
+ for _,u in ipairs(unique[rnd].units) do
+ saveRecent(u)
+ end
+ return unique[rnd].path
+ 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/raw/entity_default.diff b/plugins/raw/entity_default.diff
new file mode 100644
index 000000000..a99f8ebba
--- /dev/null
+++ b/plugins/raw/entity_default.diff
@@ -0,0 +1,29 @@
+--- ../objects.old/entity_default.txt 2012-09-17 17:59:28.853898702 +0400
++++ entity_default.txt 2012-09-17 17:59:28.684899429 +0400
+@@ -49,6 +49,7 @@
+ [TRAPCOMP:ITEM_TRAPCOMP_SPIKEDBALL]
+ [TRAPCOMP:ITEM_TRAPCOMP_LARGESERRATEDDISC]
+ [TRAPCOMP:ITEM_TRAPCOMP_MENACINGSPIKE]
++ [TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON]
+ [TOY:ITEM_TOY_PUZZLEBOX]
+ [TOY:ITEM_TOY_BOAT]
+ [TOY:ITEM_TOY_HAMMER]
+@@ -204,6 +205,8 @@
+ [PERMITTED_JOB:WAX_WORKER]
+ [PERMITTED_BUILDING:SOAP_MAKER]
+ [PERMITTED_BUILDING:SCREW_PRESS]
++ [PERMITTED_BUILDING:STEAM_ENGINE]
++ [PERMITTED_BUILDING:MAGMA_STEAM_ENGINE]
+ [PERMITTED_REACTION:TAN_A_HIDE]
+ [PERMITTED_REACTION:RENDER_FAT]
+ [PERMITTED_REACTION:MAKE_SOAP_FROM_TALLOW]
+@@ -248,6 +251,9 @@
+ [PERMITTED_REACTION:ROSE_GOLD_MAKING]
+ [PERMITTED_REACTION:BISMUTH_BRONZE_MAKING]
+ [PERMITTED_REACTION:ADAMANTINE_WAFERS]
++ [PERMITTED_REACTION:STOKE_BOILER]
++ [PERMITTED_REACTION:SPATTER_ADD_EXTRACT_WEAPON]
++ [PERMITTED_REACTION:SPATTER_ADD_EXTRACT_AMMO]
+ [WORLD_CONSTRUCTION:TUNNEL]
+ [WORLD_CONSTRUCTION:BRIDGE]
+ [WORLD_CONSTRUCTION:ROAD]
diff --git a/plugins/raw/material_template_default.diff b/plugins/raw/material_template_default.diff
new file mode 100644
index 000000000..8b6ef327b
--- /dev/null
+++ b/plugins/raw/material_template_default.diff
@@ -0,0 +1,10 @@
+--- ../objects.old/material_template_default.txt 2012-09-17 17:59:28.907898469 +0400
++++ material_template_default.txt 2012-09-17 17:59:28.695899382 +0400
+@@ -2374,6 +2374,7 @@
+ [MAX_EDGE:500]
+ [ABSORPTION:100]
+ [LIQUID_MISC_CREATURE]
++ [REACTION_CLASS:CREATURE_EXTRACT]
+ [ROTS]
+
+ This is for creatures that are "made of fire". Right now there isn't a good format for that.
diff --git a/plugins/raw/reaction_spatter.txt b/plugins/raw/reaction_spatter.txt
new file mode 100644
index 000000000..085be7fdd
--- /dev/null
+++ b/plugins/raw/reaction_spatter.txt
@@ -0,0 +1,144 @@
+reaction_spatter
+
+[OBJECT:REACTION]
+
+Reaction name must start with 'SPATTER_ADD_':
+
+[REACTION:SPATTER_ADD_OBJECT_LIQUID]
+ [NAME:coat object with liquid]
+ [ADVENTURE_MODE_ENABLED]
+ [SKILL:WAX_WORKING]
+ [REAGENT:extract:150:LIQUID_MISC:NONE:NONE:NONE]
+ [MIN_DIMENSION:150]
+ [REAGENT:extract container:1:NONE:NONE:NONE:NONE]
+ [CONTAINS:extract]
+ [PRESERVE_REAGENT]
+ [DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
+ The object to improve must be after the input mat, so that it is known:
+ [REAGENT:object:1:NONE:NONE:NONE:NONE]
+ [PRESERVE_REAGENT]
+ Need some excuse why the spatter is water-resistant:
+ [REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:FAT][UNROTTEN]
+ The probability is used as spatter size; Legendary gives +90%:
+ COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
+ [IMPROVEMENT:800:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
+
+[REACTION:SPATTER_ADD_WEAPON_EXTRACT]
+ [NAME:coat weapon with extract]
+ [BUILDING:CRAFTSMAN:NONE]
+ [SKILL:WAX_WORKING]
+ [REAGENT:extract:150:LIQUID_MISC:NONE:NONE:NONE]
+ [MIN_DIMENSION:150]
+ [REACTION_CLASS:CREATURE_EXTRACT]
+ [REAGENT:extract container:1:NONE:NONE:NONE:NONE]
+ [CONTAINS:extract]
+ [PRESERVE_REAGENT]
+ [DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
+ The object to improve must be after the input mat, so that it is known:
+ [REAGENT:object:1:WEAPON:NONE:NONE:NONE]
+ [PRESERVE_REAGENT]
+ Need some excuse why the spatter is water-resistant:
+ [REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
+ The probability is used as spatter size; Legendary gives +90%:
+ COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
+ [IMPROVEMENT:800:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
+
+[REACTION:SPATTER_ADD_AMMO_EXTRACT]
+ [NAME:coat ammo with extract]
+ [BUILDING:CRAFTSMAN:NONE]
+ [SKILL:WAX_WORKING]
+ [REAGENT:extract:50:LIQUID_MISC:NONE:NONE:NONE]
+ [MIN_DIMENSION:50]
+ [REACTION_CLASS:CREATURE_EXTRACT]
+ [REAGENT:extract container:1:NONE:NONE:NONE:NONE]
+ [CONTAINS:extract]
+ [PRESERVE_REAGENT]
+ [DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
+ The object to improve must be after the input mat, so that it is known:
+ [REAGENT:object:1:AMMO:NONE:NONE:NONE]
+ [PRESERVE_REAGENT]
+ Need some excuse why the spatter is water-resistant:
+ [REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
+ The probability is used as spatter size; Legendary gives +90%:
+ COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
+ [IMPROVEMENT:200:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
+
+[REACTION:SPATTER_ADD_WEAPON_GCS]
+ [NAME:coat weapon with GCS venom]
+ [BUILDING:CRAFTSMAN:NONE]
+ [SKILL:WAX_WORKING]
+ [REAGENT:extract:150:LIQUID_MISC:NONE:CAVE_SPIDER_GIANT:POISON]
+ [MIN_DIMENSION:150]
+ [REACTION_CLASS:CREATURE_EXTRACT]
+ [REAGENT:extract container:1:NONE:NONE:NONE:NONE]
+ [CONTAINS:extract]
+ [PRESERVE_REAGENT]
+ [DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
+ The object to improve must be after the input mat, so that it is known:
+ [REAGENT:object:1:WEAPON:NONE:NONE:NONE]
+ [PRESERVE_REAGENT]
+ Need some excuse why the spatter is water-resistant:
+ [REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
+ The probability is used as spatter size; Legendary gives +90%:
+ COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
+ [IMPROVEMENT:800:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
+
+[REACTION:SPATTER_ADD_AMMO_GCS]
+ [NAME:coat ammo with GCS venom]
+ [BUILDING:CRAFTSMAN:NONE]
+ [SKILL:WAX_WORKING]
+ [REAGENT:extract:50:LIQUID_MISC:NONE:CAVE_SPIDER_GIANT:POISON]
+ [MIN_DIMENSION:50]
+ [REACTION_CLASS:CREATURE_EXTRACT]
+ [REAGENT:extract container:1:NONE:NONE:NONE:NONE]
+ [CONTAINS:extract]
+ [PRESERVE_REAGENT]
+ [DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
+ The object to improve must be after the input mat, so that it is known:
+ [REAGENT:object:1:AMMO:NONE:NONE:NONE]
+ [PRESERVE_REAGENT]
+ Need some excuse why the spatter is water-resistant:
+ [REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
+ The probability is used as spatter size; Legendary gives +90%:
+ COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
+ [IMPROVEMENT:200:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
+
+[REACTION:SPATTER_ADD_WEAPON_GDS]
+ [NAME:coat weapon with GDS venom]
+ [BUILDING:CRAFTSMAN:NONE]
+ [SKILL:WAX_WORKING]
+ [REAGENT:extract:150:LIQUID_MISC:NONE:SCORPION_DESERT_GIANT:POISON]
+ [MIN_DIMENSION:150]
+ [REACTION_CLASS:CREATURE_EXTRACT]
+ [REAGENT:extract container:1:NONE:NONE:NONE:NONE]
+ [CONTAINS:extract]
+ [PRESERVE_REAGENT]
+ [DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
+ The object to improve must be after the input mat, so that it is known:
+ [REAGENT:object:1:WEAPON:NONE:NONE:NONE]
+ [PRESERVE_REAGENT]
+ Need some excuse why the spatter is water-resistant:
+ [REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
+ The probability is used as spatter size; Legendary gives +90%:
+ COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
+ [IMPROVEMENT:800:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
+
+[REACTION:SPATTER_ADD_AMMO_GDS]
+ [NAME:coat ammo with GDS venom]
+ [BUILDING:CRAFTSMAN:NONE]
+ [SKILL:WAX_WORKING]
+ [REAGENT:extract:50:LIQUID_MISC:NONE:SCORPION_DESERT_GIANT:POISON]
+ [MIN_DIMENSION:50]
+ [REACTION_CLASS:CREATURE_EXTRACT]
+ [REAGENT:extract container:1:NONE:NONE:NONE:NONE]
+ [CONTAINS:extract]
+ [PRESERVE_REAGENT]
+ [DOES_NOT_DETERMINE_PRODUCT_AMOUNT]
+ The object to improve must be after the input mat, so that it is known:
+ [REAGENT:object:1:AMMO:NONE:NONE:NONE]
+ [PRESERVE_REAGENT]
+ Need some excuse why the spatter is water-resistant:
+ [REAGENT:grease:1:GLOB:NONE:NONE:NONE][REACTION_CLASS:TALLOW][UNROTTEN]
+ The probability is used as spatter size; Legendary gives +90%:
+ COVERED = liquid, GLAZED = solid, BANDS = paste, SPIKES = powder
+ [IMPROVEMENT:200:object:COVERED:GET_MATERIAL_FROM_REAGENT:extract:NONE]
diff --git a/plugins/siege-engine.cpp b/plugins/siege-engine.cpp
new file mode 100644
index 000000000..3b95aba35
--- /dev/null
+++ b/plugins/siege-engine.cpp
@@ -0,0 +1,1865 @@
+#include "Core.h"
+#include
+#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_actual.h"
+#include "df/items_other_id.h"
+#include "df/building_stockpilest.h"
+#include "df/stockpile_links.h"
+#include "df/workshop_profile.h"
+#include "df/strain_type.h"
+#include "df/material.h"
+#include "df/flow_type.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(1, 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;
+}
+
+static const int WEAR_TICKS = 806400;
+
+static bool apply_impact_damage(df::item *item, int minv, int maxv)
+{
+ MaterialInfo info(item);
+ if (!info.isValid())
+ {
+ item->setWear(3);
+ return false;
+ }
+
+ auto &strength = info.material->strength;
+
+ // Use random strain type excluding COMPRESSIVE (conveniently last)
+ int type = random_int(strain_type::COMPRESSIVE);
+ int power = minv + random_int(maxv-minv+1);
+
+ // High elasticity materials just bend
+ if (strength.strain_at_yield[type] >= 5000)
+ return true;
+
+ // Instant fracture?
+ int fracture = strength.fracture[type];
+ if (fracture <= power)
+ {
+ item->setWear(3);
+ return false;
+ }
+
+ // Impact within elastic strain range?
+ int yield = strength.yield[type];
+ if (yield > power)
+ return true;
+
+ // Can wear?
+ auto actual = virtual_cast(item);
+ if (!actual)
+ return false;
+
+ // Transform plastic deformation to wear
+ int max_wear = WEAR_TICKS * 4;
+ int cur_wear = WEAR_TICKS * actual->wear + actual->wear_timer;
+ cur_wear += int64_t(power - yield)*max_wear/(fracture - yield);
+
+ if (cur_wear >= max_wear)
+ {
+ actual->wear = 3;
+ return false;
+ }
+ else
+ {
+ actual->wear = cur_wear / WEAR_TICKS;
+ actual->wear_timer = cur_wear % WEAR_TICKS;
+ return true;
+ }
+}
+
+/*
+ * 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 = obj->is_catapult ? 2 : -1;
+ 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;
+
+ if (unit->counters.unconscious > 0)
+ time += unit->counters.unconscious;
+
+ 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;
+}
+
+static int computeNearbyWeight(lua_State *L)
+{
+ auto engine = find_engine(L, 1);
+ luaL_checktype(L, 2, LUA_TTABLE);
+ luaL_checktype(L, 3, LUA_TTABLE);
+ const char *fname = luaL_optstring(L, 4, "nearby_weight");
+
+ std::vector units;
+ std::vector weights;
+
+ lua_pushnil(L);
+
+ while (lua_next(L, 3))
+ {
+ df::unit *unit;
+ if (lua_isnumber(L, -2))
+ unit = df::unit::find(lua_tointeger(L, -2));
+ else
+ unit = Lua::CheckDFObject(L, -2);
+ if (!unit)
+ continue;
+ units.push_back(UnitPath::get(unit));
+ weights.push_back(lua_tonumber(L, -1));
+ lua_pop(L, 1);
+ }
+
+ lua_pushnil(L);
+
+ while (lua_next(L, 2))
+ {
+ Lua::StackUnwinder frame(L, 1);
+
+ lua_getfield(L, frame[1], "unit");
+ df::unit *unit = Lua::CheckDFObject(L, -1);
+
+ lua_getfield(L, frame[1], "time");
+ float time = luaL_checknumber(L, lua_gettop(L));
+
+ df::coord pos;
+
+ lua_getfield(L, frame[1], "pos");
+ if (lua_isnil(L, -1))
+ {
+ if (!unit) luaL_error(L, "either unit or pos is required");
+ pos = UnitPath::get(unit)->posAtTime(time);
+ }
+ else
+ Lua::CheckDFAssign(L, &pos, -1);
+
+ float sum = 0.0f;
+
+ for (size_t i = 0; i < units.size(); i++)
+ {
+ if (units[i]->unit == unit)
+ continue;
+
+ auto diff = units[i]->posAtTime(time) - pos;
+ float dist = 1 + sqrtf(diff.x*diff.x + diff.y*diff.y + diff.z*diff.z);
+ sum += weights[i]/(dist*dist);
+ }
+
+ lua_pushnumber(L, sum);
+ lua_setfield(L, frame[1], fname);
+ }
+
+ return 0;
+}
+
+/*
+ * Projectile hook
+ */
+
+static const int offsets[8][2] = {
+ { -1, -1 }, { 0, -1 }, { 1, -1 },
+ { -1, 0 }, { 1, 0 },
+ { -1, 1 }, { 0, 1 }, { 1, 1 }
+};
+
+struct projectile_hook : df::proj_itemst {
+ typedef df::proj_itemst interpose_base;
+
+ void aimAtPoint(EngineInfo *engine, const ProjectilePath &path)
+ {
+ target_pos = path.target;
+
+ // Debug
+ Maps::getTileOccupancy(path.goal)->bits.arrow_color = COLOR_LIGHTMAGENTA;
+
+ 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 aimAtPoint(EngineInfo *engine, int skill, const ProjectilePath &path)
+ {
+ df::coord fail_target = path.goal;
+
+ orient_engine(engine->bld, path.goal);
+
+ // Debug
+ Maps::getTileOccupancy(path.goal)->bits.arrow_color = COLOR_LIGHTRED;
+
+ // Dabbling always hit in 7x7 area
+ if (skill < skill_rating::Novice)
+ {
+ fail_target.x += random_int(7)-3;
+ fail_target.y += random_int(7)-3;
+ aimAtPoint(engine, ProjectilePath(path.origin, fail_target));
+ return;
+ }
+
+ // Exact hit chance
+ float hit_chance = 1.04f - powf(0.8f, skill);
+
+ if (float(rand())/RAND_MAX < hit_chance)
+ {
+ aimAtPoint(engine, path);
+ return;
+ }
+
+ // Otherwise perturb
+ if (skill <= skill_rating::Proficient)
+ {
+ // 5x5
+ fail_target.x += random_int(5)-2;
+ fail_target.y += random_int(5)-2;
+ }
+ else
+ {
+ // 3x3
+ int idx = random_int(8);
+ fail_target.x += offsets[idx][0];
+ fail_target.y += offsets[idx][1];
+ }
+
+ ProjectilePath fail(path.origin, fail_target, path.fudge_delta, path.fudge_factor);
+ aimAtPoint(engine, fail);
+ }
+
+ void aimAtArea(EngineInfo *engine, int skill)
+ {
+ 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, skill, path);
+ return;
+ }
+ }
+
+ if (!last_passable.isValid())
+ last_passable = target;
+
+ aimAtPoint(engine, skill, 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, skill);
+ else
+ proj->aimAtPoint(engine, skill, 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);
+
+ // Dabbling can't aim
+ if (skill < skill_rating::Novice)
+ aimAtArea(engine, skill);
+ else
+ {
+ 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, skill);
+ }
+
+ 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;
+
+ float bonus = 1.0f + 0.1f*(origin_pos.z -cur_pos.z);
+ bonus *= 1.0f + (distance_flown - 60) / 200.0f;
+ speed *= bonus;
+
+ // Flight direction vector
+ df::coord dist = target_pos - origin_pos;
+ float vx = dist.x, vy = dist.y, vz = fabs((float)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];
+
+ // Liquids are vaporized so that they cover nearby units
+ if (child->isLiquid())
+ {
+ auto flow = Maps::spawnFlow(
+ cur_pos,
+ flow_type::MaterialVapor,
+ child->getMaterial(), child->getMaterialIndex(),
+ 100
+ );
+
+ // should it leave a puddle too?..
+ if (flow && Items::remove(mc, child))
+ continue;
+ }
+
+ auto proj = Items::makeProjectile(mc, child);
+ if (!proj) continue;
+
+ bool keep = apply_impact_damage(child, 50000, int(250000*bonus));
+
+ proj->flags.bits.no_impact_destroy = keep;
+ //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 = std::max(min_zspeed, 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_COMMAND(computeNearbyWeight),
+ 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/sort.cpp b/plugins/sort.cpp
index ff51fc773..4b2bf7bbd 100644
--- a/plugins/sort.cpp
+++ b/plugins/sort.cpp
@@ -228,7 +228,7 @@ static void sort_null_first(vector ¶meters)
vector_insert_at(parameters, 0, std::string("(vector_get(layer->layer_objects,idx));
}
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/tweak.cpp b/plugins/tweak.cpp
index fbea30231..fb286e0d7 100644
--- a/plugins/tweak.cpp
+++ b/plugins/tweak.cpp
@@ -29,11 +29,20 @@
#include "df/criminal_case.h"
#include "df/unit_inventory_item.h"
#include "df/viewscreen_dwarfmodest.h"
+#include "df/viewscreen_layer_unit_actionst.h"
#include "df/squad_order_trainst.h"
#include "df/ui_build_selector.h"
#include "df/building_trapst.h"
#include "df/item_actual.h"
+#include "df/item_liquipowder.h"
+#include "df/item_barst.h"
+#include "df/item_threadst.h"
+#include "df/item_clothst.h"
#include "df/contaminant.h"
+#include "df/layer_object.h"
+#include "df/reaction.h"
+#include "df/reaction_reagent_itemst.h"
+#include "df/reaction_reagent_flags.h"
#include
@@ -89,6 +98,17 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector \n"
+ " Further improves temperature updates by ensuring that 1 degree of\n"
+ " item temperature is crossed in no more than specified number of frames\n"
+ " when updating from the environment temperature. Use 0 to disable.\n"
+ " tweak fix-dimensions [disable]\n"
+ " Fixes subtracting small amount of thread/cloth/liquid from a stack\n"
+ " by splitting the stack and subtracting from the remaining single item.\n"
+ " tweak advmode-contained [disable]\n"
+ " Fixes custom reactions with container inputs in advmode. The issue is\n"
+ " that the screen tries to force you to select the contents separately\n"
+ " from the container. This forcefully skips child reagents.\n"
));
return CR_OK;
}
@@ -293,6 +313,187 @@ struct stable_temp_hook : df::item_actual {
IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, adjustTemperature);
IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, updateContaminants);
+static int map_temp_mult = -1;
+static int max_heat_ticks = 0;
+
+struct fast_heat_hook : df::item_actual {
+ typedef df::item_actual interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(
+ bool, updateTempFromMap,
+ (bool local, bool contained, bool adjust, int32_t rate_mult)
+ ) {
+ int cmult = map_temp_mult;
+ map_temp_mult = rate_mult;
+
+ bool rv = INTERPOSE_NEXT(updateTempFromMap)(local, contained, adjust, rate_mult);
+ map_temp_mult = cmult;
+ return rv;
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(
+ bool, updateTemperature,
+ (uint16_t temp, bool local, bool contained, bool adjust, int32_t rate_mult)
+ ) {
+ // Some items take ages to cross the last degree, so speed them up
+ if (map_temp_mult > 0 && temp != temperature && max_heat_ticks > 0)
+ {
+ int spec = getSpecHeat();
+ if (spec != 60001)
+ rate_mult = std::max(map_temp_mult, spec/max_heat_ticks/abs(temp - temperature));
+ }
+
+ return INTERPOSE_NEXT(updateTemperature)(temp, local, contained, adjust, rate_mult);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t rate_mult))
+ {
+ if (map_temp_mult > 0)
+ rate_mult = map_temp_mult;
+
+ return INTERPOSE_NEXT(adjustTemperature)(temp, rate_mult);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(fast_heat_hook, updateTempFromMap);
+IMPLEMENT_VMETHOD_INTERPOSE(fast_heat_hook, updateTemperature);
+IMPLEMENT_VMETHOD_INTERPOSE(fast_heat_hook, adjustTemperature);
+
+static void correct_dimension(df::item_actual *self, int32_t &delta, int32_t dim)
+{
+ // Zero dimension or remainder?
+ if (dim <= 0 || self->stack_size <= 1) return;
+ int rem = delta % dim;
+ if (rem == 0) return;
+ // If destroys, pass through
+ int intv = delta / dim;
+ if (intv >= self->stack_size) return;
+ // Subtract int part
+ delta = rem;
+ self->stack_size -= intv;
+ if (self->stack_size <= 1) return;
+
+ // If kills the item or cannot split, round up.
+ if (!self->flags.bits.in_inventory || !Items::getContainer(self))
+ {
+ delta = dim;
+ return;
+ }
+
+ // Otherwise split the stack
+ color_ostream_proxy out(Core::getInstance().getConsole());
+ out.print("fix-dimensions: splitting stack #%d for delta %d.\n", self->id, delta);
+
+ auto copy = self->splitStack(self->stack_size-1, true);
+ if (copy) copy->categorize(true);
+}
+
+struct dimension_lqp_hook : df::item_liquipowder {
+ typedef df::item_liquipowder interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(bool, subtractDimension, (int32_t delta))
+ {
+ correct_dimension(this, delta, dimension);
+ return INTERPOSE_NEXT(subtractDimension)(delta);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(dimension_lqp_hook, subtractDimension);
+
+struct dimension_bar_hook : df::item_barst {
+ typedef df::item_barst interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(bool, subtractDimension, (int32_t delta))
+ {
+ correct_dimension(this, delta, dimension);
+ return INTERPOSE_NEXT(subtractDimension)(delta);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(dimension_bar_hook, subtractDimension);
+
+struct dimension_thread_hook : df::item_threadst {
+ typedef df::item_threadst interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(bool, subtractDimension, (int32_t delta))
+ {
+ correct_dimension(this, delta, dimension);
+ return INTERPOSE_NEXT(subtractDimension)(delta);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(dimension_thread_hook, subtractDimension);
+
+struct dimension_cloth_hook : df::item_clothst {
+ typedef df::item_clothst interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(bool, subtractDimension, (int32_t delta))
+ {
+ correct_dimension(this, delta, dimension);
+ return INTERPOSE_NEXT(subtractDimension)(delta);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(dimension_cloth_hook, subtractDimension);
+
+struct advmode_contained_hook : df::viewscreen_layer_unit_actionst {
+ typedef df::viewscreen_layer_unit_actionst interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input))
+ {
+ auto old_reaction = cur_reaction;
+ auto old_reagent = reagent;
+
+ INTERPOSE_NEXT(feed)(input);
+
+ if (cur_reaction && (cur_reaction != old_reaction || reagent != old_reagent))
+ {
+ old_reagent = reagent;
+
+ // Skip reagents already contained by others
+ while (reagent < (int)cur_reaction->reagents.size()-1)
+ {
+ if (!cur_reaction->reagents[reagent]->flags.bits.IN_CONTAINER)
+ break;
+ reagent++;
+ }
+
+ if (old_reagent != reagent)
+ {
+ // Reproduces a tiny part of the orginal screen code
+ choice_items.clear();
+
+ auto preagent = cur_reaction->reagents[reagent];
+ reagent_amnt_left = preagent->quantity;
+
+ for (int i = held_items.size()-1; i >= 0; i--)
+ {
+ if (!preagent->matchesRoot(held_items[i], cur_reaction->index))
+ continue;
+ if (linear_index(sel_items, held_items[i]) >= 0)
+ continue;
+ choice_items.push_back(held_items[i]);
+ }
+
+ layer_objects[6]->setListLength(choice_items.size());
+
+ if (!choice_items.empty())
+ {
+ layer_objects[4]->active = layer_objects[5]->active = false;
+ layer_objects[6]->active = true;
+ }
+ else if (layer_objects[6]->active)
+ {
+ layer_objects[6]->active = false;
+ layer_objects[5]->active = true;
+ }
+ }
+ }
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(advmode_contained_hook, feed);
+
static void enable_hook(color_ostream &out, VMethodInterposeLinkBase &hook, vector ¶meters)
{
if (vector_get(parameters, 1) == "disable")
@@ -430,6 +631,28 @@ static command_result tweak(color_ostream &out, vector ¶meters)
enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, adjustTemperature), parameters);
enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, updateContaminants), parameters);
}
+ else if (cmd == "fast-heat")
+ {
+ if (parameters.size() < 2)
+ return CR_WRONG_USAGE;
+ max_heat_ticks = atoi(parameters[1].c_str());
+ if (max_heat_ticks <= 0)
+ parameters[1] = "disable";
+ enable_hook(out, INTERPOSE_HOOK(fast_heat_hook, updateTempFromMap), parameters);
+ enable_hook(out, INTERPOSE_HOOK(fast_heat_hook, updateTemperature), parameters);
+ enable_hook(out, INTERPOSE_HOOK(fast_heat_hook, adjustTemperature), parameters);
+ }
+ else if (cmd == "fix-dimensions")
+ {
+ enable_hook(out, INTERPOSE_HOOK(dimension_lqp_hook, subtractDimension), parameters);
+ enable_hook(out, INTERPOSE_HOOK(dimension_bar_hook, subtractDimension), parameters);
+ enable_hook(out, INTERPOSE_HOOK(dimension_thread_hook, subtractDimension), parameters);
+ enable_hook(out, INTERPOSE_HOOK(dimension_cloth_hook, subtractDimension), parameters);
+ }
+ else if (cmd == "advmode-contained")
+ {
+ enable_hook(out, INTERPOSE_HOOK(advmode_contained_hook, feed), parameters);
+ }
else
return CR_WRONG_USAGE;
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/fix/stable-temp.lua b/scripts/fix/stable-temp.lua
index d06d0fcce..27a88ef7b 100644
--- a/scripts/fix/stable-temp.lua
+++ b/scripts/fix/stable-temp.lua
@@ -1,5 +1,9 @@
-- Reset item temperature to the value of their tile.
+local args = {...}
+
+local apply = (args[1] == 'apply')
+
local count = 0
local types = {}
@@ -9,13 +13,16 @@ local function update_temp(item,btemp)
local tid = item:getType()
types[tid] = (types[tid] or 0) + 1
end
- item.temperature = btemp
- item.temperature_fraction = 0
- if item.contaminants then
- for _,c in ipairs(item.contaminants) do
- c.temperature = btemp
- c.temperature_fraction = 0
+ if apply then
+ item.temperature = btemp
+ item.temperature_fraction = 0
+
+ if item.contaminants then
+ for _,c in ipairs(item.contaminants) do
+ c.temperature = btemp
+ c.temperature_fraction = 0
+ end
end
end
@@ -23,7 +30,9 @@ local function update_temp(item,btemp)
update_temp(sub,btemp)
end
- item:checkTemperatureDamage()
+ if apply then
+ item:checkTemperatureDamage()
+ end
end
local last_frame = df.global.world.frame_counter-1
@@ -39,7 +48,11 @@ for _,item in ipairs(df.global.world.items.all) do
end
end
-print('Items updated: '..count)
+if apply then
+ print('Items updated: '..count)
+else
+ print('Items not in equilibrium: '..count)
+end
local tlist = {}
for k,_ in pairs(types) do tlist[#tlist+1] = k end
@@ -47,3 +60,7 @@ table.sort(tlist, function(a,b) return types[a] > types[b] end)
for _,k in ipairs(tlist) do
print(' '..df.item_type[k]..':', types[k])
end
+
+if not apply then
+ print("Use 'fix/stable-temp apply' to force-change temperature.")
+end
diff --git a/scripts/gui/hello-world.lua b/scripts/gui/hello-world.lua
index 80986bbf6..c8cd3bd01 100644
--- a/scripts/gui/hello-world.lua
+++ b/scripts/gui/hello-world.lua
@@ -4,19 +4,21 @@ local gui = require 'gui'
local text = 'Woohoo, lua viewscreen :)'
-local screen = mkinstance(gui.FramedScreen, {
+local screen = gui.FramedScreen{
frame_style = gui.GREY_LINE_FRAME,
frame_title = 'Hello World',
frame_width = #text+6,
frame_height = 3,
- onRenderBody = function(self, dc)
- dc:seek(3,1):string(text, COLOR_LIGHTGREEN)
- end,
- onInput = function(self,keys)
- if keys.LEAVESCREEN or keys.SELECT then
- self:dismiss()
- end
+}
+
+function screen:onRenderBody(dc)
+ dc:seek(3,1):string(text, COLOR_LIGHTGREEN)
+end
+
+function screen:onInput(keys)
+ if keys.LEAVESCREEN or keys.SELECT then
+ self:dismiss()
end
-}):init()
+end
screen:show()
diff --git a/scripts/gui/liquids.lua b/scripts/gui/liquids.lua
index 89f08b7cf..cddb9f01d 100644
--- a/scripts/gui/liquids.lua
+++ b/scripts/gui/liquids.lua
@@ -53,13 +53,7 @@ local permaflows = {
Toggle = defclass(Toggle)
-function Toggle:init(items)
- self:init_fields{
- items = items,
- selected = 1
- }
- return self
-end
+Toggle.ATTRS{ items = {}, selected = 1 }
function Toggle:get()
return self.items[self.selected]
@@ -89,16 +83,14 @@ LiquidsUI = defclass(LiquidsUI, guidm.MenuOverlay)
LiquidsUI.focus_path = 'liquids'
function LiquidsUI:init()
- self:init_fields{
- brush = mkinstance(Toggle):init(brushes),
- paint = mkinstance(Toggle):init(paints),
- flow = mkinstance(Toggle):init(flowbits),
- set = mkinstance(Toggle):init(setmode),
- permaflow = mkinstance(Toggle):init(permaflows),
+ self:assign{
+ brush = Toggle{ items = brushes },
+ paint = Toggle{ items = paints },
+ flow = Toggle{ items = flowbits },
+ set = Toggle{ items = setmode },
+ permaflow = Toggle{ items = permaflows },
amount = 7,
}
- guidm.MenuOverlay.init(self)
- return self
end
function LiquidsUI:onDestroy()
@@ -201,6 +193,7 @@ function LiquidsUI:onRenderBody(dc)
end
function ensure_blocks(cursor, size, cb)
+ size = size or xyz2pos(1,1,1)
local cx,cy,cz = pos2xyz(cursor)
local all = true
for x=1,size.x or 1,16 do
@@ -298,5 +291,5 @@ if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/LookAround') then
qerror("This script requires the main dwarfmode view in 'k' mode")
end
-local list = mkinstance(LiquidsUI):init()
+local list = LiquidsUI()
list:show()
diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua
index 4468e1dcb..d1e8ec803 100644
--- a/scripts/gui/mechanisms.lua
+++ b/scripts/gui/mechanisms.lua
@@ -43,13 +43,11 @@ MechanismList = defclass(MechanismList, guidm.MenuOverlay)
MechanismList.focus_path = 'mechanisms'
-function MechanismList:init(building)
- self:init_fields{
+function MechanismList:init(info)
+ self:assign{
links = {}, selected = 1
}
- guidm.MenuOverlay.init(self)
- self:fillList(building)
- return self
+ self:fillList(info.building)
end
function MechanismList:fillList(building)
@@ -122,10 +120,10 @@ 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
-local list = mkinstance(MechanismList):init(df.global.world.selected_building)
+local list = MechanismList{ building = df.global.world.selected_building }
list:show()
list:changeSelected(1)
diff --git a/scripts/gui/power-meter.lua b/scripts/gui/power-meter.lua
index 8baf43e7e..6c2f699ac 100644
--- a/scripts/gui/power-meter.lua
+++ b/scripts/gui/power-meter.lua
@@ -13,15 +13,13 @@ PowerMeter = defclass(PowerMeter, guidm.MenuOverlay)
PowerMeter.focus_path = 'power-meter'
function PowerMeter:init()
- self:init_fields{
+ self:assign{
min_power = 0, max_power = -1, invert = false,
}
- guidm.MenuOverlay.init(self)
- return self
end
function PowerMeter:onShow()
- guidm.MenuOverlay.onShow(self)
+ PowerMeter.super.onShow(self)
-- Send an event to update the errors
bselector.plate_info.flags.whole = 0
@@ -112,5 +110,5 @@ then
qerror("This script requires the main dwarfmode view in build pressure plate mode")
end
-local list = mkinstance(PowerMeter):init()
+local list = PowerMeter()
list:show()
diff --git a/scripts/gui/room-list.lua b/scripts/gui/room-list.lua
index a4507466f..0de82db5f 100644
--- a/scripts/gui/room-list.lua
+++ b/scripts/gui/room-list.lua
@@ -78,15 +78,17 @@ RoomList = defclass(RoomList, guidm.MenuOverlay)
RoomList.focus_path = 'room-list'
-function RoomList:init(unit)
+RoomList.ATTRS{ unit = DEFAULT_NIL }
+
+function RoomList:init(info)
+ local unit = info.unit
local base_bld = df.global.world.selected_building
- self:init_fields{
- unit = unit, base_building = base_bld,
+ self:assign{
+ base_building = base_bld,
items = {}, selected = 1,
own_rooms = {}, spouse_rooms = {}
}
- guidm.MenuOverlay.init(self)
self.old_viewport = self:getViewport()
self.old_cursor = guidm.getCursorPos()
@@ -115,8 +117,6 @@ function RoomList:init(unit)
self.items = concat_lists({self.base_item}, self.items)
::found::
end
-
- return self
end
local sex_char = { [0] = 12, [1] = 11 }
@@ -235,12 +235,13 @@ function RoomList:onInput(keys)
end
local focus = dfhack.gui.getCurFocus()
-if focus == 'dwarfmode/QueryBuilding/Some' then
- local base = df.global.world.selected_building
- mkinstance(RoomList):init(base.owner):show()
-elseif focus == 'dwarfmode/QueryBuilding/Some/Assign/Unit' then
+
+if focus == 'dwarfmode/QueryBuilding/Some/Assign/Unit' then
local unit = df.global.ui_building_assign_units[df.global.ui_building_item_cursor]
- mkinstance(RoomList):init(unit):show()
+ RoomList{ unit = unit }:show()
+elseif string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then
+ local base = df.global.world.selected_building
+ RoomList{ unit = base.owner }:show()
else
qerror("This script requires the main dwarfmode view in 'q' mode")
end
diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua
new file mode 100644
index 000000000..c98cb1676
--- /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 = 'cages', item_type = df.item_type.CAGE },
+ { 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'
+
+SiegeEngine.ATTRS{ building = DEFAULT_NIL }
+
+function SiegeEngine:init()
+ self:assign{
+ center = utils.getBuildingCenter(self.building),
+ selected_pile = 1,
+ mode_main = {
+ render = self:callback 'onRenderBody_main',
+ input = self:callback 'onInput_main',
+ },
+ mode_aim = {
+ render = self:callback 'onRenderBody_aim',
+ input = self:callback 'onInput_aim',
+ },
+ mode_pile = {
+ render = self:callback 'onRenderBody_pile',
+ input = self:callback 'onInput_pile',
+ }
+ }
+end
+
+function SiegeEngine:onShow()
+ SiegeEngine.super.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 = SiegeEngine{ building = building }
+list:show()
diff --git a/scripts/setfps.lua b/scripts/setfps.lua
new file mode 100644
index 000000000..690f82702
--- /dev/null
+++ b/scripts/setfps.lua
@@ -0,0 +1,10 @@
+-- Set the FPS cap at runtime.
+
+local cap = ...
+local capnum = tonumber(cap)
+
+if not capnum or capnum < 1 then
+ qerror('Invalid FPS cap value: '..cap)
+end
+
+df.global.enabler.fps = capnum