summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKelly Martin2012-09-16 16:36:51 -0500
committerKelly Martin2012-09-16 16:36:51 -0500
commit38f920dd658ba32dbe5126654b21f776386f07d5 (patch)
treef48c91a895917b0715ce5f7fb5e25f98258d2c08
parent96fec768c73aee3fca8937017ec8ef59cf0d7adb (diff)
parentc927623050708da1276e109899549b6f2577180f (diff)
downloaddfhack-38f920dd658ba32dbe5126654b21f776386f07d5.tar.gz
dfhack-38f920dd658ba32dbe5126654b21f776386f07d5.tar.bz2
dfhack-38f920dd658ba32dbe5126654b21f776386f07d5.tar.xz
Merge remote-tracking branch 'q/master'
-rw-r--r--LUA_API.rst59
-rw-r--r--Lua API.html149
-rw-r--r--depends/protobuf/CMakeLists.txt6
-rw-r--r--dfhack.init-example7
-rw-r--r--library/CMakeLists.txt4
-rw-r--r--library/LuaApi.cpp67
-rw-r--r--library/LuaTools.cpp37
-rw-r--r--library/Process-darwin.cpp8
-rw-r--r--library/Process-linux.cpp8
-rw-r--r--library/Process-windows.cpp5
-rw-r--r--library/RemoteClient.cpp2
-rw-r--r--library/RemoteServer.cpp2
-rw-r--r--library/RemoteTools.cpp2
-rw-r--r--library/include/DataDefs.h2
-rw-r--r--library/include/LuaTools.h5
-rw-r--r--library/include/MemAccess.h3
-rw-r--r--library/include/RemoteTools.h2
-rw-r--r--library/include/modules/Items.h4
-rw-r--r--library/include/modules/MapCache.h10
-rw-r--r--library/include/modules/Maps.h2
-rw-r--r--library/include/modules/Screen.h6
-rw-r--r--library/include/modules/Units.h20
-rw-r--r--library/lua/dfhack.lua27
-rw-r--r--library/lua/gui.lua20
-rw-r--r--library/lua/gui/dialogs.lua271
-rw-r--r--library/lua/gui/dwarfmode.lua82
-rw-r--r--library/lua/utils.lua13
-rw-r--r--library/modules/Gui.cpp16
-rw-r--r--library/modules/Items.cpp45
-rw-r--r--library/modules/Job.cpp2
-rw-r--r--library/modules/Maps.cpp60
-rw-r--r--library/modules/Screen.cpp39
-rw-r--r--library/modules/Units.cpp481
-rw-r--r--library/modules/World.cpp10
-rw-r--r--library/modules/kitchen.cpp2
m---------library/xml0
-rwxr-xr-xpackage/darwin/dfhack-run3
-rwxr-xr-xpackage/darwin/fix-libs.sh21
-rw-r--r--plugins/CMakeLists.txt7
-rw-r--r--plugins/autolabor.cpp349
-rw-r--r--plugins/cleaners.cpp4
-rw-r--r--plugins/cleanowned.cpp2
-rw-r--r--plugins/devel/CMakeLists.txt2
-rw-r--r--plugins/devel/siege-engine.cpp1649
-rw-r--r--plugins/jobutils.cpp2
-rw-r--r--plugins/lua/power-meter.lua11
-rw-r--r--plugins/lua/rename.lua13
-rw-r--r--plugins/lua/siege-engine.lua45
-rw-r--r--plugins/manipulator.cpp163
-rw-r--r--plugins/power-meter.cpp237
-rw-r--r--plugins/proto/rename.proto7
-rw-r--r--plugins/raw/building_steam_engine.txt (renamed from plugins/devel/building_steam_engine.txt)0
-rw-r--r--plugins/raw/item_trapcomp_steam_engine.txt (renamed from plugins/devel/item_trapcomp_steam_engine.txt)4
-rw-r--r--plugins/raw/reaction_steam_engine.txt (renamed from plugins/devel/reaction_steam_engine.txt)0
-rw-r--r--plugins/rename.cpp201
-rw-r--r--plugins/steam-engine.cpp (renamed from plugins/devel/steam-engine.cpp)82
m---------plugins/stonesense0
-rw-r--r--plugins/tiletypes.cpp2
-rw-r--r--plugins/workflow.cpp2
-rw-r--r--plugins/zone.cpp4
-rw-r--r--scripts/devel/pop-screen.lua3
-rw-r--r--scripts/gui/liquids.lua41
-rw-r--r--scripts/gui/mechanisms.lua2
-rw-r--r--scripts/gui/power-meter.lua116
-rw-r--r--scripts/gui/rename.lua63
-rw-r--r--scripts/gui/siege-engine.lua490
66 files changed, 4654 insertions, 349 deletions
diff --git a/LUA_API.rst b/LUA_API.rst
index 542034f4..1ffdada0 100644
--- a/LUA_API.rst
+++ b/LUA_API.rst
@@ -550,6 +550,20 @@ Exception handling
The default value of the ``verbose`` argument of ``err:tostring()``.
+Miscellaneous
+-------------
+
+* ``dfhack.VERSION``
+
+ DFHack version string constant.
+
+* ``dfhack.curry(func,args...)``, or ``curry(func,args...)``
+
+ Returns a closure that invokes the function with args combined
+ both from the curry call and the closure call itself. I.e.
+ ``curry(func,a,b)(c,d)`` equals ``func(a,b,c,d)``.
+
+
Locking and finalization
------------------------
@@ -709,6 +723,10 @@ can be omitted.
Returns the dfhack directory path, i.e. ``".../df/hack/"``.
+* ``dfhack.getTickCount()``
+
+ Returns the tick count in ms, exactly as DF ui uses.
+
* ``dfhack.isWorldLoaded()``
Checks if the world is loaded.
@@ -850,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.
@@ -876,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*.
@@ -953,6 +999,10 @@ Items module
Move the item to the unit inventory. Returns *false* if impossible.
+* ``dfhack.items.makeProjectile(item)``
+
+ Turns the item into a projectile, and returns the new object, or *nil* if impossible.
+
Maps module
-----------
@@ -977,6 +1027,10 @@ Maps module
Returns a map block object for given df::coord or x,y,z in local tile coordinates.
+* ``dfhack.maps.ensureTileBlock(coords)``, or ``ensureTileBlock(x,y,z)``
+
+ Like ``getTileBlock``, but if the block is not allocated, try creating it.
+
* ``dfhack.maps.getRegionBiome(region_coord2d)``, or ``getRegionBiome(x,y)``
Returns the biome info struct for the given global map region.
@@ -1286,6 +1340,11 @@ Basic painting functions:
Returns *false* if coordinates out of bounds, or other error.
+* ``dfhack.screen.readTile(x,y)``
+
+ Retrieves the contents of the specified tile from the screen buffers.
+ Returns a pen, or *nil* if invalid or TrueType.
+
* ``dfhack.screen.paintString(pen,x,y,text)``
Paints the string starting at *x,y*. Uses the string characters
diff --git a/Lua API.html b/Lua API.html
index 63a4c854..168f7dcc 100644
--- a/Lua API.html
+++ b/Lua API.html
@@ -337,42 +337,43 @@ ul.auto-toc {
<li><a class="reference internal" href="#native-utilities" id="id11">Native utilities</a><ul>
<li><a class="reference internal" href="#input-output" id="id12">Input &amp; Output</a></li>
<li><a class="reference internal" href="#exception-handling" id="id13">Exception handling</a></li>
-<li><a class="reference internal" href="#locking-and-finalization" id="id14">Locking and finalization</a></li>
-<li><a class="reference internal" href="#persistent-configuration-storage" id="id15">Persistent configuration storage</a></li>
-<li><a class="reference internal" href="#material-info-lookup" id="id16">Material info lookup</a></li>
+<li><a class="reference internal" href="#miscellaneous" id="id14">Miscellaneous</a></li>
+<li><a class="reference internal" href="#locking-and-finalization" id="id15">Locking and finalization</a></li>
+<li><a class="reference internal" href="#persistent-configuration-storage" id="id16">Persistent configuration storage</a></li>
+<li><a class="reference internal" href="#material-info-lookup" id="id17">Material info lookup</a></li>
</ul>
</li>
-<li><a class="reference internal" href="#c-function-wrappers" id="id17">C++ function wrappers</a><ul>
-<li><a class="reference internal" href="#gui-module" id="id18">Gui module</a></li>
-<li><a class="reference internal" href="#job-module" id="id19">Job module</a></li>
-<li><a class="reference internal" href="#units-module" id="id20">Units module</a></li>
-<li><a class="reference internal" href="#items-module" id="id21">Items module</a></li>
-<li><a class="reference internal" href="#maps-module" id="id22">Maps module</a></li>
-<li><a class="reference internal" href="#burrows-module" id="id23">Burrows module</a></li>
-<li><a class="reference internal" href="#buildings-module" id="id24">Buildings module</a></li>
-<li><a class="reference internal" href="#constructions-module" id="id25">Constructions module</a></li>
-<li><a class="reference internal" href="#screen-api" id="id26">Screen API</a></li>
-<li><a class="reference internal" href="#internal-api" id="id27">Internal API</a></li>
+<li><a class="reference internal" href="#c-function-wrappers" id="id18">C++ function wrappers</a><ul>
+<li><a class="reference internal" href="#gui-module" id="id19">Gui module</a></li>
+<li><a class="reference internal" href="#job-module" id="id20">Job module</a></li>
+<li><a class="reference internal" href="#units-module" id="id21">Units module</a></li>
+<li><a class="reference internal" href="#items-module" id="id22">Items module</a></li>
+<li><a class="reference internal" href="#maps-module" id="id23">Maps module</a></li>
+<li><a class="reference internal" href="#burrows-module" id="id24">Burrows module</a></li>
+<li><a class="reference internal" href="#buildings-module" id="id25">Buildings module</a></li>
+<li><a class="reference internal" href="#constructions-module" id="id26">Constructions module</a></li>
+<li><a class="reference internal" href="#screen-api" id="id27">Screen API</a></li>
+<li><a class="reference internal" href="#internal-api" id="id28">Internal API</a></li>
</ul>
</li>
-<li><a class="reference internal" href="#core-interpreter-context" id="id28">Core interpreter context</a><ul>
-<li><a class="reference internal" href="#event-type" id="id29">Event type</a></li>
+<li><a class="reference internal" href="#core-interpreter-context" id="id29">Core interpreter context</a><ul>
+<li><a class="reference internal" href="#event-type" id="id30">Event type</a></li>
</ul>
</li>
</ul>
</li>
-<li><a class="reference internal" href="#lua-modules" id="id30">Lua Modules</a><ul>
-<li><a class="reference internal" href="#global-environment" id="id31">Global environment</a></li>
-<li><a class="reference internal" href="#utils" id="id32">utils</a></li>
-<li><a class="reference internal" href="#dumper" id="id33">dumper</a></li>
+<li><a class="reference internal" href="#lua-modules" id="id31">Lua Modules</a><ul>
+<li><a class="reference internal" href="#global-environment" id="id32">Global environment</a></li>
+<li><a class="reference internal" href="#utils" id="id33">utils</a></li>
+<li><a class="reference internal" href="#dumper" id="id34">dumper</a></li>
</ul>
</li>
-<li><a class="reference internal" href="#plugins" id="id34">Plugins</a><ul>
-<li><a class="reference internal" href="#burrows" id="id35">burrows</a></li>
-<li><a class="reference internal" href="#sort" id="id36">sort</a></li>
+<li><a class="reference internal" href="#plugins" id="id35">Plugins</a><ul>
+<li><a class="reference internal" href="#burrows" id="id36">burrows</a></li>
+<li><a class="reference internal" href="#sort" id="id37">sort</a></li>
</ul>
</li>
-<li><a class="reference internal" href="#scripts" id="id37">Scripts</a></li>
+<li><a class="reference internal" href="#scripts" id="id38">Scripts</a></li>
</ul>
</div>
<p>The current version of DFHack has extensive support for
@@ -842,8 +843,21 @@ following properties:</p>
</li>
</ul>
</div>
+<div class="section" id="miscellaneous">
+<h3><a class="toc-backref" href="#id14">Miscellaneous</a></h3>
+<ul>
+<li><p class="first"><tt class="docutils literal">dfhack.VERSION</tt></p>
+<p>DFHack version string constant.</p>
+</li>
+<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.curry(func,args...)</span></tt>, or <tt class="docutils literal"><span class="pre">curry(func,args...)</span></tt></p>
+<p>Returns a closure that invokes the function with args combined
+both from the curry call and the closure call itself. I.e.
+<tt class="docutils literal"><span class="pre">curry(func,a,b)(c,d)</span></tt> equals <tt class="docutils literal">func(a,b,c,d)</tt>.</p>
+</li>
+</ul>
+</div>
<div class="section" id="locking-and-finalization">
-<h3><a class="toc-backref" href="#id14">Locking and finalization</a></h3>
+<h3><a class="toc-backref" href="#id15">Locking and finalization</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.with_suspend(f[,args...])</span></tt></p>
<p>Calls <tt class="docutils literal">f</tt> with arguments after grabbing the DF core suspend lock.
@@ -876,7 +890,7 @@ Implemented using <tt class="docutils literal"><span class="pre">call_with_final
</ul>
</div>
<div class="section" id="persistent-configuration-storage">
-<h3><a class="toc-backref" href="#id15">Persistent configuration storage</a></h3>
+<h3><a class="toc-backref" href="#id16">Persistent configuration storage</a></h3>
<p>This api is intended for storing configuration options in the world itself.
It probably should be restricted to data that is world-dependent.</p>
<p>Entries are identified by a string <tt class="docutils literal">key</tt>, but it is also possible to manage
@@ -911,7 +925,7 @@ functions can just copy values in memory without doing any actual I/O.
However, currently every entry has a 180+-byte dead-weight overhead.</p>
</div>
<div class="section" id="material-info-lookup">
-<h3><a class="toc-backref" href="#id16">Material info lookup</a></h3>
+<h3><a class="toc-backref" href="#id17">Material info lookup</a></h3>
<p>A material info record has fields:</p>
<ul>
<li><p class="first"><tt class="docutils literal">type</tt>, <tt class="docutils literal">index</tt>, <tt class="docutils literal">material</tt></p>
@@ -956,7 +970,7 @@ Accept dfhack_material_category auto-assign table.</p>
</div>
</div>
<div class="section" id="c-function-wrappers">
-<h2><a class="toc-backref" href="#id17">C++ function wrappers</a></h2>
+<h2><a class="toc-backref" href="#id18">C++ function wrappers</a></h2>
<p>Thin wrappers around C++ functions, similar to the ones for virtual methods.
One notable difference is that these explicit wrappers allow argument count
adjustment according to the usual lua rules, so trailing false/nil arguments
@@ -974,6 +988,9 @@ can be omitted.</p>
<li><p class="first"><tt class="docutils literal">dfhack.getHackPath()</tt></p>
<p>Returns the dfhack directory path, i.e. <tt class="docutils literal"><span class="pre">&quot;.../df/hack/&quot;</span></tt>.</p>
</li>
+<li><p class="first"><tt class="docutils literal">dfhack.getTickCount()</tt></p>
+<p>Returns the tick count in ms, exactly as DF ui uses.</p>
+</li>
<li><p class="first"><tt class="docutils literal">dfhack.isWorldLoaded()</tt></p>
<p>Checks if the world is loaded.</p>
</li>
@@ -985,7 +1002,7 @@ can be omitted.</p>
</li>
</ul>
<div class="section" id="gui-module">
-<h3><a class="toc-backref" href="#id18">Gui module</a></h3>
+<h3><a class="toc-backref" href="#id19">Gui module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.gui.getCurViewscreen([skip_dismissed])</span></tt></p>
<p>Returns the topmost viewscreen. If <tt class="docutils literal">skip_dismissed</tt> is <em>true</em>,
@@ -1032,7 +1049,7 @@ above operations accordingly. If enabled, pauses and zooms to position.</p>
</ul>
</div>
<div class="section" id="job-module">
-<h3><a class="toc-backref" href="#id19">Job module</a></h3>
+<h3><a class="toc-backref" href="#id20">Job module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.job.cloneJobStruct(job)</tt></p>
<p>Creates a deep copy of the given job.</p>
@@ -1069,7 +1086,7 @@ a lua list containing them.</p>
</ul>
</div>
<div class="section" id="units-module">
-<h3><a class="toc-backref" href="#id20">Units module</a></h3>
+<h3><a class="toc-backref" href="#id21">Units module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.units.getPosition(unit)</tt></p>
<p>Returns true <em>x,y,z</em> of the unit, or <em>nil</em> if invalid; may be not equal to unit.pos if caged.</p>
@@ -1089,6 +1106,26 @@ a lua list containing them.</p>
<li><p class="first"><tt class="docutils literal">dfhack.units.getNemesis(unit)</tt></p>
<p>Returns the nemesis record of the unit if it has one, or <em>nil</em>.</p>
</li>
+<li><p class="first"><tt class="docutils literal">dfhack.units.isHidingCurse(unit)</tt></p>
+<p>Checks if the unit hides improved attributes from its curse.</p>
+</li>
+<li><p class="first"><tt class="docutils literal">dfhack.units.getPhysicalAttrValue(unit, attr_type)</tt></p>
+</li>
+<li><p class="first"><tt class="docutils literal">dfhack.units.getMentalAttrValue(unit, attr_type)</tt></p>
+<p>Computes the effective attribute value, including curse effect.</p>
+</li>
+<li><p class="first"><tt class="docutils literal">dfhack.units.isCrazed(unit)</tt></p>
+</li>
+<li><p class="first"><tt class="docutils literal">dfhack.units.isOpposedToLife(unit)</tt></p>
+</li>
+<li><p class="first"><tt class="docutils literal">dfhack.units.hasExtravision(unit)</tt></p>
+</li>
+<li><p class="first"><tt class="docutils literal">dfhack.units.isBloodsucker(unit)</tt></p>
+<p>Simple checks of caste attributes that can be modified by curses.</p>
+</li>
+<li><p class="first"><tt class="docutils literal">dfhack.units.getMiscTrait(unit, type[, create])</tt></p>
+<p>Finds (or creates if requested) a misc trait object with the given id.</p>
+</li>
<li><p class="first"><tt class="docutils literal">dfhack.units.isDead(unit)</tt></p>
<p>The unit is completely dead and passive, or a ghost.</p>
</li>
@@ -1109,6 +1146,12 @@ same checks the game uses to decide game-over by extinction.</p>
<p>Returns the age of the unit in years as a floating-point value.
If <tt class="docutils literal">true_age</tt> is true, ignores false identities.</p>
</li>
+<li><p class="first"><tt class="docutils literal">dfhack.units.getEffectiveSkill(unit, skill)</tt></p>
+<p>Computes the effective rating for the given skill, taking into account exhaustion, pain etc.</p>
+</li>
+<li><p class="first"><tt class="docutils literal">dfhack.units.computeMovementSpeed(unit)</tt></p>
+<p>Computes number of frames * 100 it takes the unit to move in its current state of mind and body.</p>
+</li>
<li><p class="first"><tt class="docutils literal">dfhack.units.getNoblePositions(unit)</tt></p>
<p>Returns a list of tables describing noble position assignments, or <em>nil</em>.
Every table has fields <tt class="docutils literal">entity</tt>, <tt class="docutils literal">assignment</tt> and <tt class="docutils literal">position</tt>.</p>
@@ -1130,7 +1173,7 @@ or raws. The <tt class="docutils literal">ignore_noble</tt> boolean disables the
</ul>
</div>
<div class="section" id="items-module">
-<h3><a class="toc-backref" href="#id21">Items module</a></h3>
+<h3><a class="toc-backref" href="#id22">Items module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.items.getPosition(item)</tt></p>
<p>Returns true <em>x,y,z</em> of the item, or <em>nil</em> if invalid; may be not equal to item.pos if in inventory.</p>
@@ -1170,10 +1213,13 @@ Returns <em>false</em> in case of error.</p>
<li><p class="first"><tt class="docutils literal">dfhack.items.moveToInventory(item,unit,use_mode,body_part)</tt></p>
<p>Move the item to the unit inventory. Returns <em>false</em> if impossible.</p>
</li>
+<li><p class="first"><tt class="docutils literal">dfhack.items.makeProjectile(item)</tt></p>
+<p>Turns the item into a projectile, and returns the new object, or <em>nil</em> if impossible.</p>
+</li>
</ul>
</div>
<div class="section" id="maps-module">
-<h3><a class="toc-backref" href="#id22">Maps module</a></h3>
+<h3><a class="toc-backref" href="#id23">Maps module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.maps.getSize()</tt></p>
<p>Returns map size in blocks: <em>x, y, z</em></p>
@@ -1190,6 +1236,9 @@ Returns <em>false</em> in case of error.</p>
<li><p class="first"><tt class="docutils literal">dfhack.maps.getTileBlock(coords)</tt>, or <tt class="docutils literal">getTileBlock(x,y,z)</tt></p>
<p>Returns a map block object for given df::coord or x,y,z in local tile coordinates.</p>
</li>
+<li><p class="first"><tt class="docutils literal">dfhack.maps.ensureTileBlock(coords)</tt>, or <tt class="docutils literal">ensureTileBlock(x,y,z)</tt></p>
+<p>Like <tt class="docutils literal">getTileBlock</tt>, but if the block is not allocated, try creating it.</p>
+</li>
<li><p class="first"><tt class="docutils literal">dfhack.maps.getRegionBiome(region_coord2d)</tt>, or <tt class="docutils literal">getRegionBiome(x,y)</tt></p>
<p>Returns the biome info struct for the given global map region.</p>
</li>
@@ -1221,7 +1270,7 @@ burrows, or the presence of invaders.</p>
</ul>
</div>
<div class="section" id="burrows-module">
-<h3><a class="toc-backref" href="#id23">Burrows module</a></h3>
+<h3><a class="toc-backref" href="#id24">Burrows module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.burrows.findByName(name)</tt></p>
<p>Returns the burrow pointer or <em>nil</em>.</p>
@@ -1256,7 +1305,7 @@ burrows, or the presence of invaders.</p>
</ul>
</div>
<div class="section" id="buildings-module">
-<h3><a class="toc-backref" href="#id24">Buildings module</a></h3>
+<h3><a class="toc-backref" href="#id25">Buildings module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.buildings.setOwner(item,unit)</tt></p>
<p>Replaces the owner of the building. If unit is <em>nil</em>, removes ownership.
@@ -1400,7 +1449,7 @@ can be determined this way, <tt class="docutils literal">constructBuilding</tt>
</ul>
</div>
<div class="section" id="constructions-module">
-<h3><a class="toc-backref" href="#id25">Constructions module</a></h3>
+<h3><a class="toc-backref" href="#id26">Constructions module</a></h3>
<ul>
<li><p class="first"><tt class="docutils literal">dfhack.constructions.designateNew(pos,type,item_type,mat_index)</tt></p>
<p>Designates a new construction at given position. If there already is
@@ -1416,7 +1465,7 @@ Returns <em>true, was_only_planned</em> if removed; or <em>false</em> if none fo
</ul>
</div>
<div class="section" id="screen-api">
-<h3><a class="toc-backref" href="#id26">Screen API</a></h3>
+<h3><a class="toc-backref" href="#id27">Screen API</a></h3>
<p>The screen module implements support for drawing to the tiled screen of the game.
Note that drawing only has any effect when done from callbacks, so it can only
be feasibly used in the core context.</p>
@@ -1460,6 +1509,10 @@ Otherwise should be <em>true/false</em>.</p>
</dl>
<p>Returns <em>false</em> if coordinates out of bounds, or other error.</p>
</li>
+<li><p class="first"><tt class="docutils literal">dfhack.screen.readTile(x,y)</tt></p>
+<p>Retrieves the contents of the specified tile from the screen buffers.
+Returns a pen, or <em>nil</em> if invalid or TrueType.</p>
+</li>
<li><p class="first"><tt class="docutils literal">dfhack.screen.paintString(pen,x,y,text)</tt></p>
<p>Paints the string starting at <em>x,y</em>. Uses the string characters
in sequence to override the <tt class="docutils literal">ch</tt> field of pen.</p>
@@ -1555,7 +1608,7 @@ options; if multiple interpretations exist, the table will contain multiple keys
</ul>
</div>
<div class="section" id="internal-api">
-<h3><a class="toc-backref" href="#id27">Internal API</a></h3>
+<h3><a class="toc-backref" href="#id28">Internal API</a></h3>
<p>These functions are intended for the use by dfhack developers,
and are only documented here for completeness:</p>
<ul>
@@ -1603,7 +1656,7 @@ Returns: <em>found_index</em>, or <em>nil</em> if end reached.</p>
</div>
</div>
<div class="section" id="core-interpreter-context">
-<h2><a class="toc-backref" href="#id28">Core interpreter context</a></h2>
+<h2><a class="toc-backref" href="#id29">Core interpreter context</a></h2>
<p>While plugins can create any number of interpreter instances,
there is one special context managed by dfhack core. It is the
only context that can receive events from DF and plugins.</p>
@@ -1634,7 +1687,7 @@ Using <tt class="docutils literal">timeout_active(id,nil)</tt> cancels the timer
</li>
</ul>
<div class="section" id="event-type">
-<h3><a class="toc-backref" href="#id29">Event type</a></h3>
+<h3><a class="toc-backref" href="#id30">Event type</a></h3>
<p>An event is a native object transparently wrapping a lua table,
and implementing a __call metamethod. When it is invoked, it loops
through the table with next and calls all contained values.
@@ -1666,7 +1719,7 @@ order using <tt class="docutils literal">dfhack.safecall</tt>.</p>
</div>
</div>
<div class="section" id="lua-modules">
-<h1><a class="toc-backref" href="#id30">Lua Modules</a></h1>
+<h1><a class="toc-backref" href="#id31">Lua Modules</a></h1>
<p>DFHack sets up the lua interpreter so that the built-in <tt class="docutils literal">require</tt>
function can be used to load shared lua code from hack/lua/.
The <tt class="docutils literal">dfhack</tt> namespace reference itself may be obtained via
@@ -1695,7 +1748,7 @@ in this document.</p>
</li>
</ul>
<div class="section" id="global-environment">
-<h2><a class="toc-backref" href="#id31">Global environment</a></h2>
+<h2><a class="toc-backref" href="#id32">Global environment</a></h2>
<p>A number of variables and functions are provided in the base global
environment by the mandatory init file dfhack.lua:</p>
<ul>
@@ -1736,7 +1789,7 @@ Returns <em>nil</em> if any of obj or indices is <em>nil</em>, or a numeric inde
</ul>
</div>
<div class="section" id="utils">
-<h2><a class="toc-backref" href="#id32">utils</a></h2>
+<h2><a class="toc-backref" href="#id33">utils</a></h2>
<ul>
<li><p class="first"><tt class="docutils literal">utils.compare(a,b)</tt></p>
<p>Comparator function; returns <em>-1</em> if a&lt;b, <em>1</em> if a&gt;b, <em>0</em> otherwise.</p>
@@ -1849,7 +1902,7 @@ throws an error.</p>
</ul>
</div>
<div class="section" id="dumper">
-<h2><a class="toc-backref" href="#id33">dumper</a></h2>
+<h2><a class="toc-backref" href="#id34">dumper</a></h2>
<p>A third-party lua table dumper module from
<a class="reference external" href="http://lua-users.org/wiki/DataDumper">http://lua-users.org/wiki/DataDumper</a>. Defines one
function:</p>
@@ -1863,14 +1916,14 @@ the other arguments see the original documentation link above.</p>
</div>
</div>
<div class="section" id="plugins">
-<h1><a class="toc-backref" href="#id34">Plugins</a></h1>
+<h1><a class="toc-backref" href="#id35">Plugins</a></h1>
<p>DFHack plugins may export native functions and events
to lua contexts. They are automatically imported by
<tt class="docutils literal"><span class="pre">mkmodule('plugins.&lt;name&gt;')</span></tt>; this means that a lua
module file is still necessary for <tt class="docutils literal">require</tt> to read.</p>
<p>The following plugins have lua support.</p>
<div class="section" id="burrows">
-<h2><a class="toc-backref" href="#id35">burrows</a></h2>
+<h2><a class="toc-backref" href="#id36">burrows</a></h2>
<p>Implements extended burrow manipulations.</p>
<p>Events:</p>
<ul>
@@ -1908,13 +1961,13 @@ set is the same as used by the command line.</p>
<p>The lua module file also re-exports functions from <tt class="docutils literal">dfhack.burrows</tt>.</p>
</div>
<div class="section" id="sort">
-<h2><a class="toc-backref" href="#id36">sort</a></h2>
+<h2><a class="toc-backref" href="#id37">sort</a></h2>
<p>Does not export any native functions as of now. Instead, it
calls lua code to perform the actual ordering of list items.</p>
</div>
</div>
<div class="section" id="scripts">
-<h1><a class="toc-backref" href="#id37">Scripts</a></h1>
+<h1><a class="toc-backref" href="#id38">Scripts</a></h1>
<p>Any files with the .lua extension placed into hack/scripts/*
are automatically used by the DFHack core as commands. The
matching command name consists of the name of the file sans
diff --git a/depends/protobuf/CMakeLists.txt b/depends/protobuf/CMakeLists.txt
index 24c4b275..5034f00f 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 39c0e61d..5af52709 100644
--- a/dfhack.init-example
+++ b/dfhack.init-example
@@ -16,6 +16,10 @@ keybinding add Ctrl-K autodump-destroy-item
# quicksave, only in main dwarfmode screen and menu page
keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
+# gui/rename script
+keybinding add Ctrl-Shift-N gui/rename
+keybinding add Ctrl-Shift-P "gui/rename unit-profession"
+
##############################
# Generic adv mode bindings #
##############################
@@ -52,6 +56,9 @@ keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list.work
# interface for the liquids plugin
keybinding add Alt-L@dwarfmode/LookAround gui/liquids
+# machine power sensitive pressure plate construction
+keybinding add Ctrl-Shift-M@dwarfmode/Build/Position/Trap gui/power-meter
+
###################
# UI logic tweaks #
###################
diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt
index 109a97e7..536f4d34 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 4e57b113..f69fa7a1 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 <lua.h>
#include <lauxlib.h>
@@ -728,6 +730,7 @@ static std::string getOSType()
}
static std::string getDFVersion() { return Core::getInstance().vinfo->getVersion(); }
+static uint32_t getTickCount() { return Core::getInstance().p->getTickCount(); }
static std::string getDFPath() { return Core::getInstance().p->getPath(); }
static std::string getHackPath() { return Core::getInstance().getHackPath(); }
@@ -739,6 +742,7 @@ static const LuaWrapper::FunctionReg dfhack_module[] = {
WRAP(getOSType),
WRAP(getDFVersion),
WRAP(getDFPath),
+ WRAP(getTickCount),
WRAP(getHackPath),
WRAP(isWorldLoaded),
WRAP(isMapLoaded),
@@ -811,12 +815,20 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getVisibleName),
WRAPM(Units, getIdentity),
WRAPM(Units, getNemesis),
+ WRAPM(Units, isCrazed),
+ WRAPM(Units, isOpposedToLife),
+ WRAPM(Units, hasExtravision),
+ WRAPM(Units, isBloodsucker),
+ WRAPM(Units, isMischievous),
+ WRAPM(Units, getMiscTrait),
WRAPM(Units, isDead),
WRAPM(Units, isAlive),
WRAPM(Units, isSane),
WRAPM(Units, isDwarf),
WRAPM(Units, isCitizen),
WRAPM(Units, getAge),
+ WRAPM(Units, getEffectiveSkill),
+ WRAPM(Units, computeMovementSpeed),
WRAPM(Units, getProfessionName),
WRAPM(Units, getCasteProfessionName),
WRAPM(Units, getProfessionColor),
@@ -874,6 +886,12 @@ static bool items_moveToInventory
return Items::moveToInventory(mc, item, unit, mode, body_part);
}
+static df::proj_itemst *items_makeProjectile(df::item *item)
+{
+ MapExtras::MapCache mc;
+ return Items::makeProjectile(mc, item);
+}
+
static const LuaWrapper::FunctionReg dfhack_items_module[] = {
WRAPM(Items, getGeneralRef),
WRAPM(Items, getSpecificRef),
@@ -885,6 +903,7 @@ static const LuaWrapper::FunctionReg dfhack_items_module[] = {
WRAPN(moveToContainer, items_moveToContainer),
WRAPN(moveToBuilding, items_moveToBuilding),
WRAPN(moveToInventory, items_moveToInventory),
+ WRAPN(makeProjectile, items_makeProjectile),
{ NULL, NULL }
};
@@ -933,6 +952,13 @@ static int maps_getTileBlock(lua_State *L)
return 1;
}
+static int maps_ensureTileBlock(lua_State *L)
+{
+ auto pos = CheckCoordXYZ(L, 1, true);
+ Lua::PushDFObject(L, Maps::ensureTileBlock(pos));
+ return 1;
+}
+
static int maps_getRegionBiome(lua_State *L)
{
auto pos = CheckCoordXY(L, 1, true);
@@ -949,6 +975,7 @@ static int maps_getTileBiomeRgn(lua_State *L)
static const luaL_Reg dfhack_maps_funcs[] = {
{ "isValidTilePos", maps_isValidTilePos },
{ "getTileBlock", maps_getTileBlock },
+ { "ensureTileBlock", maps_ensureTileBlock },
{ "getRegionBiome", maps_getRegionBiome },
{ "getTileBiomeRgn", maps_getTileBiomeRgn },
{ NULL, NULL }
@@ -1144,6 +1171,45 @@ static int screen_paintTile(lua_State *L)
return 1;
}
+static int screen_readTile(lua_State *L)
+{
+ int x = luaL_checkint(L, 1);
+ int y = luaL_checkint(L, 2);
+ Pen pen = Screen::readTile(x, y);
+
+ if (!pen.valid())
+ {
+ lua_pushnil(L);
+ }
+ else
+ {
+ lua_newtable(L);
+ lua_pushinteger(L, pen.ch); lua_setfield(L, -2, "ch");
+ lua_pushinteger(L, pen.fg); lua_setfield(L, -2, "fg");
+ lua_pushinteger(L, pen.bg); lua_setfield(L, -2, "bg");
+ lua_pushboolean(L, pen.bold); lua_setfield(L, -2, "bold");
+
+ if (pen.tile)
+ {
+ lua_pushinteger(L, pen.tile); lua_setfield(L, -2, "tile");
+
+ switch (pen.tile_mode) {
+ case Pen::CharColor:
+ lua_pushboolean(L, true); lua_setfield(L, -2, "tile_color");
+ break;
+ case Pen::TileColor:
+ lua_pushinteger(L, pen.tile_fg); lua_setfield(L, -2, "tile_fg");
+ lua_pushinteger(L, pen.tile_bg); lua_setfield(L, -2, "tile_bg");
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ return 1;
+}
+
static int screen_paintString(lua_State *L)
{
Pen pen;
@@ -1248,6 +1314,7 @@ static const luaL_Reg dfhack_screen_funcs[] = {
{ "getMousePos", screen_getMousePos },
{ "getWindowSize", screen_getWindowSize },
{ "paintTile", screen_paintTile },
+ { "readTile", screen_readTile },
{ "paintString", screen_paintString },
{ "fillRect", screen_fillRect },
{ "findGraphicsTile", screen_findGraphicsTile },
diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp
index 7c2c8f8d..a283d215 100644
--- a/library/LuaTools.cpp
+++ b/library/LuaTools.cpp
@@ -1211,6 +1211,39 @@ static int dfhack_open_plugin(lua_State *L)
return 0;
}
+static int dfhack_curry_wrap(lua_State *L)
+{
+ int nargs = lua_gettop(L);
+ int ncurry = lua_tointeger(L, lua_upvalueindex(1));
+ int scount = nargs + ncurry;
+
+ luaL_checkstack(L, ncurry, "stack overflow in curry");
+
+ // Insert values in O(N+M) by first shifting the existing data
+ lua_settop(L, scount);
+ for (int i = 0; i < nargs; i++)
+ lua_copy(L, nargs-i, scount-i);
+ for (int i = 1; i <= ncurry; i++)
+ lua_copy(L, lua_upvalueindex(i+1), i);
+
+ lua_callk(L, scount-1, LUA_MULTRET, 0, lua_gettop);
+
+ return lua_gettop(L);
+}
+
+static int dfhack_curry(lua_State *L)
+{
+ luaL_checkany(L, 1);
+ if (lua_isnil(L, 1))
+ luaL_argerror(L, 1, "nil function in curry");
+ if (lua_gettop(L) == 1)
+ return 1;
+ lua_pushinteger(L, lua_gettop(L));
+ lua_insert(L, 1);
+ lua_pushcclosure(L, dfhack_curry_wrap, lua_gettop(L));
+ return 1;
+}
+
bool Lua::IsCoreContext(lua_State *state)
{
// This uses a private field of the lua state to
@@ -1234,6 +1267,7 @@ static const luaL_Reg dfhack_funcs[] = {
{ "call_with_finalizer", dfhack_call_with_finalizer },
{ "with_suspend", lua_dfhack_with_suspend },
{ "open_plugin", dfhack_open_plugin },
+ { "curry", dfhack_curry },
{ NULL, NULL }
};
@@ -1546,6 +1580,9 @@ lua_State *DFHack::Lua::Open(color_ostream &out, lua_State *state)
lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_BASE_G_TOKEN);
lua_setfield(state, -2, "BASE_G");
+ lua_pushstring(state, DFHACK_VERSION);
+ lua_setfield(state, -2, "VERSION");
+
lua_pushboolean(state, IsCoreContext(state));
lua_setfield(state, -2, "is_core_context");
diff --git a/library/Process-darwin.cpp b/library/Process-darwin.cpp
index 5a97d9e0..3893cfc5 100644
--- a/library/Process-darwin.cpp
+++ b/library/Process-darwin.cpp
@@ -27,6 +27,7 @@ distribution.
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>
+#include <sys/time.h>
#include <mach-o/dyld.h>
@@ -262,6 +263,13 @@ bool Process::getThreadIDs(vector<uint32_t> & threads )
return true;
}
+uint32_t Process::getTickCount()
+{
+ struct timeval tp;
+ gettimeofday(&tp, NULL);
+ return (tp.tv_sec * 1000) + (tp.tv_usec / 1000);
+}
+
string Process::getPath()
{
char path[1024];
diff --git a/library/Process-linux.cpp b/library/Process-linux.cpp
index fe864784..4a66470f 100644
--- a/library/Process-linux.cpp
+++ b/library/Process-linux.cpp
@@ -27,6 +27,7 @@ distribution.
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>
+#include <sys/time.h>
#include <string>
#include <vector>
@@ -192,6 +193,13 @@ bool Process::getThreadIDs(vector<uint32_t> & threads )
return true;
}
+uint32_t Process::getTickCount()
+{
+ struct timeval tp;
+ gettimeofday(&tp, NULL);
+ return (tp.tv_sec * 1000) + (tp.tv_usec / 1000);
+}
+
string Process::getPath()
{
const char * cwd_name = "/proc/self/cwd";
diff --git a/library/Process-windows.cpp b/library/Process-windows.cpp
index 7eb6ff5f..db58c4d3 100644
--- a/library/Process-windows.cpp
+++ b/library/Process-windows.cpp
@@ -410,6 +410,11 @@ string Process::doReadClassName (void * vptr)
return raw;
}
+uint32_t Process::getTickCount()
+{
+ return GetTickCount();
+}
+
string Process::getPath()
{
HMODULE hmod;
diff --git a/library/RemoteClient.cpp b/library/RemoteClient.cpp
index 4d30988c..09861ad5 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 53428f2b..06a9f859 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 95c495e9..b371d60f 100644
--- a/library/RemoteTools.cpp
+++ b/library/RemoteTools.cpp
@@ -287,7 +287,7 @@ void DFHack::describeUnit(BasicUnitInfo *info, df::unit *unit,
if (mask && mask->profession())
{
- if (unit->profession >= 0)
+ if (unit->profession >= (df::profession)0)
info->set_profession(unit->profession);
if (!unit->custom_profession.empty())
info->set_custom_profession(unit->custom_profession);
diff --git a/library/include/DataDefs.h b/library/include/DataDefs.h
index 591a0c3f..61d5dec4 100644
--- a/library/include/DataDefs.h
+++ b/library/include/DataDefs.h
@@ -518,7 +518,7 @@ namespace DFHack {
template<class T>
inline const char *enum_item_raw_key(T val) {
typedef df::enum_traits<T> traits;
- return traits::is_valid(val) ? traits::key_table[val - traits::first_item_value] : NULL;
+ return traits::is_valid(val) ? traits::key_table[(short)val - traits::first_item_value] : NULL;
}
/**
diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h
index 6b1afb88..3330e23e 100644
--- a/library/include/LuaTools.h
+++ b/library/include/LuaTools.h
@@ -287,6 +287,11 @@ namespace DFHack {namespace Lua {
PushDFObject(state, ptr);
}
+ template<class T> 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<class T>
void PushVector(lua_State *state, const T &pvec, bool addn = false)
{
diff --git a/library/include/MemAccess.h b/library/include/MemAccess.h
index 0e5f618e..a226018a 100644
--- a/library/include/MemAccess.h
+++ b/library/include/MemAccess.h
@@ -281,6 +281,9 @@ namespace DFHack
/// get the DF Process FilePath
std::string getPath();
+ /// millisecond tick count, exactly as DF uses
+ uint32_t getTickCount();
+
/// modify permisions of memory range
bool setPermisions(const t_memrange & range,const t_memrange &trgrange);
diff --git a/library/include/RemoteTools.h b/library/include/RemoteTools.h
index 65884bad..e87e8026 100644
--- a/library/include/RemoteTools.h
+++ b/library/include/RemoteTools.h
@@ -88,7 +88,7 @@ namespace DFHack
{
typedef df::enum_traits<T> 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 4236f068..7493d22f 100644
--- a/library/include/modules/Items.h
+++ b/library/include/modules/Items.h
@@ -44,6 +44,7 @@ distribution.
namespace df
{
struct itemdef;
+ struct proj_itemst;
}
namespace MapExtras {
@@ -155,5 +156,8 @@ DFHACK_EXPORT bool moveToContainer(MapExtras::MapCache &mc, df::item *item, df::
DFHACK_EXPORT bool moveToBuilding(MapExtras::MapCache &mc, df::item *item, df::building_actual *building,int16_t use_mode);
DFHACK_EXPORT bool moveToInventory(MapExtras::MapCache &mc, df::item *item, df::unit *unit,
df::unit_inventory_item::T_mode mode = df::unit_inventory_item::Carried, int body_part = -1);
+
+/// Detaches the items from its current location and turns it into a projectile
+DFHACK_EXPORT df::proj_itemst *makeProjectile(MapExtras::MapCache &mc, df::item *item);
}
}
diff --git a/library/include/modules/MapCache.h b/library/include/modules/MapCache.h
index 109a20a4..262e70bb 100644
--- a/library/include/modules/MapCache.h
+++ b/library/include/modules/MapCache.h
@@ -253,6 +253,8 @@ public:
bool is_valid() { return valid; }
df::map_block *getRaw() { return block; }
+ bool Allocate();
+
MapCache *getParent() { return parent; }
private:
@@ -262,6 +264,8 @@ private:
MapCache *parent;
df::map_block *block;
+ void init();
+
int biomeIndexAt(df::coord2d p);
bool valid;
@@ -347,6 +351,12 @@ class DFHACK_EXPORT MapCache
return BlockAt(df::coord(coord.x>>4,coord.y>>4,coord.z));
}
+ bool ensureBlockAt(df::coord coord)
+ {
+ Block *b = BlockAtTile(coord);
+ return b ? b->Allocate() : false;
+ }
+
df::tiletype baseTiletypeAt (DFCoord tilecoord)
{
Block *b = BlockAtTile(tilecoord);
diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h
index 984cf16c..869b2158 100644
--- a/library/include/modules/Maps.h
+++ b/library/include/modules/Maps.h
@@ -241,9 +241,11 @@ inline bool isValidTilePos(df::coord pos) { return isValidTilePos(pos.x, pos.y,
*/
extern DFHACK_EXPORT df::map_block * getBlock (int32_t blockx, int32_t blocky, int32_t blockz);
extern DFHACK_EXPORT df::map_block * getTileBlock (int32_t x, int32_t y, int32_t z);
+extern DFHACK_EXPORT df::map_block * ensureTileBlock (int32_t x, int32_t y, int32_t z);
inline df::map_block * getBlock (df::coord pos) { return getBlock(pos.x, pos.y, pos.z); }
inline df::map_block * getTileBlock (df::coord pos) { return getTileBlock(pos.x, pos.y, pos.z); }
+inline df::map_block * ensureTileBlock (df::coord pos) { return ensureTileBlock(pos.x, pos.y, pos.z); }
extern DFHACK_EXPORT df::tiletype *getTileType(int32_t x, int32_t y, int32_t z);
extern DFHACK_EXPORT df::tile_designation *getTileDesignation(int32_t x, int32_t y, int32_t z);
diff --git a/library/include/modules/Screen.h b/library/include/modules/Screen.h
index 492e1eec..4f47205f 100644
--- a/library/include/modules/Screen.h
+++ b/library/include/modules/Screen.h
@@ -65,6 +65,9 @@ namespace DFHack
} tile_mode;
int8_t tile_fg, tile_bg;
+ bool valid() const { return tile >= 0; }
+ bool empty() const { return ch == 0 && tile == 0; }
+
Pen(char ch = 0, int8_t fg = 7, int8_t bg = 0, int tile = 0, bool color_tile = false)
: ch(ch), fg(fg&7), bg(bg), bold(!!(fg&8)),
tile(tile), tile_mode(color_tile ? CharColor : AsIs), tile_fg(0), tile_bg(0)
@@ -92,6 +95,9 @@ namespace DFHack
/// Paint one screen tile with the given pen
DFHACK_EXPORT bool paintTile(const Pen &pen, int x, int y);
+ /// Retrieves one screen tile from the buffer
+ DFHACK_EXPORT Pen readTile(int x, int y);
+
/// Paint a string onto the screen. Ignores ch and tile of pen.
DFHACK_EXPORT bool paintString(const Pen &pen, int x, int y, const std::string &text);
diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h
index 9003dc3a..65f0b58a 100644
--- a/library/include/modules/Units.h
+++ b/library/include/modules/Units.h
@@ -32,6 +32,10 @@ distribution.
#include "modules/Items.h"
#include "DataDefs.h"
#include "df/unit.h"
+#include "df/misc_trait_type.h"
+#include "df/physical_attribute_type.h"
+#include "df/mental_attribute_type.h"
+#include "df/job_skill.h"
namespace df
{
@@ -41,6 +45,7 @@ namespace df
struct historical_entity;
struct entity_position_assignment;
struct entity_position;
+ struct unit_misc_trait;
}
/**
@@ -208,6 +213,18 @@ DFHACK_EXPORT df::language_name *getVisibleName(df::unit *unit);
DFHACK_EXPORT df::assumed_identity *getIdentity(df::unit *unit);
DFHACK_EXPORT df::nemesis_record *getNemesis(df::unit *unit);
+DFHACK_EXPORT bool isHidingCurse(df::unit *unit);
+DFHACK_EXPORT int getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr);
+DFHACK_EXPORT int getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr);
+
+DFHACK_EXPORT bool isCrazed(df::unit *unit);
+DFHACK_EXPORT bool isOpposedToLife(df::unit *unit);
+DFHACK_EXPORT bool hasExtravision(df::unit *unit);
+DFHACK_EXPORT bool isBloodsucker(df::unit *unit);
+DFHACK_EXPORT bool isMischievous(df::unit *unit);
+
+DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create = false);
+
DFHACK_EXPORT bool isDead(df::unit *unit);
DFHACK_EXPORT bool isAlive(df::unit *unit);
DFHACK_EXPORT bool isSane(df::unit *unit);
@@ -216,6 +233,9 @@ DFHACK_EXPORT bool isDwarf(df::unit *unit);
DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false);
+DFHACK_EXPORT int getEffectiveSkill(df::unit *unit, df::job_skill skill_id);
+DFHACK_EXPORT int computeMovementSpeed(df::unit *unit);
+
struct NoblePosition {
df::historical_entity *entity;
df::entity_position_assignment *assignment;
diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua
index 2cbd019a..baf0d42e 100644
--- a/library/lua/dfhack.lua
+++ b/library/lua/dfhack.lua
@@ -46,6 +46,7 @@ end
-- Error handling
safecall = dfhack.safecall
+curry = dfhack.curry
function dfhack.pcall(f, ...)
return xpcall(f, dfhack.onerror, ...)
@@ -83,7 +84,7 @@ function mkmodule(module,env)
error("Not a table in package.loaded["..module.."]")
end
end
- local plugname = string.match(module,'^plugins%.(%w+)$')
+ local plugname = string.match(module,'^plugins%.([%w%-]+)$')
if plugname then
dfhack.open_plugin(pkg,plugname)
end
@@ -118,7 +119,12 @@ function defclass(class,parent)
if parent then
setmetatable(class, parent)
else
- rawset_default(class, { init_fields = rawset_default })
+ rawset_default(class, {
+ init_fields = rawset_default,
+ callback = function(self, name, ...)
+ return dfhack.curry(self[name], self, ...)
+ end
+ })
end
return class
end
@@ -163,6 +169,23 @@ function xyz2pos(x,y,z)
end
end
+function pos2xy(pos)
+ if pos then
+ local x = pos.x
+ if x and x ~= -30000 then
+ return x, pos.y
+ end
+ end
+end
+
+function xy2pos(x,y)
+ if x then
+ return {x=x,y=y}
+ else
+ return {x=-30000,y=-30000}
+ end
+end
+
function safe_index(obj,idx,...)
if obj == nil or idx == nil then
return nil
diff --git a/library/lua/gui.lua b/library/lua/gui.lua
index 9e189ea1..f9b6ab6d 100644
--- a/library/lua/gui.lua
+++ b/library/lua/gui.lua
@@ -94,6 +94,9 @@ function Painter:isValidPos()
end
function Painter:viewport(x,y,w,h)
+ if type(x) == 'table' then
+ x,y,w,h = x.x1, x.y1, x.width, x.height
+ end
local x1,y1 = self.x1+x, self.y1+y
local x2,y2 = x1+w-1, y1+h-1
local vp = {
@@ -159,10 +162,10 @@ function Painter:fill(x1,y1,x2,y2,pen,bg,bold)
if type(x1) == 'table' then
x1, y1, x2, y2, pen, bg, bold = x1.x1, x1.y1, x1.x2, x1.y2, y1, x2, y2
end
- x1 = math.max(x1,self.clip_x1)
- y1 = math.max(y1,self.clip_y1)
- x2 = math.min(x2,self.clip_x2)
- y2 = math.min(y2,self.clip_y2)
+ x1 = math.max(x1+self.x1,self.clip_x1)
+ y1 = math.max(y1+self.y1,self.clip_y1)
+ x2 = math.min(x2+self.x1,self.clip_x2)
+ y2 = math.min(y2+self.y1,self.clip_y2)
dscreen.fillRect(to_pen(self.cur_pen,pen,bg,bold),x1,y1,x2,y2)
return self
end
@@ -353,11 +356,16 @@ local function hint_coord(gap,hint)
end
end
+function FramedScreen:getWantedFrameSize()
+ return self.frame_width, self.frame_height
+end
+
function FramedScreen:updateFrameSize()
local sw, sh = dscreen.getWindowSize()
local iw, ih = sw-2, sh-2
- local width = math.min(self.frame_width or iw, iw)
- local height = math.min(self.frame_height or ih, ih)
+ local fw, fh = self:getWantedFrameSize()
+ local width = math.min(fw or iw, iw)
+ local height = math.min(fh or ih, ih)
local gw, gh = iw-width, ih-height
local x1, y1 = hint_coord(gw,self.frame_xhint), hint_coord(gh,self.frame_yhint)
self.frame_rect = mkdims_wh(x1+1,y1+1,width,height)
diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua
new file mode 100644
index 00000000..eb883465
--- /dev/null
+++ b/library/lua/gui/dialogs.lua
@@ -0,0 +1,271 @@
+-- Some simple dialog screens
+
+local _ENV = mkmodule('gui.dialogs')
+
+local gui = require('gui')
+local utils = require('utils')
+
+local dscreen = dfhack.screen
+
+MessageBox = defclass(MessageBox, gui.FramedScreen)
+
+MessageBox.focus_path = 'MessageBox'
+MessageBox.frame_style = gui.GREY_LINE_FRAME
+
+function MessageBox:init(info)
+ info = info or {}
+ self:init_fields{
+ text = info.text or {},
+ frame_title = info.title,
+ frame_width = info.frame_width,
+ on_accept = info.on_accept,
+ on_cancel = info.on_cancel,
+ on_close = info.on_close,
+ text_pen = info.text_pen
+ }
+ if type(self.text) == 'string' then
+ self.text = utils.split_string(self.text, "\n")
+ end
+ gui.FramedScreen.init(self, info)
+ return self
+end
+
+function MessageBox:getWantedFrameSize()
+ local text = self.text
+ local w = #(self.frame_title or '') + 4
+ w = math.max(w, 20)
+ w = math.max(self.frame_width or w, w)
+ for _, l in ipairs(text) do
+ w = math.max(w, #l)
+ end
+ local h = #text+1
+ if h > 1 then
+ h = h+1
+ end
+ return w+2, #text+2
+end
+
+function MessageBox:onRenderBody(dc)
+ if #self.text > 0 then
+ dc:newline(1):pen(self.text_pen or COLOR_GREY)
+ for _, l in ipairs(self.text or {}) do
+ dc:string(l):newline(1)
+ end
+ end
+
+ if self.on_accept then
+ local x,y = self.frame_rect.x1+1, self.frame_rect.y2+1
+ dscreen.paintString({fg=COLOR_LIGHTGREEN},x,y,'ESC')
+ dscreen.paintString({fg=COLOR_GREY},x+3,y,'/')
+ dscreen.paintString({fg=COLOR_LIGHTGREEN},x+4,y,'y')
+ end
+end
+
+function MessageBox:onDestroy()
+ if self.on_close then
+ self.on_close()
+ end
+end
+
+function MessageBox:onInput(keys)
+ if keys.MENU_CONFIRM then
+ self:dismiss()
+ if self.on_accept then
+ self.on_accept()
+ end
+ elseif keys.LEAVESCREEN or (keys.SELECT and not self.on_accept) then
+ self:dismiss()
+ if self.on_cancel then
+ self.on_cancel()
+ end
+ end
+end
+
+function showMessage(title, text, tcolor, on_close)
+ mkinstance(MessageBox):init{
+ text = text,
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ on_close = on_close
+ }:show()
+end
+
+function showYesNoPrompt(title, text, tcolor, on_accept, on_cancel)
+ mkinstance(MessageBox):init{
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ on_accept = on_accept,
+ on_cancel = on_cancel,
+ }:show()
+end
+
+InputBox = defclass(InputBox, MessageBox)
+
+InputBox.focus_path = 'InputBox'
+
+function InputBox:init(info)
+ info = info or {}
+ self:init_fields{
+ input = info.input or '',
+ input_pen = info.input_pen,
+ on_input = info.on_input,
+ }
+ MessageBox.init(self, info)
+ self.on_accept = nil
+ return self
+end
+
+function InputBox:getWantedFrameSize()
+ local mw, mh = MessageBox.getWantedFrameSize(self)
+ return mw, mh+2
+end
+
+function InputBox:onRenderBody(dc)
+ MessageBox.onRenderBody(self, dc)
+
+ dc:newline(1)
+ dc:pen(self.input_pen or COLOR_LIGHTCYAN)
+ dc:fill(1,dc:localY(),dc.width-2,dc:localY())
+
+ local cursor = '_'
+ if math.floor(dfhack.getTickCount()/300) % 2 == 0 then
+ cursor = ' '
+ end
+ local txt = self.input .. cursor
+ if #txt > dc.width-2 then
+ txt = string.char(27)..string.sub(txt, #txt-dc.width+4)
+ end
+ dc:string(txt)
+end
+
+function InputBox:onInput(keys)
+ if keys.SELECT then
+ self:dismiss()
+ if self.on_input then
+ self.on_input(self.input)
+ end
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+ if self.on_cancel then
+ self.on_cancel()
+ end
+ elseif keys._STRING then
+ if keys._STRING == 0 then
+ self.input = string.sub(self.input, 1, #self.input-1)
+ else
+ self.input = self.input .. string.char(keys._STRING)
+ end
+ end
+end
+
+function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width)
+ mkinstance(InputBox):init{
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ input = input,
+ on_input = on_input,
+ on_cancel = on_cancel,
+ frame_width = min_width,
+ }:show()
+end
+
+ListBox = defclass(ListBox, MessageBox)
+
+ListBox.focus_path = 'ListBox'
+
+function ListBox:init(info)
+ info = info or {}
+ self:init_fields{
+ selection = info.selection or 0,
+ choices = info.choices or {},
+ select_pen = info.select_pen,
+ on_input = info.on_input,
+ page_top = 0
+ }
+ MessageBox.init(self, info)
+ self.on_accept = nil
+ return self
+end
+
+function ListBox:getWantedFrameSize()
+ local mw, mh = MessageBox.getWantedFrameSize(self)
+ return mw, mh+#self.choices
+end
+
+function ListBox:onRenderBody(dc)
+ MessageBox.onRenderBody(self, dc)
+
+ dc:newline(1)
+
+ if self.selection>dc.height-3 then
+ self.page_top=self.selection-(dc.height-3)
+ elseif self.selection<self.page_top and self.selection >0 then
+ self.page_top=self.selection-1
+ end
+ for i,entry in ipairs(self.choices) do
+ if type(entry)=="table" then
+ entry=entry[1]
+ end
+ if i>self.page_top then
+ if i == self.selection then
+ dc:pen(self.select_pen or COLOR_LIGHTCYAN)
+ else
+ dc:pen(self.text_pen or COLOR_GREY)
+ end
+ dc:string(entry)
+ dc:newline(1)
+ end
+ end
+end
+function ListBox:moveCursor(delta)
+ local newsel=self.selection+delta
+ if #self.choices ~=0 then
+ if newsel<1 or newsel>#self.choices then
+ newsel=newsel % #self.choices
+ end
+ end
+ self.selection=newsel
+end
+function ListBox:onInput(keys)
+ if keys.SELECT then
+ self:dismiss()
+ local choice=self.choices[self.selection]
+ if self.on_input then
+ self.on_input(self.selection,choice)
+ end
+
+ if choice and choice[2] then
+ choice[2](choice,self.selection) -- maybe reverse the arguments?
+ end
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+ if self.on_cancel then
+ self.on_cancel()
+ end
+ elseif keys.CURSOR_UP then
+ self:moveCursor(-1)
+ elseif keys.CURSOR_DOWN then
+ self:moveCursor(1)
+ elseif keys.CURSOR_UP_FAST then
+ self:moveCursor(-10)
+ elseif keys.CURSOR_DOWN_FAST then
+ self:moveCursor(10)
+ end
+end
+
+function showListPrompt(title, text, tcolor, choices, on_input, on_cancel, min_width)
+ mkinstance(ListBox):init{
+ title = title,
+ text = text,
+ text_pen = tcolor,
+ choices = choices,
+ on_input = on_input,
+ on_cancel = on_cancel,
+ frame_width = min_width,
+ }:show()
+end
+
+return _ENV
diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua
index 1f7ae1b0..661e1559 100644
--- a/library/lua/gui/dwarfmode.lua
+++ b/library/lua/gui/dwarfmode.lua
@@ -46,7 +46,7 @@ function getPanelLayout()
end
function getCursorPos()
- if g_cursor ~= -30000 then
+ if g_cursor.x ~= -30000 then
return copyall(g_cursor)
end
end
@@ -136,6 +136,14 @@ function Viewport:set()
return vp
end
+function Viewport:getPos()
+ return xyz2pos(self.x1, self.y1, self.z)
+end
+
+function Viewport:getSize()
+ return xy2pos(self.width, self.height)
+end
+
function Viewport:clip(x,y,z)
return self:make(
math.max(0, math.min(x or self.x1, world_map.x_count-self.width)),
@@ -159,6 +167,18 @@ function Viewport:isVisible(target,gap)
return self:isVisibleXY(target,gap) and target.z == self.z
end
+function Viewport:tileToScreen(coord)
+ return xyz2pos(coord.x - self.x1, coord.y - self.y1, coord.z - self.z)
+end
+
+function Viewport:getCenter()
+ return xyz2pos(
+ math.floor((self.x2+self.x1)/2),
+ math.floor((self.y2+self.y1)/2),
+ self.z
+ )
+end
+
function Viewport:centerOn(target)
return self:clip(
target.x - math.floor(self.width/2),
@@ -207,16 +227,24 @@ MOVEMENT_KEYS = {
CURSOR_UP_Z_AUX = { 0, 0, 1 }, CURSOR_DOWN_Z_AUX = { 0, 0, -1 },
}
-function Viewport:scrollByKey(key)
+local function get_movement_delta(key, delta, big_step)
local info = MOVEMENT_KEYS[key]
if info then
- local delta = 10
- if info[4] then delta = 20 end
+ if info[4] then
+ delta = big_step
+ end
+
+ return delta*info[1], delta*info[2], info[3]
+ end
+end
+function Viewport:scrollByKey(key)
+ local dx, dy, dz = get_movement_delta(key, 10, 20)
+ if dx then
return self:clip(
- self.x1 + delta*info[1],
- self.y1 + delta*info[2],
- self.z + info[3]
+ self.x1 + dx,
+ self.y1 + dy,
+ self.z + dz
)
else
return self
@@ -237,16 +265,23 @@ function DwarfOverlay:getViewport(old_vp)
end
end
-function DwarfOverlay:moveCursorTo(cursor,viewport)
+function DwarfOverlay:moveCursorTo(cursor,viewport,gap)
setCursorPos(cursor)
- self:getViewport(viewport):reveal(cursor, 5, 0, 10):set()
+ self:zoomViewportTo(cursor,viewport,gap)
+end
+
+function DwarfOverlay:zoomViewportTo(target, viewport, gap)
+ if gap and self:getViewport():isVisible(target, gap) then
+ return
+ end
+ self:getViewport(viewport):reveal(target, 5, 0, 10):set()
end
-function DwarfOverlay:selectBuilding(building,cursor,viewport)
+function DwarfOverlay:selectBuilding(building,cursor,viewport,gap)
cursor = cursor or utils.getBuildingCenter(building)
df.global.world.selected_building = building
- self:moveCursorTo(cursor, viewport)
+ self:moveCursorTo(cursor, viewport, gap)
end
function DwarfOverlay:propagateMoveKeys(keys)
@@ -282,6 +317,31 @@ function DwarfOverlay:simulateViewScroll(keys, anchor, no_clip_cursor)
end
end
+function DwarfOverlay:simulateCursorMovement(keys, anchor)
+ local layout = self.df_layout
+ local cursor = getCursorPos()
+ local cx, cy, cz = pos2xyz(cursor)
+
+ if anchor and keys.A_MOVE_SAME_SQUARE then
+ setCursorPos(anchor)
+ self:getViewport():centerOn(anchor):set()
+ return 'A_MOVE_SAME_SQUARE'
+ end
+
+ for code,_ in pairs(MOVEMENT_KEYS) do
+ if keys[code] then
+ local dx, dy, dz = get_movement_delta(code, 1, 10)
+ local ncur = xyz2pos(cx+dx, cy+dy, cz+dz)
+
+ if dfhack.maps.isValidTilePos(ncur) then
+ setCursorPos(ncur)
+ self:getViewport():reveal(ncur,4,10,6,true):set()
+ return code
+ end
+ end
+ end
+end
+
function DwarfOverlay:onAboutToShow(below)
local screen = dfhack.gui.getCurViewscreen()
if below then screen = below.parent end
diff --git a/library/lua/utils.lua b/library/lua/utils.lua
index 19a4e6f6..9fa473ed 100644
--- a/library/lua/utils.lua
+++ b/library/lua/utils.lua
@@ -381,6 +381,19 @@ function getBuildingCenter(building)
return xyz2pos(building.centerx, building.centery, building.z)
end
+function split_string(self, delimiter)
+ local result = { }
+ local from = 1
+ local delim_from, delim_to = string.find( self, delimiter, from )
+ while delim_from do
+ table.insert( result, string.sub( self, from , delim_from-1 ) )
+ from = delim_to + 1
+ delim_from, delim_to = string.find( self, delimiter, from )
+ end
+ table.insert( result, string.sub( self, from ) )
+ return result
+end
+
-- Ask a yes-no question
function prompt_yes_no(msg,default)
local prompt = msg
diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp
index 1ea4bf68..91df14ea 100644
--- a/library/modules/Gui.cpp
+++ b/library/modules/Gui.cpp
@@ -173,10 +173,9 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
else if (id == &df::building_trapst::_identity)
{
auto trap = (df::building_trapst*)selected;
- if (trap->trap_type == trap_type::Lever) {
- focus += "/Lever";
+ focus += "/" + enum_item_key(trap->trap_type);
+ if (trap->trap_type == trap_type::Lever)
jobs = true;
- }
}
else if (ui_building_in_assign && *ui_building_in_assign &&
ui_building_assign_type && ui_building_assign_units &&
@@ -189,6 +188,8 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
focus += unit ? "/Unit" : "/None";
}
}
+ else
+ focus += "/" + enum_item_key(selected->getType());
if (jobs)
{
@@ -211,7 +212,14 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
if (ui_build_selector->building_type < 0)
focus += "/Type";
else if (ui_build_selector->stage != 2)
- focus += "/Position";
+ {
+ if (ui_build_selector->stage != 1)
+ focus += "/NoMaterials";
+ else
+ focus += "/Position";
+
+ focus += "/" + enum_item_key(ui_build_selector->building_type);
+ }
else
{
focus += "/Material";
diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp
index dc64143c..751797f0 100644
--- a/library/modules/Items.cpp
+++ b/library/modules/Items.cpp
@@ -72,8 +72,11 @@ using namespace std;
#include "df/general_ref_contains_itemst.h"
#include "df/general_ref_contained_in_itemst.h"
#include "df/general_ref_building_holderst.h"
+#include "df/general_ref_projectile.h"
#include "df/viewscreen_itemst.h"
#include "df/vermin.h"
+#include "df/proj_itemst.h"
+#include "df/proj_list_link.h"
#include "df/unit_inventory_item.h"
#include "df/body_part_raw.h"
@@ -88,6 +91,7 @@ using namespace df::enums;
using df::global::world;
using df::global::ui;
using df::global::ui_selected_unit;
+using df::global::proj_next_id;
#define ITEMDEF_VECTORS \
ITEM(WEAPON, weapons, itemdef_weaponst) \
@@ -866,3 +870,44 @@ bool DFHack::Items::moveToInventory(
return true;
}
+
+df::proj_itemst *Items::makeProjectile(MapExtras::MapCache &mc, df::item *item)
+{
+ CHECK_NULL_POINTER(item);
+
+ if (!world || !proj_next_id)
+ return NULL;
+
+ auto pos = getPosition(item);
+ if (!pos.isValid())
+ return NULL;
+
+ auto ref = df::allocate<df::general_ref_projectile>();
+ 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 54b4eb27..b74a4b73 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 305f1296..f1f40f19 100644
--- a/library/modules/Maps.cpp
+++ b/library/modules/Maps.cpp
@@ -157,6 +157,39 @@ df::map_block *Maps::getTileBlock (int32_t x, int32_t y, int32_t z)
return world->map.block_index[x >> 4][y >> 4][z];
}
+df::map_block *Maps::ensureTileBlock (int32_t x, int32_t y, int32_t z)
+{
+ if (!isValidTilePos(x,y,z))
+ return NULL;
+
+ auto column = world->map.block_index[x >> 4][y >> 4];
+ auto &slot = column[z];
+ if (slot)
+ return slot;
+
+ // Find another block below
+ int z2 = z;
+ while (z2 >= 0 && !column[z2]) z2--;
+ if (z2 < 0)
+ return NULL;
+
+ slot = new df::map_block();
+ slot->region_pos = column[z2]->region_pos;
+ slot->map_pos = column[z2]->map_pos;
+ slot->map_pos.z = z;
+
+ // Assume sky
+ df::tile_designation dsgn(0);
+ dsgn.bits.light = true;
+ dsgn.bits.outside = true;
+
+ for (int tx = 0; tx < 16; tx++)
+ for (int ty = 0; ty < 16; ty++)
+ slot->designation[tx][ty] = dsgn;
+
+ return slot;
+}
+
df::tiletype *Maps::getTileType(int32_t x, int32_t y, int32_t z)
{
df::map_block *block = getTileBlock(x,y,z);
@@ -274,7 +307,7 @@ df::feature_init *Maps::getLocalInitFeature(df::coord2d rgn_pos, int32_t index)
df::coord2d bigregion = rgn_pos / 16;
// bigregion is 16x16 regions. for each bigregion in X dimension:
- auto fptr = data->unk_204[bigregion.x][bigregion.y].features;
+ auto fptr = data->feature_map[bigregion.x][bigregion.y].features;
if (!fptr)
return NULL;
@@ -513,8 +546,14 @@ MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent)
valid = false;
bcoord = _bcoord;
block = Maps::getBlock(bcoord);
- item_counts = NULL;
tags = NULL;
+
+ init();
+}
+
+void MapExtras::Block::init()
+{
+ item_counts = NULL;
tiles = NULL;
basemats = NULL;
@@ -537,6 +576,23 @@ MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent)
}
}
+bool MapExtras::Block::Allocate()
+{
+ if (block)
+ return true;
+
+ block = Maps::ensureTileBlock(bcoord.x*16, bcoord.y*16, bcoord.z);
+ if (!block)
+ return false;
+
+ delete item_counts;
+ delete tiles;
+ delete basemats;
+ init();
+
+ return true;
+}
+
MapExtras::Block::~Block()
{
delete[] item_counts;
diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp
index c2377f2c..9f258fe0 100644
--- a/library/modules/Screen.cpp
+++ b/library/modules/Screen.cpp
@@ -100,7 +100,7 @@ static void doSetTile(const Pen &pen, int index)
bool Screen::paintTile(const Pen &pen, int x, int y)
{
- if (!gps) return false;
+ if (!gps || !pen.valid()) return false;
int dimx = gps->dimx, dimy = gps->dimy;
if (x < 0 || x >= dimx || y < 0 || y >= dimy) return false;
@@ -109,6 +109,41 @@ bool Screen::paintTile(const Pen &pen, int x, int y)
return true;
}
+Pen Screen::readTile(int x, int y)
+{
+ if (!gps) return Pen(0,0,0,-1);
+
+ int dimx = gps->dimx, dimy = gps->dimy;
+ if (x < 0 || x >= dimx || y < 0 || y >= dimy)
+ return Pen(0,0,0,-1);
+
+ int index = x*dimy + y;
+ auto screen = gps->screen + index*4;
+ if (screen[3] & 0x80)
+ return Pen(0,0,0,-1);
+
+ Pen pen(
+ screen[0], screen[1], screen[2], screen[3]?true:false,
+ gps->screentexpos[index]
+ );
+
+ if (pen.tile)
+ {
+ if (gps->screentexpos_grayscale[index])
+ {
+ pen.tile_mode = Screen::Pen::TileColor;
+ pen.tile_fg = gps->screentexpos_cf[index];
+ pen.tile_bg = gps->screentexpos_cbr[index];
+ }
+ else if (gps->screentexpos_addcolor[index])
+ {
+ pen.tile_mode = Screen::Pen::CharColor;
+ }
+ }
+
+ return pen;
+}
+
bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text)
{
if (!gps || y < 0 || y >= gps->dimy) return false;
@@ -132,7 +167,7 @@ bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text)
bool Screen::fillRect(const Pen &pen, int x1, int y1, int x2, int y2)
{
- if (!gps) return false;
+ if (!gps || !pen.valid()) return false;
if (x1 < 0) x1 = 0;
if (y1 < 0) y1 = 0;
diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp
index 874dabc3..01b7b50f 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<df::job_skill,int16_t> 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 393e7cbf..67b8c123 100644
--- a/library/modules/World.cpp
+++ b/library/modules/World.cpp
@@ -285,13 +285,13 @@ PersistentDataItem World::GetPersistentData(int entry_id)
PersistentDataItem World::GetPersistentData(const std::string &key, bool *added)
{
- *added = false;
+ if (added) *added = false;
PersistentDataItem rv = GetPersistentData(key);
if (!rv.isValid())
{
- *added = true;
+ if (added) *added = true;
rv = AddPersistentData(key);
}
@@ -300,6 +300,8 @@ PersistentDataItem World::GetPersistentData(const std::string &key, bool *added)
void World::GetPersistentData(std::vector<PersistentDataItem> *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 4300d63d..aa235780 100644
--- a/library/modules/kitchen.cpp
+++ b/library/modules/kitchen.cpp
@@ -114,7 +114,7 @@ void Kitchen::fillWatchMap(std::map<t_materialIndex, unsigned int>& 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
-Subproject df8178a989373ec7868d9195d82ae5f85145ef8
+Subproject 2bc8fbdf71143398817d31e06e169a01cce37c5
diff --git a/package/darwin/dfhack-run b/package/darwin/dfhack-run
index 865c8bd2..cc69db96 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 00000000..cff98b6a
--- /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 a2e52017..6e207385 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -44,6 +44,9 @@ endif()
install(DIRECTORY lua/
DESTINATION ${DFHACK_LUA_DESTINATION}/plugins
FILES_MATCHING PATTERN "*.lua")
+install(DIRECTORY raw/
+ DESTINATION ${DFHACK_DATA_DESTINATION}/raw
+ FILES_MATCHING PATTERN "*.txt")
# Protobuf
FILE(GLOB PROJECT_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto)
@@ -92,7 +95,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(seedwatch seedwatch.cpp)
DFHACK_PLUGIN(initflags initflags.cpp)
DFHACK_PLUGIN(stockpiles stockpiles.cpp)
- DFHACK_PLUGIN(rename rename.cpp PROTOBUFS rename)
+ DFHACK_PLUGIN(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename)
DFHACK_PLUGIN(jobutils jobutils.cpp)
DFHACK_PLUGIN(workflow workflow.cpp)
DFHACK_PLUGIN(showmood showmood.cpp)
@@ -114,6 +117,8 @@ if (BUILD_SUPPORTED)
# this one exports functions to lua
DFHACK_PLUGIN(burrows burrows.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(sort sort.cpp LINK_LIBRARIES lua)
+ DFHACK_PLUGIN(steam-engine steam-engine.cpp)
+ DFHACK_PLUGIN(power-meter power-meter.cpp LINK_LIBRARIES lua)
# not yet. busy with other crud again...
#DFHACK_PLUGIN(versionosd versionosd.cpp)
endif()
diff --git a/plugins/autolabor.cpp b/plugins/autolabor.cpp
index c3a2b313..c39b126c 100644
--- a/plugins/autolabor.cpp
+++ b/plugins/autolabor.cpp
@@ -9,6 +9,7 @@
#include <vector>
#include <algorithm>
+#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<struct labor_info> 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 <std::string> & 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 <std::string> & 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 <std::string> & 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 <std::string> & 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 <std::string> & 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 <std::string> & 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 <string> & 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<df::building_stockpilest>(build);
StockpileInfo *spi = new StockpileInfo(sp);
@@ -1580,7 +1551,7 @@ static int stockcheck(color_ostream &out, vector <string> & parameters)
}
- std::vector<df::item*> &items = world->items.other[df::enums::items_other_id::ANY_FREE];
+ std::vector<df::item*> &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 <string> & 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 <string> & 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 de204f61..c0301de7 100644
--- a/plugins/cleaners.cpp
+++ b/plugins/cleaners.cpp
@@ -50,12 +50,12 @@ command_result cleanmap (color_ostream &out, bool snow, bool mud)
// filter snow
if(!snow
&& spatter->mat_type == builtin_mats::WATER
- && spatter->mat_state == matter_state::Powder)
+ && spatter->mat_state == (short)matter_state::Powder)
continue;
// filter mud
if(!mud
&& spatter->mat_type == builtin_mats::MUD
- && spatter->mat_state == matter_state::Solid)
+ && spatter->mat_state == (short)matter_state::Solid)
continue;
delete evt;
diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp
index c1521b8d..cd01fd61 100644
--- a/plugins/cleanowned.cpp
+++ b/plugins/cleanowned.cpp
@@ -116,7 +116,7 @@ command_result df_cleanowned (color_ostream &out, vector <string> & parameters)
}
else if (item->flags.bits.on_ground)
{
- int32_t type = item->getType();
+ df::item_type type = item->getType();
if(type == item_type::MEAT ||
type == item_type::FISH ||
type == item_type::VERMIN ||
diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt
index f126ae53..39e8f7b6 100644
--- a/plugins/devel/CMakeLists.txt
+++ b/plugins/devel/CMakeLists.txt
@@ -18,7 +18,7 @@ DFHACK_PLUGIN(stripcaged stripcaged.cpp)
DFHACK_PLUGIN(rprobe rprobe.cpp)
DFHACK_PLUGIN(nestboxes nestboxes.cpp)
DFHACK_PLUGIN(vshook vshook.cpp)
-DFHACK_PLUGIN(steam-engine steam-engine.cpp)
+DFHACK_PLUGIN(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
IF(UNIX)
DFHACK_PLUGIN(ref-index ref-index.cpp)
ENDIF()
diff --git a/plugins/devel/siege-engine.cpp b/plugins/devel/siege-engine.cpp
new file mode 100644
index 00000000..a41bfe5f
--- /dev/null
+++ b/plugins/devel/siege-engine.cpp
@@ -0,0 +1,1649 @@
+#include "Core.h"
+#include <Console.h>
+#include <Export.h>
+#include <Error.h>
+#include <PluginManager.h>
+#include <modules/Gui.h>
+#include <modules/Screen.h>
+#include <modules/Maps.h>
+#include <modules/MapCache.h>
+#include <modules/World.h>
+#include <modules/Units.h>
+#include <modules/Job.h>
+#include <LuaTools.h>
+#include <TileTypes.h>
+#include <vector>
+#include <cstdio>
+#include <stack>
+#include <string>
+#include <cmath>
+#include <string.h>
+
+#include <VTableInterpose.h>
+#include "df/graphic.h"
+#include "df/building_siegeenginest.h"
+#include "df/builtin_mats.h"
+#include "df/world.h"
+#include "df/buildings_other_id.h"
+#include "df/job.h"
+#include "df/building_drawbuffer.h"
+#include "df/ui.h"
+#include "df/viewscreen_dwarfmodest.h"
+#include "df/ui_build_selector.h"
+#include "df/flow_info.h"
+#include "df/report.h"
+#include "df/proj_itemst.h"
+#include "df/unit.h"
+#include "df/unit_soul.h"
+#include "df/unit_skill.h"
+#include "df/physical_attribute_type.h"
+#include "df/creature_raw.h"
+#include "df/caste_raw.h"
+#include "df/caste_raw_flags.h"
+#include "df/assumed_identity.h"
+#include "df/game_mode.h"
+#include "df/unit_misc_trait.h"
+#include "df/job.h"
+#include "df/job_item.h"
+#include "df/item.h"
+#include "df/items_other_id.h"
+#include "df/building_stockpilest.h"
+#include "df/stockpile_links.h"
+#include "df/workshop_profile.h"
+
+#include "MiscUtils.h"
+
+using std::vector;
+using std::string;
+using std::stack;
+using namespace DFHack;
+using namespace df::enums;
+
+using df::global::gamemode;
+using df::global::gps;
+using df::global::world;
+using df::global::ui;
+using df::global::ui_build_selector;
+
+using Screen::Pen;
+
+DFHACK_PLUGIN("siege-engine");
+
+/*
+ * Misc. utils
+ */
+
+typedef std::pair<df::coord, df::coord> 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<int, int> get_engine_range(df::building_siegeenginest *bld)
+{
+ if (bld->type == siegeengine_type::Ballista)
+ return std::make_pair(0, 200);
+ else
+ return std::make_pair(30, 100);
+}
+
+static void orient_engine(df::building_siegeenginest *bld, df::coord target)
+{
+ int dx = target.x - bld->centerx;
+ int dy = target.y - bld->centery;
+
+ if (abs(dx) > abs(dy))
+ bld->facing = (dx > 0) ?
+ df::building_siegeenginest::Right :
+ df::building_siegeenginest::Left;
+ else
+ bld->facing = (dy > 0) ?
+ df::building_siegeenginest::Down :
+ df::building_siegeenginest::Up;
+}
+
+static int random_int(int val)
+{
+ return int(int64_t(rand())*val/RAND_MAX);
+}
+
+static int point_distance(df::coord speed)
+{
+ return std::max(abs(speed.x), std::max(abs(speed.y), abs(speed.z)));
+}
+
+inline void normalize(float &x, float &y, float &z)
+{
+ float dist = sqrtf(x*x + y*y + z*z);
+ if (dist == 0.0f) return;
+ x /= dist; y /= dist; z /= dist;
+}
+
+static void random_direction(float &x, float &y, float &z)
+{
+ float a, b, d;
+ for (;;) {
+ a = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f;
+ b = (rand() + 0.5f)*2.0f/RAND_MAX - 1.0f;
+ d = a*a + b*b;
+ if (d < 1.0f)
+ break;
+ }
+
+ float sq = sqrtf(1-d);
+ x = 2.0f*a*sq;
+ y = 2.0f*b*sq;
+ z = 1.0f - 2.0f*d;
+}
+
+/*
+ * Configuration object
+ */
+
+static bool enable_plugin();
+
+struct EngineInfo {
+ int id;
+ df::building_siegeenginest *bld;
+
+ df::coord center;
+ coord_range building_rect;
+
+ bool is_catapult;
+ int proj_speed, hit_delay;
+ std::pair<int, int> 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<int> 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<df::building*, EngineInfo*> engines;
+static std::map<df::coord, df::building*> coord_engines;
+
+static EngineInfo *find_engine(df::building *bld, bool create = false)
+{
+ auto ebld = strict_virtual_cast<df::building_siegeenginest>(bld);
+ if (!ebld)
+ return NULL;
+
+ auto &obj = engines[bld];
+
+ if (obj)
+ {
+ obj->bld = ebld;
+ return obj;
+ }
+
+ if (!create)
+ return NULL;
+
+ obj = new EngineInfo();
+
+ obj->id = bld->id;
+ obj->bld = ebld;
+ obj->center = df::coord(bld->centerx, bld->centery, bld->z);
+ obj->building_rect = coord_range(
+ df::coord(bld->x1, bld->y1, bld->z),
+ df::coord(bld->x2, bld->y2, bld->z)
+ );
+ obj->is_catapult = (ebld->type == siegeengine_type::Catapult);
+ obj->proj_speed = 2;
+ obj->hit_delay = 3;
+ obj->fire_range = get_engine_range(ebld);
+
+ obj->ammo_vector_id = job_item_vector_id::BOULDER;
+ obj->ammo_item_type = item_type::BOULDER;
+
+ obj->operator_id = obj->operator_frame = -1;
+
+ coord_engines[obj->center] = bld;
+ return obj;
+}
+
+static EngineInfo *find_engine(lua_State *L, int idx, bool create = false, bool silent = false)
+{
+ auto bld = Lua::CheckDFObject<df::building_siegeenginest>(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<df::building_siegeenginest>(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<PersistentDataItem> 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<PersistentDataItem> 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<float, df::coord> path;
+
+ struct Hit {
+ UnitPath *path;
+ df::coord pos;
+ int dist;
+ float time, lmargin, rmargin;
+ };
+
+ static std::map<df::unit*, UnitPath*> cache;
+
+ static UnitPath *get(df::unit *unit)
+ {
+ auto &cv = cache[unit];
+ if (!cv) cv = new UnitPath(unit);
+ return cv;
+ };
+
+ UnitPath(df::unit *unit) : unit(unit)
+ {
+ if (unit->flags1.bits.rider)
+ {
+ auto mount = df::unit::find(unit->relations.rider_mount_id);
+
+ if (mount)
+ {
+ path = get(mount)->path;
+ return;
+ }
+ }
+
+ df::coord pos = unit->pos;
+ df::coord dest = unit->path.dest;
+ auto &upath = unit->path.path;
+
+ if (dest.isValid() && !upath.x.empty())
+ {
+ float time = unit->counters.job_counter+0.5f;
+ float speed = Units::computeMovementSpeed(unit)/100.0f;
+
+ for (size_t i = 0; i < upath.size(); i++)
+ {
+ df::coord new_pos = upath[i];
+ if (new_pos == pos)
+ continue;
+
+ float delay = speed;
+ if (new_pos.x != pos.x && new_pos.y != pos.y)
+ delay *= 362.0/256.0;
+
+ path[time] = pos;
+ pos = new_pos;
+ time += delay + 1;
+ }
+ }
+
+ path[MAX_TIME] = pos;
+ }
+
+ void get_margin(std::map<float,df::coord>::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> *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<df::unit*, UnitPath*> 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<df::unit>(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<df::unit>(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<UnitPath::Hit> *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<UnitPath::Hit> hits;
+ proposeUnitHits(engine, &hits, bias);
+
+ lua_createtable(L, hits.size(), 0);
+
+ for (size_t i = 0; i < hits.size(); i++)
+ {
+ auto &hit = hits[i];
+ lua_createtable(L, 0, 6);
+ Lua::SetField(L, hit.path->unit, -1, "unit");
+ Lua::SetField(L, hit.pos, -1, "pos");
+ Lua::SetField(L, hit.dist, -1, "dist");
+ Lua::SetField(L, hit.time, -1, "time");
+ push_margin(L, hit.lmargin); lua_setfield(L, -2, "lmargin");
+ push_margin(L, hit.rmargin); lua_setfield(L, -2, "rmargin");
+ lua_rawseti(L, -2, i+1);
+ }
+
+ return 1;
+}
+
+/*
+ * Projectile hook
+ */
+
+struct projectile_hook : df::proj_itemst {
+ typedef df::proj_itemst interpose_base;
+
+ void aimAtPoint(EngineInfo *engine, const ProjectilePath &path)
+ {
+ target_pos = path.target;
+
+ PathMetrics raytrace(path);
+
+ // Materialize map blocks, or the projectile will crash into them
+ for (int i = 0; i < raytrace.collision_step; i++)
+ Maps::ensureTileBlock(path[i]);
+
+ // Find valid hit point for catapult stones
+ if (flags.bits.high_flying)
+ {
+ if (raytrace.hits())
+ fall_threshold = raytrace.goal_step;
+ else
+ fall_threshold = (raytrace.collision_z_step+raytrace.collision_step-1)/2;
+
+ while (fall_threshold < raytrace.collision_step-1)
+ {
+ if (isTargetableTile(path[fall_threshold]))
+ break;
+
+ fall_threshold++;
+ }
+ }
+
+ fall_threshold = std::max(fall_threshold, engine->fire_range.first);
+ fall_threshold = std::min(fall_threshold, engine->fire_range.second);
+ }
+
+ void aimAtArea(EngineInfo *engine)
+ {
+ df::coord target, last_passable;
+ df::coord tbase = engine->target.first;
+ df::coord tsize = engine->getTargetSize();
+ bool success = false;
+
+ for (int i = 0; i < 50; i++)
+ {
+ target = tbase + df::coord(
+ random_int(tsize.x), random_int(tsize.y), random_int(tsize.z)
+ );
+
+ if (adjustToTarget(engine, &target))
+ last_passable = target;
+ else
+ continue;
+
+ ProjectilePath path(engine->center, target, engine->is_catapult ? 0.5f : 0.0f);
+ PathMetrics raytrace(path);
+
+ if (raytrace.hits() && engine->isInRange(raytrace.goal_step))
+ {
+ aimAtPoint(engine, path);
+ return;
+ }
+ }
+
+ if (!last_passable.isValid())
+ last_passable = target;
+
+ aimAtPoint(engine, ProjectilePath(engine->center, last_passable));
+ }
+
+ static int safeAimProjectile(lua_State *L)
+ {
+ color_ostream &out = *Lua::GetOutput(L);
+ auto proj = (projectile_hook*)lua_touserdata(L, 1);
+ auto engine = (EngineInfo*)lua_touserdata(L, 2);
+ int skill = lua_tointeger(L, 3);
+
+ if (!Lua::PushModulePublic(out, L, "plugins.siege-engine", "doAimProjectile"))
+ luaL_error(L, "Projectile aiming AI not available");
+
+ Lua::PushDFObject(L, engine->bld);
+ Lua::Push(L, proj->item);
+ Lua::Push(L, engine->target.first);
+ Lua::Push(L, engine->target.second);
+ Lua::Push(L, skill);
+
+ lua_call(L, 5, 1);
+
+ if (lua_isnil(L, -1))
+ proj->aimAtArea(engine);
+ else
+ proj->aimAtPoint(engine, decode_path(L, -1, engine->center));
+
+ return 0;
+ }
+
+ void doCheckMovement()
+ {
+ if (flags.bits.parabolic || distance_flown != 0 ||
+ fall_counter != fall_delay || item == NULL)
+ return;
+
+ auto engine = find_engine(origin_pos);
+ if (!engine || !engine->hasTarget())
+ return;
+
+ auto L = Lua::Core::State;
+ CoreSuspendClaimer suspend;
+ color_ostream_proxy out(Core::getInstance().getConsole());
+
+ int skill = getOperatorSkill(engine->bld, true);
+
+ lua_pushcfunction(L, safeAimProjectile);
+ lua_pushlightuserdata(L, this);
+ lua_pushlightuserdata(L, engine);
+ lua_pushinteger(L, skill);
+
+ if (!Lua::Core::SafeCall(out, 3, 0))
+ aimAtArea(engine);
+
+ switch (item->getType())
+ {
+ case item_type::CAGE:
+ flags.bits.bouncing = false;
+ break;
+ case item_type::BIN:
+ case item_type::BARREL:
+ flags.bits.bouncing = false;
+ break;
+ default:
+ break;
+ }
+ }
+
+ void doLaunchContents()
+ {
+ // Translate cartoon flight speed to parabolic
+ float speed = 100000.0f / (fall_delay + 1);
+ int min_zspeed = (fall_delay+1)*4900;
+
+ // Flight direction vector
+ df::coord dist = target_pos - origin_pos;
+ float vx = dist.x, vy = dist.y, vz = fabs(dist.z);
+ normalize(vx, vy, vz);
+
+ int start_z = 0;
+
+ // Start at tile top, if hit a wall
+ ProjectilePath path(origin_pos, target_pos);
+ auto next_pos = path[distance_flown+1];
+ if (next_pos.z == cur_pos.z && !isPassableTile(next_pos))
+ start_z = 49000;
+
+ MapExtras::MapCache mc;
+ std::vector<df::item*> contents;
+ Items::getContainedItems(item, &contents);
+
+ for (size_t i = 0; i < contents.size(); i++)
+ {
+ auto child = contents[i];
+ auto proj = Items::makeProjectile(mc, child);
+ if (!proj) continue;
+
+ proj->flags.bits.no_impact_destroy = true;
+ //proj->flags.bits.bouncing = true;
+ proj->flags.bits.piercing = true;
+ proj->flags.bits.parabolic = true;
+ proj->flags.bits.unk9 = true;
+ proj->flags.bits.no_collide = true;
+
+ proj->pos_z = start_z;
+
+ float sx, sy, sz;
+ random_direction(sx, sy, sz);
+ sx += vx*0.7; sy += vy*0.7; sz += vz*0.7;
+ if (sz < 0) sz = -sz;
+ normalize(sx, sy, sz);
+
+ proj->speed_x = int(speed * sx);
+ proj->speed_y = int(speed * sy);
+ proj->speed_z = int(speed * sz);
+ }
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, checkMovement, ())
+ {
+ if (flags.bits.high_flying || flags.bits.piercing)
+ doCheckMovement();
+
+ return INTERPOSE_NEXT(checkMovement)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(bool, checkImpact, (bool no_damage_floor))
+ {
+ if (!flags.bits.has_hit_ground && !flags.bits.parabolic &&
+ flags.bits.high_flying && !flags.bits.bouncing &&
+ !flags.bits.no_impact_destroy && target_pos != origin_pos &&
+ item && item->flags.bits.container)
+ {
+ doLaunchContents();
+ }
+
+ return INTERPOSE_NEXT(checkImpact)(no_damage_floor);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(projectile_hook, checkMovement);
+IMPLEMENT_VMETHOD_INTERPOSE(projectile_hook, checkImpact);
+
+/*
+ * Building hook
+ */
+
+struct building_hook : df::building_siegeenginest {
+ typedef df::building_siegeenginest interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(df::workshop_profile*, getWorkshopProfile, ())
+ {
+ if (auto engine = find_engine(this))
+ return &engine->profile;
+
+ return INTERPOSE_NEXT(getWorkshopProfile)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(df::stockpile_links*, getStockpileLinks, ())
+ {
+ if (auto engine = find_engine(this))
+ {
+ update_stockpile_links(engine);
+ return &engine->links;
+ }
+
+ return INTERPOSE_NEXT(getStockpileLinks)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, updateAction, ())
+ {
+ INTERPOSE_NEXT(updateAction)();
+
+ if (jobs.empty())
+ return;
+
+ if (auto engine = find_engine(this))
+ {
+ auto job = jobs[0];
+ bool save_op = false;
+
+ switch (job->job_type)
+ {
+ case job_type::LoadCatapult:
+ if (!job->job_items.empty())
+ {
+ auto item = job->job_items[0];
+ item->item_type = engine->ammo_item_type;
+ item->vector_id = engine->ammo_vector_id;
+
+ switch (item->item_type)
+ {
+ case item_type::NONE:
+ case item_type::BOULDER:
+ case item_type::BLOCKS:
+ item->mat_type = 0;
+ break;
+
+ case item_type::BIN:
+ case item_type::BARREL:
+ item->mat_type = -1;
+ // A hack to make it take objects assigned to stockpiles.
+ // Since reaction_id is not set, the actual value is not used.
+ item->contains.resize(1);
+ break;
+
+ default:
+ item->mat_type = -1;
+ break;
+ }
+ }
+ // fallthrough
+
+ case job_type::LoadBallista:
+ case job_type::FireCatapult:
+ case job_type::FireBallista:
+ if (auto worker = Job::getWorker(job))
+ {
+ engine->operator_id = worker->id;
+ engine->operator_frame = world->frame_counter;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(building_hook, getWorkshopProfile);
+IMPLEMENT_VMETHOD_INTERPOSE(building_hook, getStockpileLinks);
+IMPLEMENT_VMETHOD_INTERPOSE(building_hook, updateAction);
+
+/*
+ * Initialization
+ */
+
+DFHACK_PLUGIN_LUA_FUNCTIONS {
+ DFHACK_LUA_FUNCTION(clearTargetArea),
+ DFHACK_LUA_FUNCTION(setTargetArea),
+ DFHACK_LUA_FUNCTION(isLinkedToPile),
+ DFHACK_LUA_FUNCTION(addStockpileLink),
+ DFHACK_LUA_FUNCTION(removeStockpileLink),
+ DFHACK_LUA_FUNCTION(saveWorkshopProfile),
+ DFHACK_LUA_FUNCTION(getTileStatus),
+ DFHACK_LUA_FUNCTION(paintAimScreen),
+ DFHACK_LUA_FUNCTION(canTargetUnit),
+ DFHACK_LUA_FUNCTION(isPassableTile),
+ DFHACK_LUA_FUNCTION(isTreeTile),
+ DFHACK_LUA_FUNCTION(isTargetableTile),
+ DFHACK_LUA_END
+};
+
+DFHACK_PLUGIN_LUA_COMMANDS {
+ DFHACK_LUA_COMMAND(getTargetArea),
+ DFHACK_LUA_COMMAND(getAmmoItem),
+ DFHACK_LUA_COMMAND(setAmmoItem),
+ DFHACK_LUA_COMMAND(getStockpileLinks),
+ DFHACK_LUA_COMMAND(projPosAtStep),
+ DFHACK_LUA_COMMAND(projPathMetrics),
+ DFHACK_LUA_COMMAND(adjustToTarget),
+ DFHACK_LUA_COMMAND(traceUnitPath),
+ DFHACK_LUA_COMMAND(unitPosAtTime),
+ DFHACK_LUA_COMMAND(proposeUnitHits),
+ DFHACK_LUA_END
+};
+
+static bool is_enabled = false;
+
+static void enable_hooks(bool enable)
+{
+ is_enabled = enable;
+
+ INTERPOSE_HOOK(projectile_hook, checkMovement).apply(enable);
+ INTERPOSE_HOOK(projectile_hook, checkImpact).apply(enable);
+
+ INTERPOSE_HOOK(building_hook, getWorkshopProfile).apply(enable);
+ INTERPOSE_HOOK(building_hook, getStockpileLinks).apply(enable);
+ INTERPOSE_HOOK(building_hook, updateAction).apply(enable);
+
+ if (enable)
+ load_engines();
+ else
+ clear_engines();
+}
+
+static bool enable_plugin()
+{
+ if (is_enabled)
+ return true;
+
+ auto pworld = Core::getInstance().getWorld();
+ auto entry = pworld->GetPersistentData("siege-engine/enabled", NULL);
+ if (!entry.isValid())
+ return false;
+
+ enable_hooks(true);
+ return true;
+}
+
+static void clear_caches(color_ostream &out)
+{
+ if (!UnitPath::cache.empty())
+ {
+ for (auto it = UnitPath::cache.begin(); it != UnitPath::cache.end(); ++it)
+ delete it->second;
+
+ UnitPath::cache.clear();
+ }
+}
+
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
+{
+ switch (event) {
+ case SC_MAP_LOADED:
+ {
+ auto pworld = Core::getInstance().getWorld();
+ bool enable = pworld->GetPersistentData("siege-engine/enabled").isValid();
+
+ if (enable)
+ {
+ out.print("Enabling the siege engine plugin.\n");
+ enable_hooks(true);
+ }
+ else
+ enable_hooks(false);
+ }
+ break;
+ case SC_MAP_UNLOADED:
+ enable_hooks(false);
+ break;
+ default:
+ break;
+ }
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
+{
+ if (Core::getInstance().isMapLoaded())
+ plugin_onstatechange(out, SC_MAP_LOADED);
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown ( color_ostream &out )
+{
+ enable_hooks(false);
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_onupdate ( color_ostream &out )
+{
+ clear_caches(out);
+ return CR_OK;
+}
diff --git a/plugins/jobutils.cpp b/plugins/jobutils.cpp
index 24ad4170..dbfe26b9 100644
--- a/plugins/jobutils.cpp
+++ b/plugins/jobutils.cpp
@@ -372,7 +372,7 @@ static command_result job_cmd(color_ostream &out, vector <string> & parameters)
out << "Job item updated." << endl;
- if (item->item_type < 0 && minfo.isValid())
+ if (item->item_type < (df::item_type)0 && minfo.isValid())
out.printerr("WARNING: Due to a probable bug, creature & plant material subtype\n"
" is ignored unless the item type is also specified.\n");
diff --git a/plugins/lua/power-meter.lua b/plugins/lua/power-meter.lua
new file mode 100644
index 00000000..310e51c4
--- /dev/null
+++ b/plugins/lua/power-meter.lua
@@ -0,0 +1,11 @@
+local _ENV = mkmodule('plugins.power-meter')
+
+--[[
+
+ Native functions:
+
+ * makePowerMeter(plate_info,min_power,max_power,invert)
+
+--]]
+
+return _ENV \ No newline at end of file
diff --git a/plugins/lua/rename.lua b/plugins/lua/rename.lua
new file mode 100644
index 00000000..0e7128f5
--- /dev/null
+++ b/plugins/lua/rename.lua
@@ -0,0 +1,13 @@
+local _ENV = mkmodule('plugins.rename')
+
+--[[
+
+ Native functions:
+
+ * canRenameBuilding(building)
+ * isRenamingBuilding(building)
+ * renameBuilding(building, name)
+
+--]]
+
+return _ENV \ No newline at end of file
diff --git a/plugins/lua/siege-engine.lua b/plugins/lua/siege-engine.lua
new file mode 100644
index 00000000..89c47659
--- /dev/null
+++ b/plugins/lua/siege-engine.lua
@@ -0,0 +1,45 @@
+local _ENV = mkmodule('plugins.siege-engine')
+
+--[[
+
+ Native functions:
+
+ * getTargetArea(building) -> point1, point2
+ * clearTargetArea(building)
+ * setTargetArea(building, point1, point2) -> true/false
+
+--]]
+
+Z_STEP_COUNT = 15
+Z_STEP = 1/31
+
+function findShotHeight(engine, target)
+ local path = { target = target, delta = 0.0 }
+
+ if projPathMetrics(engine, path).goal_step then
+ return path
+ end
+
+ for i = 1,Z_STEP_COUNT do
+ path.delta = i*Z_STEP
+ if projPathMetrics(engine, path).goal_step then
+ return path
+ end
+
+ path.delta = -i*Z_STEP
+ if projPathMetrics(engine, path).goal_step then
+ return path
+ end
+ end
+end
+
+function doAimProjectile(engine, item, target_min, target_max, skill)
+ print(item, df.skill_rating[skill])
+ local targets = proposeUnitHits(engine)
+ if #targets > 0 then
+ local rnd = math.random(#targets)
+ return findShotHeight(engine, targets[rnd].pos)
+ end
+end
+
+return _ENV \ No newline at end of file
diff --git a/plugins/manipulator.cpp b/plugins/manipulator.cpp
index 1a90d2ee..f4096965 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<df::interface_key> *events);
@@ -328,10 +345,11 @@ protected:
vector<UnitInfo *> 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<df::unit*> &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<df::interface_key> *events)
@@ -453,8 +489,8 @@ void viewscreen_unitlaborsst::feed(set<df::interface_key> *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<df::interface_key> *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<df::interface_key> *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<df::interface_key> *events)
altsort = ALTSORT_PROFESSION;
break;
case ALTSORT_PROFESSION:
+ altsort = ALTSORT_HAPPINESS;
+ break;
+ case ALTSORT_HAPPINESS:
altsort = ALTSORT_NAME;
break;
}
@@ -603,9 +645,9 @@ void viewscreen_unitlaborsst::render()
dfhack_viewscreen::render();
Screen::clear();
- Screen::drawBorder(" Manage Labors ");
+ Screen::drawBorder(" Dwarf Manipulator - Manage Labors ");
- for (int col = 0; col < labors_width; col++)
+ for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++)
{
int col_offset = col + first_column;
if (col_offset >= NUM_COLUMNS)
@@ -620,21 +662,21 @@ void viewscreen_unitlaborsst::render()
bg = 7;
}
- Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 1);
- Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 2);
+ Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 1);
+ Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 2);
df::profession profession = columns[col_offset].profession;
- if (profession != profession::NONE)
+ if ((profession != profession::NONE) && (ui->race_id != -1))
{
auto graphics = world->raws.creatures.all[ui->race_id]->graphics;
Screen::paintTile(
Screen::Pen(' ', fg, 0,
graphics.profession_add_color[creature_graphics_role::DEFAULT][profession],
graphics.profession_texpos[creature_graphics_role::DEFAULT][profession]),
- 1 + name_width + 1 + prof_width + 1 + col, 3);
+ col_offsets[DISP_COLUMN_LABORS] + col, 3);
}
}
- for (int row = 0; row < height; row++)
+ for (int row = 0; row < num_rows; row++)
{
int row_offset = row + first_row;
if (row_offset >= units.size())
@@ -643,6 +685,26 @@ void viewscreen_unitlaborsst::render()
UnitInfo *cur = units[row_offset];
df::unit *unit = cur->unit;
int8_t fg = 15, bg = 0;
+
+ int happy = cur->unit->status.happiness;
+ string happiness = stl_sprintf("%4i", happy);
+ if (happy == 0) // miserable
+ fg = 13; // 5:1
+ else if (happy <= 25) // very unhappy
+ fg = 12; // 4:1
+ else if (happy <= 50) // unhappy
+ fg = 4; // 4:0
+ else if (happy < 75) // fine
+ fg = 14; // 6:1
+ else if (happy < 125) // quite content
+ fg = 6; // 6:0
+ else if (happy < 150) // happy
+ fg = 2; // 2:0
+ else // ecstatic
+ fg = 10; // 2:1
+ Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_HAPPINESS], 4 + row, happiness);
+
+ fg = 15;
if (row_offset == sel_row)
{
fg = 0;
@@ -650,18 +712,18 @@ void viewscreen_unitlaborsst::render()
}
string name = cur->name;
- name.resize(name_width);
- Screen::paintString(Screen::Pen(' ', fg, bg), 1, 4 + row, name);
+ name.resize(col_widths[DISP_COLUMN_NAME]);
+ Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_NAME], 4 + row, name);
string profession = cur->profession;
- profession.resize(prof_width);
+ profession.resize(col_widths[DISP_COLUMN_PROFESSION]);
fg = cur->color;
bg = 0;
- Screen::paintString(Screen::Pen(' ', fg, bg), 1 + name_width + 1, 4 + row, profession);
+ Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_PROFESSION], 4 + row, profession);
// Print unit's skills and labor assignments
- for (int col = 0; col < labors_width; col++)
+ for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++)
{
int col_offset = col + first_column;
fg = 15;
@@ -693,7 +755,7 @@ void viewscreen_unitlaborsst::render()
}
else
bg = 4;
- Screen::paintTile(Screen::Pen(c, fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 4 + row);
+ Screen::paintTile(Screen::Pen(c, fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 4 + row);
}
}
@@ -703,17 +765,17 @@ void viewscreen_unitlaborsst::render()
{
df::unit *unit = cur->unit;
int x = 1;
- Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->transname);
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->transname);
x += cur->transname.length();
if (cur->transname.length())
{
- Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ", ");
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ", ");
x += 2;
}
- Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->profession);
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->profession);
x += cur->profession.length();
- Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ": ");
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ": ");
x += 2;
string str;
@@ -740,7 +802,7 @@ void viewscreen_unitlaborsst::render()
else
str = stl_sprintf("Not %s (0/500)", ENUM_ATTR_STR(job_skill, caption_noun, columns[sel_column].skill));
}
- Screen::paintString(Screen::Pen(' ', 9, 0), x, 3 + height + 2, str);
+ Screen::paintString(Screen::Pen(' ', 9, 0), x, 3 + num_rows + 2, str);
canToggle = (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE);
}
@@ -779,6 +841,9 @@ void viewscreen_unitlaborsst::render()
case ALTSORT_PROFESSION:
OutputString(15, x, gps->dimy - 2, "Profession");
break;
+ case ALTSORT_HAPPINESS:
+ OutputString(15, x, gps->dimy - 2, "Happiness");
+ break;
default:
OutputString(15, x, gps->dimy - 2, "Unknown");
break;
@@ -810,7 +875,7 @@ struct unitlist_hook : df::viewscreen_unitlistst
{
int x = 2;
OutputString(12, x, gps->dimy - 2, "l"); // UNITVIEW_PRF_PROF key
- OutputString(15, x, gps->dimy - 2, ": Manage labors");
+ OutputString(15, x, gps->dimy - 2, ": Manage labors (DFHack)");
}
}
};
diff --git a/plugins/power-meter.cpp b/plugins/power-meter.cpp
new file mode 100644
index 00000000..0466b68e
--- /dev/null
+++ b/plugins/power-meter.cpp
@@ -0,0 +1,237 @@
+#include "Core.h"
+#include <Console.h>
+#include <Export.h>
+#include <Error.h>
+#include <PluginManager.h>
+#include <modules/Gui.h>
+#include <modules/Screen.h>
+#include <modules/Maps.h>
+#include <modules/World.h>
+#include <TileTypes.h>
+#include <vector>
+#include <cstdio>
+#include <stack>
+#include <string>
+#include <cmath>
+#include <string.h>
+
+#include <VTableInterpose.h>
+#include "df/graphic.h"
+#include "df/building_trapst.h"
+#include "df/builtin_mats.h"
+#include "df/world.h"
+#include "df/buildings_other_id.h"
+#include "df/machine.h"
+#include "df/machine_info.h"
+#include "df/building_drawbuffer.h"
+#include "df/ui.h"
+#include "df/viewscreen_dwarfmodest.h"
+#include "df/ui_build_selector.h"
+#include "df/flow_info.h"
+#include "df/report.h"
+
+#include "MiscUtils.h"
+
+using std::vector;
+using std::string;
+using std::stack;
+using namespace DFHack;
+using namespace df::enums;
+
+using df::global::gps;
+using df::global::world;
+using df::global::ui;
+using df::global::ui_build_selector;
+
+DFHACK_PLUGIN("power-meter");
+
+static const uint32_t METER_BIT = 0x80000000U;
+
+static void init_plate_info(df::pressure_plate_info &plate_info)
+{
+ plate_info.water_min = 1;
+ plate_info.water_max = 7;
+ plate_info.flags.whole = METER_BIT;
+ plate_info.flags.bits.water = true;
+ plate_info.flags.bits.resets = true;
+}
+
+/*
+ * Hook for the pressure plate itself. Implements core logic.
+ */
+
+struct trap_hook : df::building_trapst {
+ typedef df::building_trapst interpose_base;
+
+ // Engine detection
+
+ bool is_power_meter()
+ {
+ return trap_type == trap_type::PressurePlate &&
+ (plate_info.flags.whole & METER_BIT) != 0;
+ }
+
+ inline bool is_fully_built()
+ {
+ return getBuildStage() >= getMaxBuildStage();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, getName, (std::string *buf))
+ {
+ if (is_power_meter())
+ {
+ buf->clear();
+ *buf += "Power Meter";
+ return;
+ }
+
+ INTERPOSE_NEXT(getName)(buf);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, updateAction, ())
+ {
+ if (is_power_meter())
+ {
+ auto pdsgn = Maps::getTileDesignation(centerx,centery,z);
+
+ if (pdsgn)
+ {
+ bool active = false;
+ auto &gears = world->buildings.other[buildings_other_id::GEAR_ASSEMBLY];
+
+ for (size_t i = 0; i < gears.size(); i++)
+ {
+ // Adjacent
+ auto gear = gears[i];
+ int deltaxy = abs(centerx - gear->centerx) + abs(centery - gear->centery);
+ if (gear->z != z || deltaxy != 1)
+ continue;
+ // Linked to machine
+ auto info = gears[i]->getMachineInfo();
+ if (!info || info->machine_id < 0)
+ continue;
+ // an active machine
+ auto machine = df::machine::find(info->machine_id);
+ if (!machine || !machine->flags.bits.active)
+ continue;
+ // with adequate power?
+ int power = machine->cur_power - machine->min_power;
+ if (power < 0 || machine->cur_power <= 0)
+ continue;
+ if (power < plate_info.track_min)
+ continue;
+ if (power > plate_info.track_max && plate_info.track_max >= 0)
+ continue;
+
+ active = true;
+ break;
+ }
+
+ if (plate_info.flags.bits.citizens)
+ active = !active;
+
+ // Temporarily set the tile water amount based on power state
+ auto old_dsgn = *pdsgn;
+ pdsgn->bits.liquid_type = tile_liquid::Water;
+ pdsgn->bits.flow_size = (active ? 7 : 0);
+
+ INTERPOSE_NEXT(updateAction)();
+
+ *pdsgn = old_dsgn;
+ return;
+ }
+ }
+
+ INTERPOSE_NEXT(updateAction)();
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, drawBuilding, (df::building_drawbuffer *db, void *unk))
+ {
+ INTERPOSE_NEXT(drawBuilding)(db, unk);
+
+ if (is_power_meter() && is_fully_built())
+ {
+ db->fore[0][0] = 3;
+ }
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(trap_hook, getName);
+IMPLEMENT_VMETHOD_INTERPOSE(trap_hook, updateAction);
+IMPLEMENT_VMETHOD_INTERPOSE(trap_hook, drawBuilding);
+
+static bool enabled = false;
+
+static void enable_hooks(bool enable)
+{
+ enabled = enable;
+
+ INTERPOSE_HOOK(trap_hook, getName).apply(enable);
+ INTERPOSE_HOOK(trap_hook, updateAction).apply(enable);
+ INTERPOSE_HOOK(trap_hook, drawBuilding).apply(enable);
+}
+
+static bool makePowerMeter(df::pressure_plate_info *info, int min_power, int max_power, bool invert)
+{
+ CHECK_NULL_POINTER(info);
+
+ if (!enabled)
+ {
+ auto pworld = Core::getInstance().getWorld();
+ auto entry = pworld->GetPersistentData("power-meter/enabled", NULL);
+ if (!entry.isValid())
+ return false;
+
+ enable_hooks(true);
+ }
+
+ init_plate_info(*info);
+ info->track_min = min_power;
+ info->track_max = max_power;
+ info->flags.bits.citizens = invert;
+ return true;
+}
+
+DFHACK_PLUGIN_LUA_FUNCTIONS {
+ DFHACK_LUA_FUNCTION(makePowerMeter),
+ DFHACK_LUA_END
+};
+
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
+{
+ switch (event) {
+ case SC_MAP_LOADED:
+ {
+ auto pworld = Core::getInstance().getWorld();
+ bool enable = pworld->GetPersistentData("power-meter/enabled").isValid();
+
+ if (enable)
+ {
+ out.print("Enabling the power meter plugin.\n");
+ enable_hooks(true);
+ }
+ }
+ break;
+ case SC_MAP_UNLOADED:
+ enable_hooks(false);
+ break;
+ default:
+ break;
+ }
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
+{
+ if (Core::getInstance().isMapLoaded())
+ plugin_onstatechange(out, SC_MAP_LOADED);
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown ( color_ostream &out )
+{
+ enable_hooks(false);
+ return CR_OK;
+}
diff --git a/plugins/proto/rename.proto b/plugins/proto/rename.proto
index aa1e95f4..810091fc 100644
--- a/plugins/proto/rename.proto
+++ b/plugins/proto/rename.proto
@@ -17,3 +17,10 @@ message RenameUnitIn {
optional string nickname = 2;
optional string profession = 3;
}
+
+// RPC RenameBuilding : RenameBuildingIn -> EmptyMessage
+message RenameBuildingIn {
+ required int32 building_id = 1;
+
+ optional string name = 2;
+}
diff --git a/plugins/devel/building_steam_engine.txt b/plugins/raw/building_steam_engine.txt
index 48657b0c..48657b0c 100644
--- a/plugins/devel/building_steam_engine.txt
+++ b/plugins/raw/building_steam_engine.txt
diff --git a/plugins/devel/item_trapcomp_steam_engine.txt b/plugins/raw/item_trapcomp_steam_engine.txt
index bae6f5b2..c35f6ef4 100644
--- a/plugins/devel/item_trapcomp_steam_engine.txt
+++ b/plugins/raw/item_trapcomp_steam_engine.txt
@@ -4,8 +4,8 @@ item_trapcomp_steam_engine
[ITEM_TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON]
[NAME:piston:pistons]
-[ADJECTIVE:huge]
-[SIZE:1600]
+[ADJECTIVE:heavy]
+[SIZE:1800]
[HITS:1]
[MATERIAL_SIZE:6]
[METAL]
diff --git a/plugins/devel/reaction_steam_engine.txt b/plugins/raw/reaction_steam_engine.txt
index 175ffdd5..175ffdd5 100644
--- a/plugins/devel/reaction_steam_engine.txt
+++ b/plugins/raw/reaction_steam_engine.txt
diff --git a/plugins/rename.cpp b/plugins/rename.cpp
index 1871d0f7..99dc6848 100644
--- a/plugins/rename.cpp
+++ b/plugins/rename.cpp
@@ -3,11 +3,15 @@
#include "Export.h"
#include "PluginManager.h"
+#include <Error.h>
+#include <LuaTools.h>
+
#include "modules/Gui.h"
#include "modules/Translation.h"
#include "modules/Units.h"
+#include "modules/World.h"
-#include "DataDefs.h"
+#include <VTableInterpose.h>
#include "df/ui.h"
#include "df/world.h"
#include "df/squad.h"
@@ -18,6 +22,11 @@
#include "df/historical_figure_info.h"
#include "df/assumed_identity.h"
#include "df/language_name.h"
+#include "df/building_stockpilest.h"
+#include "df/building_workshopst.h"
+#include "df/building_furnacest.h"
+#include "df/building_trapst.h"
+#include "df/building_siegeenginest.h"
#include "RemoteServer.h"
#include "rename.pb.h"
@@ -36,6 +45,8 @@ using namespace dfproto;
using df::global::ui;
using df::global::world;
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event);
+
static command_result rename(color_ostream &out, vector <string> & parameters);
DFHACK_PLUGIN("rename");
@@ -51,8 +62,32 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector <Plugi
" rename unit \"nickname\"\n"
" rename unit-profession \"custom profession\"\n"
" (a unit must be highlighted in the ui)\n"
+ " rename building \"nickname\"\n"
+ " (a building must be highlighted via 'q')\n"
));
+
+ if (Core::getInstance().isMapLoaded())
+ plugin_onstatechange(out, SC_MAP_LOADED);
}
+
+ return CR_OK;
+}
+
+static void init_buildings(bool enable);
+
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
+{
+ switch (event) {
+ case SC_MAP_LOADED:
+ init_buildings(true);
+ break;
+ case SC_MAP_UNLOADED:
+ init_buildings(false);
+ break;
+ default:
+ break;
+ }
+
return CR_OK;
}
@@ -61,6 +96,133 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
return CR_OK;
}
+/*
+ * Building renaming - it needs some per-type hacking.
+ */
+
+#define KNOWN_BUILDINGS \
+ BUILDING('p', building_stockpilest, "Stockpile") \
+ BUILDING('w', building_workshopst, NULL) \
+ BUILDING('e', building_furnacest, NULL) \
+ BUILDING('T', building_trapst, NULL) \
+ BUILDING('i', building_siegeenginest, NULL)
+
+#define BUILDING(code, cname, tag) \
+ struct cname##_hook : df::cname { \
+ typedef df::cname interpose_base; \
+ DEFINE_VMETHOD_INTERPOSE(void, getName, (std::string *buf)) { \
+ if (!name.empty()) {\
+ buf->clear(); \
+ *buf += name; \
+ *buf += " ("; \
+ if (tag) *buf += (const char*)tag; \
+ else { std::string tmp; INTERPOSE_NEXT(getName)(&tmp); *buf += tmp; } \
+ *buf += ")"; \
+ return; \
+ } \
+ else \
+ INTERPOSE_NEXT(getName)(buf); \
+ } \
+ }; \
+ IMPLEMENT_VMETHOD_INTERPOSE(cname##_hook, getName);
+KNOWN_BUILDINGS
+#undef BUILDING
+
+static char getBuildingCode(df::building *bld)
+{
+ CHECK_NULL_POINTER(bld);
+
+#define BUILDING(code, cname, tag) \
+ if (strict_virtual_cast<df::cname>(bld)) return code;
+KNOWN_BUILDINGS
+#undef BUILDING
+
+ return 0;
+}
+
+static bool enable_building_rename(char code, bool enable)
+{
+ switch (code) {
+#define BUILDING(code, cname, tag) \
+ case code: return INTERPOSE_HOOK(cname##_hook, getName).apply(enable);
+KNOWN_BUILDINGS
+#undef BUILDING
+ default:
+ return false;
+ }
+}
+
+static void disable_building_rename()
+{
+#define BUILDING(code, cname, tag) \
+ INTERPOSE_HOOK(cname##_hook, getName).remove();
+KNOWN_BUILDINGS
+#undef BUILDING
+}
+
+static bool is_enabled_building(char code)
+{
+ switch (code) {
+#define BUILDING(code, cname, tag) \
+ case code: return INTERPOSE_HOOK(cname##_hook, getName).is_applied();
+KNOWN_BUILDINGS
+#undef BUILDING
+ default:
+ return false;
+ }
+}
+
+static void init_buildings(bool enable)
+{
+ disable_building_rename();
+
+ if (enable)
+ {
+ auto pworld = Core::getInstance().getWorld();
+ auto entry = pworld->GetPersistentData("rename/building_types");
+
+ if (entry.isValid())
+ {
+ std::string val = entry.val();
+ for (size_t i = 0; i < val.size(); i++)
+ enable_building_rename(val[i], true);
+ }
+ }
+}
+
+static bool canRenameBuilding(df::building *bld)
+{
+ return getBuildingCode(bld) != 0;
+}
+
+static bool isRenamingBuilding(df::building *bld)
+{
+ return is_enabled_building(getBuildingCode(bld));
+}
+
+static bool renameBuilding(df::building *bld, std::string name)
+{
+ char code = getBuildingCode(bld);
+ if (code == 0 && !name.empty())
+ return false;
+
+ if (!name.empty() && !is_enabled_building(code))
+ {
+ auto pworld = Core::getInstance().getWorld();
+ auto entry = pworld->GetPersistentData("rename/building_types", NULL);
+ if (!entry.isValid())
+ return false;
+
+ if (!enable_building_rename(code, true))
+ return false;
+
+ entry.val().push_back(code);
+ }
+
+ bld->name = name;
+ return true;
+}
+
static df::squad *getSquadByIndex(unsigned idx)
{
auto entity = df::historical_entity::find(ui->group_id);
@@ -101,14 +263,37 @@ static command_result RenameUnit(color_ostream &stream, const RenameUnitIn *in)
return CR_OK;
}
+static command_result RenameBuilding(color_ostream &stream, const RenameBuildingIn *in)
+{
+ auto building = df::building::find(in->building_id());
+ if (!building)
+ return CR_NOT_FOUND;
+
+ if (in->has_name())
+ {
+ if (!renameBuilding(building, in->name()))
+ return CR_FAILURE;
+ }
+
+ return CR_OK;
+}
+
DFhackCExport RPCService *plugin_rpcconnect(color_ostream &)
{
RPCService *svc = new RPCService();
svc->addFunction("RenameSquad", RenameSquad);
svc->addFunction("RenameUnit", RenameUnit);
+ svc->addFunction("RenameBuilding", RenameBuilding);
return svc;
}
+DFHACK_PLUGIN_LUA_FUNCTIONS {
+ DFHACK_LUA_FUNCTION(canRenameBuilding),
+ DFHACK_LUA_FUNCTION(isRenamingBuilding),
+ DFHACK_LUA_FUNCTION(renameBuilding),
+ DFHACK_LUA_END
+};
+
static command_result rename(color_ostream &out, vector <string> &parameters)
{
CoreSuspender suspend;
@@ -167,6 +352,20 @@ static command_result rename(color_ostream &out, vector <string> &parameters)
unit->custom_profession = parameters[1];
}
+ else if (cmd == "building")
+ {
+ if (parameters.size() != 2)
+ return CR_WRONG_USAGE;
+
+ if (ui->main.mode != ui_sidebar_mode::QueryBuilding)
+ return CR_WRONG_USAGE;
+
+ if (!renameBuilding(world->selected_building, parameters[1]))
+ {
+ out.printerr("This type of building is not supported.\n");
+ return CR_FAILURE;
+ }
+ }
else
{
if (!parameters.empty() && cmd != "?")
diff --git a/plugins/devel/steam-engine.cpp b/plugins/steam-engine.cpp
index 5c344f78..cacfc6e1 100644
--- a/plugins/devel/steam-engine.cpp
+++ b/plugins/steam-engine.cpp
@@ -34,6 +34,82 @@
#include "MiscUtils.h"
+/*
+ * This plugin implements a steam engine workshop. It activates
+ * if there are any workshops in the raws with STEAM_ENGINE in
+ * their token, and provides the necessary behavior.
+ *
+ * Construction:
+ *
+ * The workshop needs water as its input, which it takes via a
+ * passable floor tile below it, like usual magma workshops do.
+ * The magma version also needs magma.
+ *
+ * ISSUE: Since this building is a machine, and machine collapse
+ * code cannot be modified, it would collapse over true open space.
+ * As a loophole, down stair provides support to machines, while
+ * being passable, so use them.
+ *
+ * After constructing the building itself, machines can be connected
+ * to the edge tiles that look like gear boxes. Their exact position
+ * is extracted from the workshop raws.
+ *
+ * ISSUE: Like with collapse above, part of the code involved in
+ * machine connection cannot be modified. As a result, the workshop
+ * can only immediately connect to machine components built AFTER it.
+ * This also means that engines cannot be chained without intermediate
+ * short axles that can be built later.
+ *
+ * Operation:
+ *
+ * In order to operate the engine, queue the Stoke Boiler job.
+ * A furnace operator will come, possibly bringing a bar of fuel,
+ * and perform it. As a result, a "boiling water" item will appear
+ * in the 't' view of the workshop.
+ *
+ * Note: The completion of the job will actually consume one unit
+ * of appropriate liquids from below the workshop.
+ *
+ * Every such item gives 100 power, up to a limit of 300 for coal,
+ * and 500 for a magma engine. The building can host twice that
+ * amount of items to provide longer autonomous running. When the
+ * boiler gets filled to capacity, all queued jobs are suspended;
+ * once it drops back to 3+1 or 5+1 items, they are re-enabled.
+ *
+ * While the engine is providing power, steam is being consumed.
+ * The consumption speed includes a fixed 10% waste rate, and
+ * the remaining 90% are applied proportionally to the actual
+ * load in the machine. With the engine at nominal 300 power with
+ * 150 load in the system, it will consume steam for actual
+ * 300*(10% + 90%*150/300) = 165 power.
+ *
+ * Masterpiece mechanism and chain will decrease the mechanical
+ * power drawn by the engine itself from 10 to 5. Masterpiece
+ * barrel decreases waste rate by 4%. Masterpiece piston and pipe
+ * decrease it by further 4%, and also decrease the whole steam
+ * use rate by 10%.
+ *
+ * Explosions:
+ *
+ * The engine must be constructed using barrel, pipe and piston
+ * from fire-safe, or in the magma version magma-safe metals.
+ *
+ * During operation weak parts get gradually worn out, and
+ * eventually the engine explodes. It should also explode if
+ * toppled during operation by a building destroyer, or a
+ * tantruming dwarf.
+ *
+ * Save files:
+ *
+ * It should be safe to load and view fortresses using engines
+ * from a DF version without DFHack installed, except that in such
+ * case the engines won't work. However actually making modifications
+ * to them, or machines they connect to (including by pulling levers),
+ * can easily result in inconsistent state once this plugin is
+ * available again. The effects may be as weird as negative power
+ * being generated.
+ */
+
using std::vector;
using std::string;
using std::stack;
@@ -465,9 +541,11 @@ struct workshop_hook : df::building_workshopst {
power_rate = 0.0f;
}
// waste rate: 1-10% depending on piston assembly quality
- float waste = 0.1f - 0.015f * get_component_quality(1);
+ float piston_qual = get_component_quality(1);
+ float waste = 0.1f - 0.016f * 0.5f * (piston_qual + get_component_quality(2));
+ float efficiency_coeff = 1.0f - 0.02f * piston_qual;
// apply rate and waste factor
- ticks *= (waste + 0.9f*power_rate)*power_level;
+ ticks *= (waste + 0.9f*power_rate)*power_level*efficiency_coeff;
// end result
return std::max(1, int(ticks));
}
diff --git a/plugins/stonesense b/plugins/stonesense
-Subproject 2a62ba5ed2607f4dbf0473e77502d4e19c19678
+Subproject 37a823541538023b9f3d0d1e8039cf32851de68
diff --git a/plugins/tiletypes.cpp b/plugins/tiletypes.cpp
index 190bda7c..6af94f2e 100644
--- a/plugins/tiletypes.cpp
+++ b/plugins/tiletypes.cpp
@@ -767,7 +767,7 @@ command_result executePaintJob(color_ostream &out)
}
// Remove liquid from walls, etc
- if (type != -1 && !DFHack::FlowPassable(type))
+ if (type != (df::tiletype)-1 && !DFHack::FlowPassable(type))
{
des.bits.flow_size = 0;
//des.bits.liquid_type = DFHack::liquid_water;
diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp
index 639e7c56..98258682 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 fc89fecc..c496f49b 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 00000000..f1ee072c
--- /dev/null
+++ b/scripts/devel/pop-screen.lua
@@ -0,0 +1,3 @@
+-- For killing bugged out gui script screens.
+
+dfhack.screen.dismiss(dfhack.gui.getCurViewscreen())
diff --git a/scripts/gui/liquids.lua b/scripts/gui/liquids.lua
index 869cac90..89f08b7c 100644
--- a/scripts/gui/liquids.lua
+++ b/scripts/gui/liquids.lua
@@ -3,6 +3,7 @@
local utils = require 'utils'
local gui = require 'gui'
local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
local liquids = require('plugins.liquids')
@@ -199,6 +200,42 @@ function LiquidsUI:onRenderBody(dc)
dc:string("Enter", COLOR_LIGHTGREEN):string(": Paint")
end
+function ensure_blocks(cursor, size, cb)
+ local cx,cy,cz = pos2xyz(cursor)
+ local all = true
+ for x=1,size.x or 1,16 do
+ for y=1,size.y or 1,16 do
+ for z=1,size.z do
+ if not dfhack.maps.getTileBlock(cx+x-1, cy+y-1, cz+z-1) then
+ all = false
+ end
+ end
+ end
+ end
+ if all then
+ cb()
+ return
+ end
+ dlg.showYesNoPrompt(
+ 'Instantiate Blocks',
+ 'Not all map blocks are allocated - instantiate?\n\nWarning: new untested feature.',
+ COLOR_YELLOW,
+ function()
+ for x=1,size.x or 1,16 do
+ for y=1,size.y or 1,16 do
+ for z=1,size.z do
+ dfhack.maps.ensureTileBlock(cx+x-1, cy+y-1, cz+z-1)
+ end
+ end
+ end
+ cb()
+ end,
+ function()
+ cb()
+ end
+ )
+end
+
function LiquidsUI:onInput(keys)
local paint = self.paint:get()
local liquid = paint.liquid
@@ -239,13 +276,15 @@ function LiquidsUI:onInput(keys)
else
guidm.clearSelection()
end
- liquids.paint(
+ local cb = curry(
+ liquids.paint,
cursor,
self.brush:get().tag, self.paint:get().tag,
self.amount, size,
self.set:get().tag, self.flow:get().tag,
self.permaflow:get().tag
)
+ ensure_blocks(cursor, size, cb)
elseif self:propagateMoveKeys(keys) then
return
elseif keys.D_LOOK_ARENA_WATER then
diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua
index 4468e1dc..c14bfcbe 100644
--- a/scripts/gui/mechanisms.lua
+++ b/scripts/gui/mechanisms.lua
@@ -122,7 +122,7 @@ function MechanismList:onInput(keys)
end
end
-if not string.find(dfhack.gui.getCurFocus(), 'dwarfmode/QueryBuilding/Some') then
+if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then
qerror("This script requires the main dwarfmode view in 'q' mode")
end
diff --git a/scripts/gui/power-meter.lua b/scripts/gui/power-meter.lua
new file mode 100644
index 00000000..8baf43e7
--- /dev/null
+++ b/scripts/gui/power-meter.lua
@@ -0,0 +1,116 @@
+-- Interface front-end for power-meter plugin.
+
+local utils = require 'utils'
+local gui = require 'gui'
+local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
+
+local plugin = require('plugins.power-meter')
+local bselector = df.global.ui_build_selector
+
+PowerMeter = defclass(PowerMeter, guidm.MenuOverlay)
+
+PowerMeter.focus_path = 'power-meter'
+
+function PowerMeter:init()
+ self:init_fields{
+ min_power = 0, max_power = -1, invert = false,
+ }
+ guidm.MenuOverlay.init(self)
+ return self
+end
+
+function PowerMeter:onShow()
+ guidm.MenuOverlay.onShow(self)
+
+ -- Send an event to update the errors
+ bselector.plate_info.flags.whole = 0
+ self:sendInputToParent('BUILDING_TRIGGER_ENABLE_WATER')
+end
+
+function PowerMeter:onRenderBody(dc)
+ dc:fill(0,0,dc.width-1,13,gui.CLEAR_PEN)
+ dc:seek(1,1):pen(COLOR_WHITE)
+ dc:string("Power Meter"):newline():newline(1)
+ dc:string("Placement"):newline():newline(1)
+
+ dc:string("Excess power range:")
+
+ dc:newline(3):string("as", COLOR_LIGHTGREEN)
+ dc:string(": Min ")
+ if self.min_power <= 0 then
+ dc:string("(any)")
+ else
+ dc:string(''..self.min_power)
+ end
+
+ dc:newline(3):string("zx", COLOR_LIGHTGREEN)
+ dc:string(": Max ")
+ if self.max_power < 0 then
+ dc:string("(any)")
+ else
+ dc:string(''..self.max_power)
+ end
+ dc:newline():newline(1)
+
+ dc:string("i",COLOR_LIGHTGREEN):string(": ")
+ if self.invert then
+ dc:string("Inverted")
+ else
+ dc:string("Not inverted")
+ end
+end
+
+function PowerMeter:onInput(keys)
+ if keys.CUSTOM_I then
+ self.invert = not self.invert
+ elseif keys.BUILDING_TRIGGER_MIN_WATER_UP then
+ self.min_power = self.min_power + 10
+ elseif keys.BUILDING_TRIGGER_MIN_WATER_DOWN then
+ self.min_power = math.max(0, self.min_power - 10)
+ elseif keys.BUILDING_TRIGGER_MAX_WATER_UP then
+ if self.max_power < 0 then
+ self.max_power = 0
+ else
+ self.max_power = self.max_power + 10
+ end
+ elseif keys.BUILDING_TRIGGER_MAX_WATER_DOWN then
+ self.max_power = math.max(-1, self.max_power - 10)
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+ self:sendInputToParent('LEAVESCREEN')
+ elseif keys.SELECT then
+ if #bselector.errors == 0 then
+ if not plugin.makePowerMeter(
+ bselector.plate_info,
+ self.min_power, self.max_power, self.invert
+ )
+ then
+ dlg.showMessage(
+ 'Power Meter',
+ 'Could not initialize.', COLOR_LIGHTRED
+ )
+
+ self:dismiss()
+ self:sendInputToParent('LEAVESCREEN')
+ return
+ end
+
+ self:sendInputToParent('SELECT')
+ if bselector.stage ~= 1 then
+ self:dismiss()
+ end
+ end
+ elseif self:propagateMoveKeys(keys) then
+ return
+ end
+end
+
+if dfhack.gui.getCurFocus() ~= 'dwarfmode/Build/Position/Trap'
+or bselector.building_subtype ~= df.trap_type.PressurePlate
+then
+ qerror("This script requires the main dwarfmode view in build pressure plate mode")
+end
+
+local list = mkinstance(PowerMeter):init()
+list:show()
diff --git a/scripts/gui/rename.lua b/scripts/gui/rename.lua
new file mode 100644
index 00000000..a457a0bf
--- /dev/null
+++ b/scripts/gui/rename.lua
@@ -0,0 +1,63 @@
+-- Rename various objects via gui.
+
+local gui = require 'gui'
+local dlg = require 'gui.dialogs'
+local plugin = require 'plugins.rename'
+
+local mode = ...
+local focus = dfhack.gui.getCurFocus()
+
+local function verify_mode(expected)
+ if mode ~= nil and mode ~= expected then
+ qerror('Invalid UI state for mode '..mode)
+ end
+end
+
+if string.match(focus, '^dwarfmode/QueryBuilding/Some') then
+ verify_mode('building')
+
+ local building = df.global.world.selected_building
+ if plugin.canRenameBuilding(building) then
+ dlg.showInputPrompt(
+ 'Rename Building',
+ 'Enter a new name for the building:', COLOR_GREEN,
+ building.name,
+ curry(plugin.renameBuilding, building)
+ )
+ else
+ dlg.showMessage(
+ 'Rename Building',
+ 'Cannot rename this type of building.', COLOR_LIGHTRED
+ )
+ end
+elseif dfhack.gui.getSelectedUnit(true) then
+ local unit = dfhack.gui.getSelectedUnit(true)
+
+ if mode == 'unit-profession' then
+ dlg.showInputPrompt(
+ 'Rename Unit',
+ 'Enter a new profession for the unit:', COLOR_GREEN,
+ unit.custom_profession,
+ function(newval)
+ unit.custom_profession = newval
+ end
+ )
+ else
+ verify_mode('unit')
+
+ local vname = dfhack.units.getVisibleName(unit)
+ local vnick = ''
+ if vname and vname.has_name then
+ vnick = vname.nickname
+ end
+
+ dlg.showInputPrompt(
+ 'Rename Unit',
+ 'Enter a new nickname for the unit:', COLOR_GREEN,
+ vnick,
+ curry(dfhack.units.setNickname, unit)
+ )
+ end
+elseif mode then
+ verify_mode(nil)
+end
diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua
new file mode 100644
index 00000000..47043cbb
--- /dev/null
+++ b/scripts/gui/siege-engine.lua
@@ -0,0 +1,490 @@
+-- Front-end for the siege engine plugin.
+
+local utils = require 'utils'
+local gui = require 'gui'
+local guidm = require 'gui.dwarfmode'
+local dlg = require 'gui.dialogs'
+
+local plugin = require 'plugins.siege-engine'
+local wmap = df.global.world.map
+
+local LEGENDARY = df.skill_rating.Legendary
+
+-- Globals kept between script calls
+last_target_min = last_target_min or nil
+last_target_max = last_target_max or nil
+
+local item_choices = {
+ { caption = 'boulders (default)', item_type = df.item_type.BOULDER },
+ { caption = 'blocks', item_type = df.item_type.BLOCKS },
+ { caption = 'weapons', item_type = df.item_type.WEAPON },
+ { caption = 'trap components', item_type = df.item_type.TRAPCOMP },
+ { caption = 'bins', item_type = df.item_type.BIN },
+ { caption = 'barrels', item_type = df.item_type.BARREL },
+ { caption = 'anything', item_type = -1 },
+}
+
+local item_choice_idx = {}
+for i,v in ipairs(item_choices) do
+ item_choice_idx[v.item_type] = i
+end
+
+SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay)
+
+SiegeEngine.focus_path = 'siege-engine'
+
+function SiegeEngine:init(building)
+ self:init_fields{
+ building = building,
+ center = utils.getBuildingCenter(building),
+ selected_pile = 1,
+ }
+ guidm.MenuOverlay.init(self)
+ self.mode_main = {
+ render = self:callback 'onRenderBody_main',
+ input = self:callback 'onInput_main',
+ }
+ self.mode_aim = {
+ render = self:callback 'onRenderBody_aim',
+ input = self:callback 'onInput_aim',
+ }
+ self.mode_pile = {
+ render = self:callback 'onRenderBody_pile',
+ input = self:callback 'onInput_pile',
+ }
+ return self
+end
+
+function SiegeEngine:onShow()
+ guidm.MenuOverlay.onShow(self)
+
+ self.old_cursor = guidm.getCursorPos()
+ self.old_viewport = self:getViewport()
+
+ self.mode = self.mode_main
+ self:showCursor(false)
+end
+
+function SiegeEngine:onDestroy()
+ if self.save_profile then
+ plugin.saveWorkshopProfile(self.building)
+ end
+ if not self.no_select_building then
+ self:selectBuilding(self.building, self.old_cursor, self.old_viewport, 10)
+ end
+end
+
+function SiegeEngine:showCursor(enable)
+ local cursor = guidm.getCursorPos()
+ if cursor and not enable then
+ self.cursor = cursor
+ self.target_select_first = nil
+ guidm.clearCursorPos()
+ elseif not cursor and enable then
+ local view = self:getViewport()
+ cursor = self.cursor
+ if not cursor or not view:isVisible(cursor) then
+ cursor = view:getCenter()
+ end
+ self.cursor = nil
+ guidm.setCursorPos(cursor)
+ end
+end
+
+function SiegeEngine:centerViewOn(pos)
+ local cursor = guidm.getCursorPos()
+ if cursor then
+ guidm.setCursorPos(pos)
+ else
+ self.cursor = pos
+ end
+ self:getViewport():centerOn(pos):set()
+end
+
+function SiegeEngine:zoomToTarget()
+ local target_min, target_max = plugin.getTargetArea(self.building)
+ if target_min then
+ local cx = math.floor((target_min.x + target_max.x)/2)
+ local cy = math.floor((target_min.y + target_max.y)/2)
+ local cz = math.floor((target_min.z + target_max.z)/2)
+ local pos = plugin.adjustToTarget(self.building, xyz2pos(cx,cy,cz))
+ self:centerViewOn(pos)
+ end
+end
+
+function paint_target_grid(dc, view, origin, p1, p2)
+ local r1, sz, r2 = guidm.getSelectionRange(p1, p2)
+
+ if view.z < r1.z or view.z > r2.z then
+ return
+ end
+
+ local p1 = view:tileToScreen(r1)
+ local p2 = view:tileToScreen(r2)
+ local org = view:tileToScreen(origin)
+ dc:pen{ fg = COLOR_CYAN, bg = COLOR_CYAN, ch = '+', bold = true }
+
+ -- Frame
+ dc:fill(p1.x,p1.y,p1.x,p2.y)
+ dc:fill(p1.x,p1.y,p2.x,p1.y)
+ dc:fill(p2.x,p1.y,p2.x,p2.y)
+ dc:fill(p1.x,p2.y,p2.x,p2.y)
+
+ -- Grid
+ local gxmin = org.x+10*math.ceil((p1.x-org.x)/10)
+ local gxmax = org.x+10*math.floor((p2.x-org.x)/10)
+ local gymin = org.y+10*math.ceil((p1.y-org.y)/10)
+ local gymax = org.y+10*math.floor((p2.y-org.y)/10)
+ for x = gxmin,gxmax,10 do
+ for y = gymin,gymax,10 do
+ dc:fill(p1.x,y,p2.x,y)
+ dc:fill(x,p1.y,x,p2.y)
+ end
+ end
+end
+
+function SiegeEngine:renderTargetView(target_min, target_max)
+ local view = self:getViewport()
+ local map = self.df_layout.map
+ local map_dc = gui.Painter.new(map)
+
+ plugin.paintAimScreen(
+ self.building, view:getPos(),
+ xy2pos(map.x1, map.y1), view:getSize()
+ )
+
+ if target_min and math.floor(dfhack.getTickCount()/500) % 2 == 0 then
+ paint_target_grid(map_dc, view, self.center, target_min, target_max)
+ end
+
+ local cursor = guidm.getCursorPos()
+ if cursor then
+ local cx, cy, cz = pos2xyz(view:tileToScreen(cursor))
+ if cz == 0 then
+ map_dc:seek(cx,cy):char('X', COLOR_YELLOW)
+ end
+ end
+end
+
+function SiegeEngine:scrollPiles(delta)
+ local links = plugin.getStockpileLinks(self.building)
+ if links then
+ self.selected_pile = 1+(self.selected_pile+delta-1) % #links
+ return links[self.selected_pile]
+ end
+end
+
+function SiegeEngine:renderStockpiles(dc, links, nlines)
+ local idx = (self.selected_pile-1) % #links
+ local page = math.floor(idx/nlines)
+ for i = page*nlines,math.min(#links,(page+1)*nlines)-1 do
+ local color = COLOR_BROWN
+ if i == idx then
+ color = COLOR_YELLOW
+ end
+ dc:newline(2):string(utils.getBuildingName(links[i+1]), color)
+ end
+end
+
+function SiegeEngine:onRenderBody_main(dc)
+ dc:newline(1):pen(COLOR_WHITE):string("Target: ")
+
+ local target_min, target_max = plugin.getTargetArea(self.building)
+ if target_min then
+ dc:string(
+ (target_max.x-target_min.x+1).."x"..
+ (target_max.y-target_min.y+1).."x"..
+ (target_max.z-target_min.z+1).." Rect"
+ )
+ else
+ dc:string("None (default)")
+ end
+
+ dc:newline(3):string("r",COLOR_LIGHTGREEN):string(": Rectangle")
+ if last_target_min then
+ dc:string(", "):string("p",COLOR_LIGHTGREEN):string(": Paste")
+ end
+ dc:newline(3)
+ if target_min then
+ dc:string("x",COLOR_LIGHTGREEN):string(": Clear, ")
+ dc:string("z",COLOR_LIGHTGREEN):string(": Zoom")
+ end
+
+ dc:newline():newline(1)
+ if self.building.type == df.siegeengine_type.Ballista then
+ dc:string("Uses ballista arrows")
+ else
+ local item = plugin.getAmmoItem(self.building)
+ dc:string("u",COLOR_LIGHTGREEN):string(": Use ")
+ if item_choice_idx[item] then
+ dc:string(item_choices[item_choice_idx[item]].caption)
+ else
+ dc:string(df.item_type[item])
+ end
+ end
+
+ dc:newline():newline(1)
+ dc:string("t",COLOR_LIGHTGREEN):string(": Take from stockpile"):newline(3)
+ local links = plugin.getStockpileLinks(self.building)
+ local bottom = dc.height - 5
+ if links then
+ dc:string("d",COLOR_LIGHTGREEN):string(": Delete, ")
+ dc:string("o",COLOR_LIGHTGREEN):string(": Zoom"):newline()
+ self:renderStockpiles(dc, links, bottom-2-dc:localY())
+ dc:newline():newline()
+ end
+
+ local prof = self.building:getWorkshopProfile() or {}
+ dc:seek(1,math.max(dc:localY(),19)):string('ghjk',COLOR_LIGHTGREEN)dc:string(': ')
+ dc:string(df.skill_rating.attrs[prof.min_level or 0].caption):string('-')
+ dc:string(df.skill_rating.attrs[math.min(LEGENDARY,prof.max_level or 3000)].caption)
+ dc:newline():newline()
+
+ if self.target_select_first then
+ self:renderTargetView(self.target_select_first, guidm.getCursorPos())
+ else
+ self:renderTargetView(target_min, target_max)
+ end
+end
+
+function SiegeEngine:setTargetArea(p1, p2)
+ self.target_select_first = nil
+
+ if not plugin.setTargetArea(self.building, p1, p2) then
+ dlg.showMessage(
+ 'Set Target Area',
+ 'Could not set the target area', COLOR_LIGHTRED
+ )
+ else
+ last_target_min = p1
+ last_target_max = p2
+ end
+end
+
+function SiegeEngine:setAmmoItem(choice)
+ if self.building.type == df.siegeengine_type.Ballista then
+ return
+ end
+
+ if not plugin.setAmmoItem(self.building, choice.item_type) then
+ dlg.showMessage(
+ 'Set Ammo Item',
+ 'Could not set the ammo item', COLOR_LIGHTRED
+ )
+ end
+end
+
+function SiegeEngine:onInput_main(keys)
+ if keys.CUSTOM_R then
+ self:showCursor(true)
+ self.target_select_first = nil
+ self.mode = self.mode_aim
+ elseif keys.CUSTOM_P and last_target_min then
+ self:setTargetArea(last_target_min, last_target_max)
+ elseif keys.CUSTOM_U then
+ local item = plugin.getAmmoItem(self.building)
+ local idx = 1 + (item_choice_idx[item] or 0) % #item_choices
+ self:setAmmoItem(item_choices[idx])
+ elseif keys.CUSTOM_Z then
+ self:zoomToTarget()
+ elseif keys.CUSTOM_X then
+ plugin.clearTargetArea(self.building)
+ elseif keys.SECONDSCROLL_UP then
+ self:scrollPiles(-1)
+ elseif keys.SECONDSCROLL_DOWN then
+ self:scrollPiles(1)
+ elseif keys.CUSTOM_D then
+ local pile = self:scrollPiles(0)
+ if pile then
+ plugin.removeStockpileLink(self.building, pile)
+ end
+ elseif keys.CUSTOM_O then
+ local pile = self:scrollPiles(0)
+ if pile then
+ self:centerViewOn(utils.getBuildingCenter(pile))
+ end
+ elseif keys.CUSTOM_T then
+ self:showCursor(true)
+ self.mode = self.mode_pile
+ self:sendInputToParent('CURSOR_DOWN_Z')
+ self:sendInputToParent('CURSOR_UP_Z')
+ elseif keys.CUSTOM_G then
+ local prof = plugin.saveWorkshopProfile(self.building)
+ prof.min_level = math.max(0, prof.min_level-1)
+ plugin.saveWorkshopProfile(self.building)
+ elseif keys.CUSTOM_H then
+ local prof = plugin.saveWorkshopProfile(self.building)
+ prof.min_level = math.min(LEGENDARY, prof.min_level+1)
+ plugin.saveWorkshopProfile(self.building)
+ elseif keys.CUSTOM_J then
+ local prof = plugin.saveWorkshopProfile(self.building)
+ prof.max_level = math.max(0, math.min(LEGENDARY,prof.max_level)-1)
+ plugin.saveWorkshopProfile(self.building)
+ elseif keys.CUSTOM_K then
+ local prof = plugin.saveWorkshopProfile(self.building)
+ prof.max_level = math.min(LEGENDARY, prof.max_level+1)
+ if prof.max_level >= LEGENDARY then prof.max_level = 3000 end
+ plugin.saveWorkshopProfile(self.building)
+ elseif self:simulateViewScroll(keys) then
+ self.cursor = nil
+ else
+ return false
+ end
+ return true
+end
+
+local status_table = {
+ ok = { pen = COLOR_GREEN, msg = "Target accessible" },
+ out_of_range = { pen = COLOR_CYAN, msg = "Target out of range" },
+ blocked = { pen = COLOR_RED, msg = "Target obstructed" },
+ semi_blocked = { pen = COLOR_BROWN, msg = "Partially obstructed" },
+}
+
+function SiegeEngine:onRenderBody_aim(dc)
+ local cursor = guidm.getCursorPos()
+ local first = self.target_select_first
+
+ dc:newline(1):string('Select target rectangle'):newline()
+
+ local info = status_table[plugin.getTileStatus(self.building, cursor)]
+ if info then
+ dc:newline(2):string(info.msg, info.pen)
+ else
+ dc:newline(2):string('ERROR', COLOR_RED)
+ end
+
+ dc:newline():newline(1):string("Enter",COLOR_LIGHTGREEN)
+ if first then
+ dc:string(": Finish rectangle")
+ else
+ dc:string(": Start rectangle")
+ end
+ dc:newline()
+
+ local target_min, target_max = plugin.getTargetArea(self.building)
+ if target_min then
+ dc:newline(1):string("z",COLOR_LIGHTGREEN):string(": Zoom to current target")
+ end
+
+ if first then
+ self:renderTargetView(first, cursor)
+ else
+ local target_min, target_max = plugin.getTargetArea(self.building)
+ self:renderTargetView(target_min, target_max)
+ end
+end
+
+function SiegeEngine:onInput_aim(keys)
+ if keys.SELECT then
+ local cursor = guidm.getCursorPos()
+ if self.target_select_first then
+ self:setTargetArea(self.target_select_first, cursor)
+
+ self.mode = self.mode_main
+ self:showCursor(false)
+ else
+ self.target_select_first = cursor
+ end
+ elseif keys.CUSTOM_Z then
+ self:zoomToTarget()
+ elseif keys.LEAVESCREEN then
+ self.mode = self.mode_main
+ self:showCursor(false)
+ elseif self:simulateCursorMovement(keys) then
+ self.cursor = nil
+ else
+ return false
+ end
+ return true
+end
+
+function SiegeEngine:onRenderBody_pile(dc)
+ dc:newline(1):string('Select pile to take from'):newline():newline(2)
+
+ local sel = df.global.world.selected_building
+
+ if df.building_stockpilest:is_instance(sel) then
+ dc:string(utils.getBuildingName(sel), COLOR_GREEN):newline():newline(1)
+
+ if plugin.isLinkedToPile(self.building, sel) then
+ dc:string("Already taking from here"):newline():newline(2)
+ dc:string("d", COLOR_LIGHTGREEN):string(": Delete link")
+ else
+ dc:string("Enter",COLOR_LIGHTGREEN):string(": Take from this pile")
+ end
+ elseif sel then
+ dc:string(utils.getBuildingName(sel), COLOR_DARKGREY)
+ dc:newline():newline(1)
+ dc:string("Not a stockpile",COLOR_LIGHTRED)
+ else
+ dc:string("No building selected", COLOR_DARKGREY)
+ end
+end
+
+function SiegeEngine:onInput_pile(keys)
+ if keys.SELECT then
+ local sel = df.global.world.selected_building
+ if df.building_stockpilest:is_instance(sel)
+ and not plugin.isLinkedToPile(self.building, sel) then
+ plugin.addStockpileLink(self.building, sel)
+
+ df.global.world.selected_building = self.building
+ self.mode = self.mode_main
+ self:showCursor(false)
+ end
+ elseif keys.CUSTOM_D then
+ local sel = df.global.world.selected_building
+ if df.building_stockpilest:is_instance(sel) then
+ plugin.removeStockpileLink(self.building, sel)
+ end
+ elseif keys.LEAVESCREEN then
+ df.global.world.selected_building = self.building
+ self.mode = self.mode_main
+ self:showCursor(false)
+ elseif self:propagateMoveKeys(keys) then
+ --
+ else
+ return false
+ end
+ return true
+end
+
+function SiegeEngine:onRenderBody(dc)
+ dc:clear()
+ dc:seek(1,1):pen(COLOR_WHITE):string(utils.getBuildingName(self.building)):newline()
+
+ self.mode.render(dc)
+
+ dc:seek(1, math.max(dc:localY(), 21)):pen(COLOR_WHITE)
+ dc:string("ESC", COLOR_LIGHTGREEN):string(": Back, ")
+ dc:string("c", COLOR_LIGHTGREEN):string(": Recenter")
+end
+
+function SiegeEngine:onInput(keys)
+ if self.mode.input(keys) then
+ --
+ elseif keys.CUSTOM_C then
+ self:centerViewOn(self.center)
+ elseif keys.LEAVESCREEN then
+ self:dismiss()
+ elseif keys.LEAVESCREEN_ALL then
+ self:dismiss()
+ self.no_select_building = true
+ guidm.clearCursorPos()
+ df.global.ui.main.mode = df.ui_sidebar_mode.Default
+ df.global.world.selected_building = nil
+ end
+end
+
+if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/SiegeEngine') then
+ qerror("This script requires a siege engine selected in 'q' mode")
+end
+
+local building = df.global.world.selected_building
+
+if not df.building_siegeenginest:is_instance(building) then
+ qerror("A siege engine must be selected")
+end
+
+local list = mkinstance(SiegeEngine):init(df.global.world.selected_building)
+list:show()