Merge branch 'master' of git://github.com/angavrilov/dfhack

develop
Timothy Collett 2012-09-10 11:56:23 -04:00
commit ccefd02ee3
67 changed files with 6730 additions and 637 deletions

@ -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.
@ -764,10 +782,20 @@ Gui module
Adds a regular announcement with given text, color, and brightness.
The is_bright boolean actually seems to invert the brightness.
* ``dfhack.gui.showZoomAnnouncement(type,pos,text,color[,is_bright])``
Like above, but also specifies a position you can zoom to from the announcement menu.
* ``dfhack.gui.showPopupAnnouncement(text,color[,is_bright])``
Pops up a titan-style modal announcement window.
* ``dfhack.gui.showAutoAnnouncement(type,pos,text,color[,is_bright])``
Uses the type to look up options from announcements.txt, and calls the
above operations accordingly. If enabled, pauses and zooms to position.
Job module
----------
@ -840,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.
@ -866,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*.
@ -959,10 +1015,18 @@ Maps module
Returns a map block object for given x,y,z in local block coordinates.
* ``dfhack.maps.isValidTilePos(coords)``, or isValidTilePos(x,y,z)``
Checks if the given df::coord or x,y,z in local tile coordinates are valid.
* ``dfhack.maps.getTileBlock(coords)``, or ``getTileBlock(x,y,z)``
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.
@ -971,6 +1035,11 @@ Maps module
Enables updates for liquid flow or temperature, unless already active.
* ``dfhack.maps.spawnFlow(pos,type,mat_type,mat_index,dimension)``
Spawns a new flow (i.e. steam/mist/dust/etc) at the given pos, and with
the given parameters. Returns it, or *nil* if unsuccessful.
* ``dfhack.maps.getGlobalInitFeature(index)``
Returns the global feature object with the given index.
@ -1267,6 +1336,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

@ -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>,
@ -1019,13 +1036,20 @@ the container itself.</p>
<p>Adds a regular announcement with given text, color, and brightness.
The is_bright boolean actually seems to invert the brightness.</p>
</li>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.gui.showZoomAnnouncement(type,pos,text,color[,is_bright])</span></tt></p>
<p>Like above, but also specifies a position you can zoom to from the announcement menu.</p>
</li>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.gui.showPopupAnnouncement(text,color[,is_bright])</span></tt></p>
<p>Pops up a titan-style modal announcement window.</p>
</li>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.gui.showAutoAnnouncement(type,pos,text,color[,is_bright])</span></tt></p>
<p>Uses the type to look up options from announcements.txt, and calls the
above operations accordingly. If enabled, pauses and zooms to position.</p>
</li>
</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>
@ -1062,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>
@ -1082,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>
@ -1102,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>
@ -1123,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>
@ -1166,7 +1216,7 @@ Returns <em>false</em> in case of error.</p>
</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>
@ -1177,15 +1227,25 @@ Returns <em>false</em> in case of error.</p>
<li><p class="first"><tt class="docutils literal">dfhack.maps.getBlock(x,y,z)</tt></p>
<p>Returns a map block object for given x,y,z in local block coordinates.</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.maps.isValidTilePos(coords)</tt>, or isValidTilePos(x,y,z)``</p>
<p>Checks if the given df::coord or x,y,z in local tile coordinates are valid.</p>
</li>
<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>
<li><p class="first"><tt class="docutils literal"><span class="pre">dfhack.maps.enableBlockUpdates(block[,flow,temperature])</span></tt></p>
<p>Enables updates for liquid flow or temperature, unless already active.</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.maps.spawnFlow(pos,type,mat_type,mat_index,dimension)</tt></p>
<p>Spawns a new flow (i.e. steam/mist/dust/etc) at the given pos, and with
the given parameters. Returns it, or <em>nil</em> if unsuccessful.</p>
</li>
<li><p class="first"><tt class="docutils literal">dfhack.maps.getGlobalInitFeature(index)</tt></p>
<p>Returns the global feature object with the given index.</p>
</li>
@ -1207,7 +1267,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>
@ -1242,7 +1302,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.
@ -1386,7 +1446,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
@ -1402,7 +1462,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>
@ -1446,6 +1506,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>
@ -1541,7 +1605,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>
@ -1589,7 +1653,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>
@ -1620,7 +1684,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.
@ -1652,7 +1716,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
@ -1681,7 +1745,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>
@ -1722,7 +1786,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>
@ -1835,7 +1899,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>
@ -1849,14 +1913,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>
@ -1894,13 +1958,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

@ -16,6 +16,10 @@ keybinding add Ctrl-K autodump-destroy-item
# quicksave, only in main dwarfmode screen and menu page
keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
# gui/rename script
keybinding add Ctrl-Shift-N gui/rename
keybinding add Ctrl-Shift-P "gui/rename unit-profession"
##############################
# Generic adv mode bindings #
##############################
@ -48,3 +52,25 @@ keybinding add Ctrl-M@dwarfmode/QueryBuilding/Some gui/mechanisms
# browse rooms of same owner
keybinding add Alt-R@dwarfmode/QueryBuilding/Some gui/room-list.work
# interface for the liquids plugin
keybinding add Alt-L@dwarfmode/LookAround gui/liquids
# machine power sensitive pressure plate construction
keybinding add Ctrl-Shift-M@dwarfmode/Build/Position/Trap gui/power-meter
###################
# UI logic tweaks #
###################
# stabilize the cursor of dwarfmode when switching menus
tweak stable-cursor
# stop military from considering training as 'patrol duty'
tweak patrol-duty
# display creature weight in build plate menu as ??K, instead of (???df: Max
tweak readable-build-plate
# improve FPS by squashing endless item temperature update loops
tweak stable-temp

@ -219,28 +219,30 @@ static std::string getScriptHelp(std::string path, std::string helpprefix)
return "No help available.";
}
static std::map<string,string> listScripts(PluginManager *plug_mgr, std::string path)
static void listScripts(PluginManager *plug_mgr, std::map<string,string> &pset, std::string path, bool all, std::string prefix = "")
{
std::vector<string> files;
getdir(path, files);
std::map<string,string> pset;
for (size_t i = 0; i < files.size(); i++)
{
if (hasEnding(files[i], ".lua"))
{
std::string help = getScriptHelp(path + files[i], "-- ");
pset[files[i].substr(0, files[i].size()-4)] = help;
pset[prefix + files[i].substr(0, files[i].size()-4)] = help;
}
else if (plug_mgr->eval_ruby && hasEnding(files[i], ".rb"))
{
std::string help = getScriptHelp(path + files[i], "# ");
pset[files[i].substr(0, files[i].size()-3)] = help;
pset[prefix + files[i].substr(0, files[i].size()-3)] = help;
}
else if (all && !files[i].empty() && files[i][0] != '.')
{
listScripts(plug_mgr, pset, path+files[i]+"/", all, prefix+files[i]+"/");
}
}
return pset;
}
static bool fileExists(std::string path)
@ -335,7 +337,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
con.print("Basic commands:\n"
" help|?|man - This text.\n"
" help COMMAND - Usage help for the given command.\n"
" ls|dir [PLUGIN] - List available commands. Optionally for single plugin.\n"
" ls|dir [-a] [PLUGIN] - List available commands. Optionally for single plugin.\n"
" cls - Clear the console.\n"
" fpause - Force DF to pause.\n"
" die - Force DF to close immediately\n"
@ -469,6 +471,12 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
}
else if(first == "ls" || first == "dir")
{
bool all = false;
if (parts.size() && parts[0] == "-a")
{
all = true;
vector_erase_at(parts, 0);
}
if(parts.size())
{
string & plugname = parts[0];
@ -491,7 +499,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
con.print(
"builtin:\n"
" help|?|man - This text or help specific to a plugin.\n"
" ls [PLUGIN] - List available commands. Optionally for single plugin.\n"
" ls [-a] [PLUGIN] - List available commands. Optionally for single plugin.\n"
" cls - Clear the console.\n"
" fpause - Force DF to pause.\n"
" die - Force DF to close immediately\n"
@ -523,7 +531,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
con.print(" %-22s- %s\n",(*iter).name.c_str(), (*iter).description.c_str());
con.reset_color();
}
auto scripts = listScripts(plug_mgr, getHackPath() + "scripts/");
std::map<string, string> scripts;
listScripts(plug_mgr, scripts, getHackPath() + "scripts/", all);
if (!scripts.empty())
{
con.print("\nscripts:\n");

@ -218,8 +218,10 @@ virtual_identity::virtual_identity(size_t size, TAllocateFn alloc,
virtual_identity::~virtual_identity()
{
// Remove interpose entries, so that they don't try accessing this object later
for (int i = interpose_list.size()-1; i >= 0; i--)
interpose_list[i]->remove();
for (auto it = interpose_list.begin(); it != interpose_list.end(); ++it)
if (it->second)
it->second->on_host_delete(this);
interpose_list.clear();
}
/* Vtable name to identity lookup. */
@ -372,7 +374,7 @@ void DFHack::bitfieldToString(std::vector<std::string> *pvec, const void *p,
unsigned size, const bitfield_item_info *items)
{
for (unsigned i = 0; i < size; i++) {
int value = getBitfieldField(p, i, std::min(1,items[i].size));
int value = getBitfieldField(p, i, std::max(1,items[i].size));
if (value) {
std::string name = format_key(items[i].name, i);

@ -77,6 +77,9 @@ distribution.
#include "df/job_material_category.h"
#include "df/burrow.h"
#include "df/building_civzonest.h"
#include "df/region_map_entry.h"
#include "df/flow_info.h"
#include "df/unit_misc_trait.h"
#include <lua.h>
#include <lauxlib.h>
@ -726,6 +729,7 @@ static std::string getOSType()
}
static std::string getDFVersion() { return Core::getInstance().vinfo->getVersion(); }
static uint32_t getTickCount() { return Core::getInstance().p->getTickCount(); }
static std::string getDFPath() { return Core::getInstance().p->getPath(); }
static std::string getHackPath() { return Core::getInstance().getHackPath(); }
@ -737,6 +741,7 @@ static const LuaWrapper::FunctionReg dfhack_module[] = {
WRAP(getOSType),
WRAP(getDFVersion),
WRAP(getDFPath),
WRAP(getTickCount),
WRAP(getHackPath),
WRAP(isWorldLoaded),
WRAP(isMapLoaded),
@ -755,7 +760,9 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
WRAPM(Gui, getSelectedUnit),
WRAPM(Gui, getSelectedItem),
WRAPM(Gui, showAnnouncement),
WRAPM(Gui, showZoomAnnouncement),
WRAPM(Gui, showPopupAnnouncement),
WRAPM(Gui, showAutoAnnouncement),
{ NULL, NULL }
};
@ -807,12 +814,20 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getVisibleName),
WRAPM(Units, getIdentity),
WRAPM(Units, getNemesis),
WRAPM(Units, isCrazed),
WRAPM(Units, isOpposedToLife),
WRAPM(Units, hasExtravision),
WRAPM(Units, isBloodsucker),
WRAPM(Units, isMischievous),
WRAPM(Units, getMiscTrait),
WRAPM(Units, isDead),
WRAPM(Units, isAlive),
WRAPM(Units, isSane),
WRAPM(Units, isDwarf),
WRAPM(Units, isCitizen),
WRAPM(Units, getAge),
WRAPM(Units, getEffectiveSkill),
WRAPM(Units, computeMovementSpeed),
WRAPM(Units, getProfessionName),
WRAPM(Units, getCasteProfessionName),
WRAPM(Units, getProfessionColor),
@ -911,9 +926,17 @@ static const LuaWrapper::FunctionReg dfhack_maps_module[] = {
WRAPM(Maps, getGlobalInitFeature),
WRAPM(Maps, getLocalInitFeature),
WRAPM(Maps, canWalkBetween),
WRAPM(Maps, spawnFlow),
{ NULL, NULL }
};
static int maps_isValidTilePos(lua_State *L)
{
auto pos = CheckCoordXYZ(L, 1, true);
lua_pushboolean(L, Maps::isValidTilePos(pos));
return 1;
}
static int maps_getTileBlock(lua_State *L)
{
auto pos = CheckCoordXYZ(L, 1, true);
@ -921,6 +944,13 @@ static int maps_getTileBlock(lua_State *L)
return 1;
}
static int maps_ensureTileBlock(lua_State *L)
{
auto pos = CheckCoordXYZ(L, 1, true);
Lua::PushDFObject(L, Maps::ensureTileBlock(pos));
return 1;
}
static int maps_getRegionBiome(lua_State *L)
{
auto pos = CheckCoordXY(L, 1, true);
@ -931,12 +961,13 @@ static int maps_getRegionBiome(lua_State *L)
static int maps_getTileBiomeRgn(lua_State *L)
{
auto pos = CheckCoordXYZ(L, 1, true);
Lua::PushPosXY(L, Maps::getTileBiomeRgn(pos));
return 1;
return Lua::PushPosXY(L, Maps::getTileBiomeRgn(pos));
}
static const luaL_Reg dfhack_maps_funcs[] = {
{ "isValidTilePos", maps_isValidTilePos },
{ "getTileBlock", maps_getTileBlock },
{ "ensureTileBlock", maps_ensureTileBlock },
{ "getRegionBiome", maps_getRegionBiome },
{ "getTileBiomeRgn", maps_getTileBiomeRgn },
{ NULL, NULL }
@ -1132,6 +1163,45 @@ static int screen_paintTile(lua_State *L)
return 1;
}
static int screen_readTile(lua_State *L)
{
int x = luaL_checkint(L, 1);
int y = luaL_checkint(L, 2);
Pen pen = Screen::readTile(x, y);
if (!pen.valid())
{
lua_pushnil(L);
}
else
{
lua_newtable(L);
lua_pushinteger(L, pen.ch); lua_setfield(L, -2, "ch");
lua_pushinteger(L, pen.fg); lua_setfield(L, -2, "fg");
lua_pushinteger(L, pen.bg); lua_setfield(L, -2, "bg");
lua_pushboolean(L, pen.bold); lua_setfield(L, -2, "bold");
if (pen.tile)
{
lua_pushinteger(L, pen.tile); lua_setfield(L, -2, "tile");
switch (pen.tile_mode) {
case Pen::CharColor:
lua_pushboolean(L, true); lua_setfield(L, -2, "tile_color");
break;
case Pen::TileColor:
lua_pushinteger(L, pen.tile_fg); lua_setfield(L, -2, "tile_fg");
lua_pushinteger(L, pen.tile_bg); lua_setfield(L, -2, "tile_bg");
break;
default:
break;
}
}
}
return 1;
}
static int screen_paintString(lua_State *L)
{
Pen pen;
@ -1236,6 +1306,7 @@ static const luaL_Reg dfhack_screen_funcs[] = {
{ "getMousePos", screen_getMousePos },
{ "getWindowSize", screen_getWindowSize },
{ "paintTile", screen_paintTile },
{ "readTile", screen_readTile },
{ "paintString", screen_paintString },
{ "fillRect", screen_fillRect },
{ "findGraphicsTile", screen_findGraphicsTile },

@ -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");

@ -894,7 +894,7 @@ static int meta_bitfield_len(lua_State *state)
static void read_bitfield(lua_State *state, uint8_t *ptr, bitfield_identity *id, int idx)
{
int size = id->getBits()[idx].size;
int size = std::max(1, id->getBits()[idx].size);
int value = getBitfieldField(ptr, idx, size);
if (size <= 1)
@ -951,7 +951,7 @@ static int meta_bitfield_newindex(lua_State *state)
}
int idx = check_container_index(state, id->getNumBits(), 2, iidx, "write");
int size = id->getBits()[idx].size;
int size = std::max(1, id->getBits()[idx].size);
if (lua_isboolean(state, 3) || lua_isnil(state, 3))
setBitfieldField(ptr, idx, size, lua_toboolean(state, 3));

@ -186,14 +186,17 @@ Plugin::~Plugin()
bool Plugin::load(color_ostream &con)
{
RefAutolock lock(access);
if(state == PS_BROKEN)
{
return false;
}
else if(state == PS_LOADED)
{
return true;
RefAutolock lock(access);
if(state == PS_LOADED)
{
return true;
}
else if(state != PS_UNLOADED)
{
return false;
}
state = PS_LOADING;
}
// enter suspend
CoreSuspender suspend;
@ -202,6 +205,7 @@ bool Plugin::load(color_ostream &con)
if(!plug)
{
con.printerr("Can't load plugin %s\n", filename.c_str());
RefAutolock lock(access);
state = PS_BROKEN;
return false;
}
@ -211,6 +215,7 @@ bool Plugin::load(color_ostream &con)
{
con.printerr("Plugin %s has no name or version.\n", filename.c_str());
ClosePlugin(plug);
RefAutolock lock(access);
state = PS_BROKEN;
return false;
}
@ -219,9 +224,11 @@ bool Plugin::load(color_ostream &con)
con.printerr("Plugin %s was not built for this version of DFHack.\n"
"Plugin: %s, DFHack: %s\n", *plug_name, *plug_version, DFHACK_VERSION);
ClosePlugin(plug);
RefAutolock lock(access);
state = PS_BROKEN;
return false;
}
RefAutolock lock(access);
plugin_init = (command_result (*)(color_ostream &, std::vector <PluginCommand> &)) LookupPlugin(plug, "plugin_init");
if(!plugin_init)
{
@ -273,8 +280,11 @@ bool Plugin::unload(color_ostream &con)
}
// wait for all calls to finish
access->wait();
state = PS_UNLOADING;
access->unlock();
// enter suspend
CoreSuspender suspend;
access->lock();
// notify plugin about shutdown, if it has a shutdown function
command_result cr = CR_OK;
if(plugin_shutdown)

@ -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];

@ -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";

@ -410,6 +410,11 @@ string Process::doReadClassName (void * vptr)
return raw;
}
uint32_t Process::getTickCount()
{
return GetTickCount();
}
string Process::getPath()
{
HMODULE hmod;

@ -48,6 +48,26 @@ struct MSVC_MPTR {
intptr_t this_shift;
};
static uint32_t *follow_jmp(void *ptr)
{
uint8_t *p = (uint8_t*)ptr;
for (;;)
{
switch (*p)
{
case 0xE9:
p += 5 + *(int32_t*)(p+1);
break;
case 0xEB:
p += 2 + *(int8_t*)(p+1);
break;
default:
return (uint32_t*)p;
}
}
}
bool DFHack::is_vmethod_pointer_(void *pptr)
{
auto pobj = (MSVC_MPTR*)pptr;
@ -55,7 +75,7 @@ bool DFHack::is_vmethod_pointer_(void *pptr)
// MSVC implements pointers to vmethods via thunks.
// This expects that they all follow a very specific pattern.
auto pval = (unsigned*)pobj->method;
auto pval = follow_jmp(pobj->method);
switch (pval[0]) {
case 0x20FF018BU: // mov eax, [ecx]; jmp [eax]
case 0x60FF018BU: // mov eax, [ecx]; jmp [eax+0x??]
@ -71,7 +91,7 @@ int DFHack::vmethod_pointer_to_idx_(void *pptr)
auto pobj = (MSVC_MPTR*)pptr;
if (!pobj->method || pobj->this_shift != 0) return -1;
auto pval = (unsigned*)pobj->method;
auto pval = follow_jmp(pobj->method);
switch (pval[0]) {
case 0x20FF018BU: // mov eax, [ecx]; jmp [eax]
return 0;
@ -154,6 +174,73 @@ bool virtual_identity::set_vmethod_ptr(int idx, void *ptr)
return Core::getInstance().p->patchMemory(&vtable[idx], &ptr, sizeof(void*));
}
/*
VMethod interposing data structures.
In order to properly support adding and removing hooks,
it is necessary to track them. This is what this class
is for. The task is further complicated by propagating
hooks to child classes that use exactly the same original
vmethod implementation.
Every applied link contains in the saved_chain field a
pointer to the next vmethod body that should be called
by the hook the link represents. This is the actual
control flow structure that needs to be maintained.
There also are connections between link objects themselves,
which constitute the bookkeeping for doing that. Finally,
every link is associated with a fixed virtual_identity host,
which represents the point in the class hierarchy where
the hook is applied.
When there are no subclasses (i.e. only one host), the
structures look like this:
+--------------+ +------------+
| link1 |-next------->| link2 |-next=NULL
|s_c: original |<-------prev-|s_c: $link1 |<--+
+--------------+ +------------+ |
|
host->interpose_list[vmethod_idx] ------+
vtable: $link2
The original vtable entry is stored in the saved_chain of the
first link. The interpose_list map points to the last one.
The hooks are called in order: link2 -> link1 -> original.
When there are subclasses that use the same vmethod, but don't
hook it, the topmost link gets a set of the child_hosts, and
the hosts have the link added to their interpose_list:
+--------------+ +----------------+
| link0 @host0 |<--+-interpose_list-| host1 |
| |-child_hosts-+----->| vtable: $link |
+--------------+ | | +----------------+
| |
| | +----------------+
+-interpose_list-| host2 |
+----->| vtable: $link |
+----------------+
When a child defines its own hook, the child_hosts link is
severed and replaced with a child_next pointer to the new
hook. The hook still points back the chain with prev.
All child links to subclasses of host2 are migrated from
link1 to link2.
+--------------+-next=NULL +--------------+-next=NULL
| link1 @host1 |-child_next------->| link2 @host2 |-child_*--->subclasses
| |<-------------prev-|s_c: $link1 |
+--------------+<-------+ +--------------+<-------+
| |
+--------------+ | +--------------+ |
| host1 |-i_list-+ | host2 |-i_list-+
|vtable: $link1| |vtable: $link2|
+--------------+ +--------------+
*/
void VMethodInterposeLinkBase::set_chain(void *chain)
{
saved_chain = chain;
@ -162,7 +249,7 @@ void VMethodInterposeLinkBase::set_chain(void *chain)
VMethodInterposeLinkBase::VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr)
: host(host), vmethod_idx(vmethod_idx), interpose_method(interpose_method), chain_mptr(chain_mptr),
saved_chain(NULL), next(NULL), prev(NULL)
applied(false), saved_chain(NULL), next(NULL), prev(NULL)
{
if (vmethod_idx < 0 || interpose_method == NULL)
{
@ -179,8 +266,83 @@ VMethodInterposeLinkBase::~VMethodInterposeLinkBase()
remove();
}
bool VMethodInterposeLinkBase::apply()
VMethodInterposeLinkBase *VMethodInterposeLinkBase::get_first_interpose(virtual_identity *id)
{
auto item = id->interpose_list[vmethod_idx];
if (!item)
return NULL;
if (item->host != id)
return NULL;
while (item->prev && item->prev->host == id)
item = item->prev;
return item;
}
void VMethodInterposeLinkBase::find_child_hosts(virtual_identity *cur, void *vmptr)
{
auto &children = cur->getChildren();
for (size_t i = 0; i < children.size(); i++)
{
auto child = static_cast<virtual_identity*>(children[i]);
auto base = get_first_interpose(child);
if (base)
{
assert(base->prev == NULL);
if (base->saved_chain != vmptr)
continue;
child_next.insert(base);
}
else
{
void *cptr = child->get_vmethod_ptr(vmethod_idx);
if (cptr != vmptr)
continue;
child_hosts.insert(child);
find_child_hosts(child, vmptr);
}
}
}
void VMethodInterposeLinkBase::on_host_delete(virtual_identity *from)
{
if (from == host)
{
// When in own host, fully delete
remove();
}
else
{
// Otherwise, drop the link to that child:
assert(child_hosts.count(from) != 0 &&
from->interpose_list[vmethod_idx] == this);
// Find and restore the original vmethod ptr
auto last = this;
while (last->prev) last = last->prev;
from->set_vmethod_ptr(vmethod_idx, last->saved_chain);
// Unlink the chains
child_hosts.erase(from);
from->interpose_list[vmethod_idx] = NULL;
}
}
bool VMethodInterposeLinkBase::apply(bool enable)
{
if (!enable)
{
remove();
return true;
}
if (is_applied())
return true;
if (!host->vtable_ptr)
@ -188,33 +350,73 @@ bool VMethodInterposeLinkBase::apply()
// Retrieve the current vtable entry
void *old_ptr = host->get_vmethod_ptr(vmethod_idx);
assert(old_ptr != NULL);
// Check if there are other interpose entries for the same slot
VMethodInterposeLinkBase *old_link = NULL;
VMethodInterposeLinkBase *old_link = host->interpose_list[vmethod_idx];
for (int i = host->interpose_list.size()-1; i >= 0; i--)
{
if (host->interpose_list[i]->vmethod_idx != vmethod_idx)
continue;
old_link = host->interpose_list[i];
assert(old_link->next == NULL && old_ptr == old_link->interpose_method);
break;
}
assert(old_ptr != NULL && (!old_link || old_link->interpose_method == old_ptr));
// Apply the new method ptr
set_chain(old_ptr);
if (!host->set_vmethod_ptr(vmethod_idx, interpose_method))
{
set_chain(NULL);
return false;
}
set_chain(old_ptr);
host->interpose_list.push_back(this);
// Push the current link into the home host
applied = true;
host->interpose_list[vmethod_idx] = this;
prev = old_link;
// Link into the chain if any
if (old_link)
child_hosts.clear();
child_next.clear();
if (old_link && old_link->host == host)
{
// If the old link is home, just push into the plain chain
assert(old_link->next == NULL);
old_link->next = this;
prev = old_link;
// Child links belong to the topmost local entry
child_hosts.swap(old_link->child_hosts);
child_next.swap(old_link->child_next);
}
else
{
// If creating a new local chain, find children with same vmethod
find_child_hosts(host, old_ptr);
if (old_link)
{
// Enter the child chain set
assert(old_link->child_hosts.count(host));
old_link->child_hosts.erase(host);
old_link->child_next.insert(this);
// Subtract our own children from the parent's sets
for (auto it = child_next.begin(); it != child_next.end(); ++it)
old_link->child_next.erase(*it);
for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it)
old_link->child_hosts.erase(*it);
}
}
// Chain subclass hooks
for (auto it = child_next.begin(); it != child_next.end(); ++it)
{
auto nlink = *it;
assert(nlink->saved_chain == old_ptr && nlink->prev == old_link);
nlink->set_chain(interpose_method);
nlink->prev = this;
}
// Chain passive subclass hosts
for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it)
{
auto nhost = *it;
assert(nhost->interpose_list[vmethod_idx] == old_link);
nhost->set_vmethod_ptr(vmethod_idx, interpose_method);
nhost->interpose_list[vmethod_idx] = this;
}
return true;
@ -225,25 +427,57 @@ void VMethodInterposeLinkBase::remove()
if (!is_applied())
return;
// Remove from the list in the identity
for (int i = host->interpose_list.size()-1; i >= 0; i--)
if (host->interpose_list[i] == this)
vector_erase_at(host->interpose_list, i);
// Remove from the chain
// Remove the link from prev to this
if (prev)
prev->next = next;
{
if (prev->host == host)
prev->next = next;
else
{
prev->child_next.erase(this);
if (next)
prev->child_next.insert(next);
}
}
if (next)
{
next->set_chain(saved_chain);
next->prev = prev;
assert(child_next.empty() && child_hosts.empty());
}
else
{
// Remove from the list in the identity and vtable
host->interpose_list[vmethod_idx] = prev;
host->set_vmethod_ptr(vmethod_idx, saved_chain);
for (auto it = child_next.begin(); it != child_next.end(); ++it)
{
auto nlink = *it;
assert(nlink->saved_chain == interpose_method && nlink->prev == this);
nlink->set_chain(saved_chain);
nlink->prev = prev;
if (prev)
prev->child_next.insert(nlink);
}
for (auto it = child_hosts.begin(); it != child_hosts.end(); ++it)
{
auto nhost = *it;
assert(nhost->interpose_list[vmethod_idx] == this);
nhost->interpose_list[vmethod_idx] = prev;
nhost->set_vmethod_ptr(vmethod_idx, saved_chain);
if (prev)
prev->child_hosts.insert(nhost);
}
}
applied = false;
prev = next = NULL;
child_next.clear();
child_hosts.clear();
set_chain(NULL);
}

@ -64,7 +64,7 @@ namespace DFHack
if (newsize == size)
return;
uint8_t* mem = (uint8_t *) realloc(bits, newsize);
if(!mem)
if(!mem && newsize != 0)
throw std::bad_alloc();
bits = mem;
if (newsize > size)
@ -207,7 +207,7 @@ namespace DFHack
else
{
T* mem = (T*) realloc(m_data, sizeof(T)*new_size);
if(!mem)
if(!mem && new_size != 0)
throw std::bad_alloc();
m_data = mem;
}

@ -303,7 +303,7 @@ namespace DFHack
void *vtable_ptr;
friend class VMethodInterposeLinkBase;
std::vector<VMethodInterposeLinkBase*> interpose_list;
std::map<int,VMethodInterposeLinkBase*> interpose_list;
protected:
virtual void doInit(Core *core);

@ -390,7 +390,7 @@ namespace df
}
virtual bool resize(void *ptr, int size) {
((container*)ptr)->resize(size);
((container*)ptr)->resize(size*8);
return true;
}

@ -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);

@ -128,7 +128,9 @@ namespace DFHack
{
PS_UNLOADED,
PS_LOADED,
PS_BROKEN
PS_BROKEN,
PS_LOADING,
PS_UNLOADING
};
friend class PluginManager;
friend class RPCService;

@ -134,22 +134,32 @@ namespace DFHack
1) Allow multiple hooks into the same vmethod
2) Auto-remove hooks when a plugin is unloaded.
*/
friend class virtual_identity;
virtual_identity *host; // Class with the vtable
int vmethod_idx;
void *interpose_method; // Pointer to the code of the interposing method
void *chain_mptr; // Pointer to the chain field below
bool applied;
void *saved_chain; // Previous pointer to the code
VMethodInterposeLinkBase *next, *prev; // Other hooks for the same method
// inherited vtable members
std::set<virtual_identity*> child_hosts;
std::set<VMethodInterposeLinkBase*> child_next;
void set_chain(void *chain);
void on_host_delete(virtual_identity *host);
VMethodInterposeLinkBase *get_first_interpose(virtual_identity *id);
void find_child_hosts(virtual_identity *cur, void *vmptr);
public:
VMethodInterposeLinkBase(virtual_identity *host, int vmethod_idx, void *interpose_method, void *chain_mptr);
~VMethodInterposeLinkBase();
bool is_applied() { return saved_chain != NULL; }
bool apply();
bool is_applied() { return applied; }
bool apply(bool enable = true);
void remove();
};

@ -32,6 +32,7 @@ distribution.
#include "DataDefs.h"
#include "df/init.h"
#include "df/ui.h"
#include "df/announcement_type.h"
namespace df {
struct viewscreen;
@ -92,11 +93,32 @@ namespace DFHack
// Show a plain announcement, or a titan-style popup message
DFHACK_EXPORT void showAnnouncement(std::string message, int color = 7, bool bright = true);
DFHACK_EXPORT void showZoomAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true);
DFHACK_EXPORT void showPopupAnnouncement(std::string message, int color = 7, bool bright = true);
// Show an announcement with effects determined by announcements.txt
DFHACK_EXPORT void showAutoAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true);
/*
* Cursor and window coords
*/
DFHACK_EXPORT df::coord getViewportPos();
DFHACK_EXPORT df::coord getCursorPos();
static const int AREA_MAP_WIDTH = 23;
static const int MENU_WIDTH = 30;
struct DwarfmodeDims {
int map_x1, map_x2, menu_x1, menu_x2, area_x1, area_x2;
int y1, y2;
bool menu_on, area_on, menu_forced;
};
DFHACK_EXPORT DwarfmodeDims getDwarfmodeViewDims();
DFHACK_EXPORT void resetDwarfmodeView(bool pause = false);
DFHACK_EXPORT bool revealInDwarfmodeMap(df::coord pos, bool center = false);
DFHACK_EXPORT bool getViewCoords (int32_t &x, int32_t &y, int32_t &z);
DFHACK_EXPORT bool setViewCoords (const int32_t x, const int32_t y, const int32_t z);

@ -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);

@ -50,6 +50,7 @@ distribution.
#include "df/tile_dig_designation.h"
#include "df/tile_traffic.h"
#include "df/feature_init.h"
#include "df/flow_type.h"
/**
* \defgroup grp_maps Maps module and its types
@ -232,14 +233,19 @@ extern DFHACK_EXPORT void getSize(uint32_t& x, uint32_t& y, uint32_t& z);
/// get the position of the map on world map
extern DFHACK_EXPORT void getPosition(int32_t& x, int32_t& y, int32_t& z);
extern DFHACK_EXPORT bool isValidTilePos(int32_t x, int32_t y, int32_t z);
inline bool isValidTilePos(df::coord pos) { return isValidTilePos(pos.x, pos.y, pos.z); }
/**
* Get the map block or NULL if block is not valid
*/
extern DFHACK_EXPORT df::map_block * getBlock (int32_t blockx, int32_t blocky, int32_t blockz);
extern DFHACK_EXPORT df::map_block * getTileBlock (int32_t x, int32_t y, int32_t z);
extern DFHACK_EXPORT df::map_block * ensureTileBlock (int32_t x, int32_t y, int32_t z);
inline df::map_block * getBlock (df::coord pos) { return getBlock(pos.x, pos.y, pos.z); }
inline df::map_block * getTileBlock (df::coord pos) { return getTileBlock(pos.x, pos.y, pos.z); }
inline df::map_block * ensureTileBlock (df::coord pos) { return ensureTileBlock(pos.x, pos.y, pos.z); }
extern DFHACK_EXPORT df::tiletype *getTileType(int32_t x, int32_t y, int32_t z);
extern DFHACK_EXPORT df::tile_designation *getTileDesignation(int32_t x, int32_t y, int32_t z);
@ -258,7 +264,7 @@ inline df::tile_occupancy *getTileOccupancy(df::coord pos) {
/**
* Returns biome info about the specified world region.
*/
DFHACK_EXPORT df::world_data::T_region_map *getRegionBiome(df::coord2d rgn_pos);
DFHACK_EXPORT df::region_map_entry *getRegionBiome(df::coord2d rgn_pos);
/**
* Returns biome world region coordinates for the given tile within given block.
@ -272,6 +278,8 @@ inline df::coord2d getTileBiomeRgn(df::coord pos) {
// Enables per-frame updates for liquid flow and/or temperature.
DFHACK_EXPORT void enableBlockUpdates(df::map_block *blk, bool flow = false, bool temperature = false);
DFHACK_EXPORT df::flow_info *spawnFlow(df::coord pos, df::flow_type type, int mat_type = 0, int mat_index = -1, int density = 100);
/// sorts the block event vector into multiple vectors by type
/// mineral veins, what's under ice, blood smears and mud
extern DFHACK_EXPORT bool SortBlockEvents(df::map_block *block,

@ -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);

@ -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;

@ -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

@ -18,7 +18,7 @@ function simulateInput(screen,...)
error('Invalid keycode: '..arg)
end
end
if type(arg) == 'number' then
if type(kv) == 'number' then
keys[#keys+1] = kv
end
end
@ -94,6 +94,9 @@ function Painter:isValidPos()
end
function Painter:viewport(x,y,w,h)
if type(x) == 'table' then
x,y,w,h = x.x1, x.y1, x.width, x.height
end
local x1,y1 = self.x1+x, self.y1+y
local x2,y2 = x1+w-1, y1+h-1
local vp = {
@ -159,10 +162,10 @@ function Painter:fill(x1,y1,x2,y2,pen,bg,bold)
if type(x1) == 'table' then
x1, y1, x2, y2, pen, bg, bold = x1.x1, x1.y1, x1.x2, x1.y2, y1, x2, y2
end
x1 = math.max(x1,self.clip_x1)
y1 = math.max(y1,self.clip_y1)
x2 = math.min(x2,self.clip_x2)
y2 = math.min(y2,self.clip_y2)
x1 = math.max(x1+self.x1,self.clip_x1)
y1 = math.max(y1+self.y1,self.clip_y1)
x2 = math.min(x2+self.x1,self.clip_x2)
y2 = math.min(y2+self.y1,self.clip_y2)
dscreen.fillRect(to_pen(self.cur_pen,pen,bg,bold),x1,y1,x2,y2)
return self
end
@ -277,6 +280,9 @@ end
function Screen:onDismiss()
end
function Screen:onDestroy()
end
function Screen:onResize(w,h)
self:updateLayout()
end
@ -350,11 +356,16 @@ local function hint_coord(gap,hint)
end
end
function FramedScreen:getWantedFrameSize()
return self.frame_width, self.frame_height
end
function FramedScreen:updateFrameSize()
local sw, sh = dscreen.getWindowSize()
local iw, ih = sw-2, sh-2
local width = math.min(self.frame_width or iw, iw)
local height = math.min(self.frame_height or ih, ih)
local fw, fh = self:getWantedFrameSize()
local width = math.min(fw or iw, iw)
local height = math.min(fh or ih, ih)
local gw, gh = iw-width, ih-height
local x1, y1 = hint_coord(gw,self.frame_xhint), hint_coord(gh,self.frame_yhint)
self.frame_rect = mkdims_wh(x1+1,y1+1,width,height)

@ -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

@ -6,6 +6,9 @@ local gui = require('gui')
local utils = require('utils')
local dscreen = dfhack.screen
local g_cursor = df.global.cursor
local g_sel_rect = df.global.selection_rect
local world_map = df.global.world.map
AREA_MAP_WIDTH = 23
@ -43,8 +46,8 @@ function getPanelLayout()
end
function getCursorPos()
if df.global.cursor.x ~= -30000 then
return copyall(df.global.cursor)
if g_cursor.x ~= -30000 then
return copyall(g_cursor)
end
end
@ -56,6 +59,51 @@ function clearCursorPos()
df.global.cursor = xyz2pos(nil)
end
function getSelection()
local p1, p2
if g_sel_rect.start_x ~= -30000 then
p1 = xyz2pos(g_sel_rect.start_x, g_sel_rect.start_y, g_sel_rect.start_z)
end
if g_sel_rect.end_x ~= -30000 then
p2 = xyz2pos(g_sel_rect.end_x, g_sel_rect.end_y, g_sel_rect.end_z)
end
return p1, p2
end
function setSelectionStart(pos)
g_sel_rect.start_x = pos.x
g_sel_rect.start_y = pos.y
g_sel_rect.start_z = pos.z
end
function setSelectionEnd(pos)
g_sel_rect.end_x = pos.x
g_sel_rect.end_y = pos.y
g_sel_rect.end_z = pos.z
end
function clearSelection()
g_sel_rect.start_x = -30000
g_sel_rect.start_y = -30000
g_sel_rect.start_z = -30000
g_sel_rect.end_x = -30000
g_sel_rect.end_y = -30000
g_sel_rect.end_z = -30000
end
function getSelectionRange(p1, p2)
local r1 = xyz2pos(
math.min(p1.x, p2.x), math.min(p1.y, p2.y), math.min(p1.z, p2.z)
)
local r2 = xyz2pos(
math.max(p1.x, p2.x), math.max(p1.y, p2.y), math.max(p1.z, p2.z)
)
local sz = xyz2pos(
r2.x - r1.x + 1, r2.y - r1.y + 1, r2.z - r1.z + 1
)
return r1, sz, r2
end
Viewport = defclass(Viewport)
function Viewport.make(map,x,y,z)
@ -88,6 +136,14 @@ function Viewport:set()
return vp
end
function Viewport:getPos()
return xyz2pos(self.x1, self.y1, self.z)
end
function Viewport:getSize()
return xy2pos(self.width, self.height)
end
function Viewport:clip(x,y,z)
return self:make(
math.max(0, math.min(x or self.x1, world_map.x_count-self.width)),
@ -111,6 +167,18 @@ function Viewport:isVisible(target,gap)
return self:isVisibleXY(target,gap) and target.z == self.z
end
function Viewport:tileToScreen(coord)
return xyz2pos(coord.x - self.x1, coord.y - self.y1, coord.z - self.z)
end
function Viewport:getCenter()
return xyz2pos(
math.floor((self.x2+self.x1)/2),
math.floor((self.y2+self.y1)/2),
self.z
)
end
function Viewport:centerOn(target)
return self:clip(
target.x - math.floor(self.width/2),
@ -159,16 +227,24 @@ MOVEMENT_KEYS = {
CURSOR_UP_Z_AUX = { 0, 0, 1 }, CURSOR_DOWN_Z_AUX = { 0, 0, -1 },
}
function Viewport:scrollByKey(key)
local function get_movement_delta(key, delta, big_step)
local info = MOVEMENT_KEYS[key]
if info then
local delta = 10
if info[4] then delta = 20 end
if info[4] then
delta = big_step
end
return delta*info[1], delta*info[2], info[3]
end
end
function Viewport:scrollByKey(key)
local dx, dy, dz = get_movement_delta(key, 10, 20)
if dx then
return self:clip(
self.x1 + delta*info[1],
self.y1 + delta*info[2],
self.z + info[3]
self.x1 + dx,
self.y1 + dy,
self.z + dz
)
else
return self
@ -189,16 +265,23 @@ function DwarfOverlay:getViewport(old_vp)
end
end
function DwarfOverlay:moveCursorTo(cursor,viewport)
function DwarfOverlay:moveCursorTo(cursor,viewport,gap)
setCursorPos(cursor)
self:getViewport(viewport):reveal(cursor, 5, 0, 10):set()
self:zoomViewportTo(cursor,viewport,gap)
end
function DwarfOverlay:selectBuilding(building,cursor,viewport)
function DwarfOverlay:zoomViewportTo(target, viewport, gap)
if gap and self:getViewport():isVisible(target, gap) then
return
end
self:getViewport(viewport):reveal(target, 5, 0, 10):set()
end
function DwarfOverlay:selectBuilding(building,cursor,viewport,gap)
cursor = cursor or utils.getBuildingCenter(building)
df.global.world.selected_building = building
self:moveCursorTo(cursor, viewport)
self:moveCursorTo(cursor, viewport, gap)
end
function DwarfOverlay:propagateMoveKeys(keys)
@ -234,6 +317,31 @@ function DwarfOverlay:simulateViewScroll(keys, anchor, no_clip_cursor)
end
end
function DwarfOverlay:simulateCursorMovement(keys, anchor)
local layout = self.df_layout
local cursor = getCursorPos()
local cx, cy, cz = pos2xyz(cursor)
if anchor and keys.A_MOVE_SAME_SQUARE then
setCursorPos(anchor)
self:getViewport():centerOn(anchor):set()
return 'A_MOVE_SAME_SQUARE'
end
for code,_ in pairs(MOVEMENT_KEYS) do
if keys[code] then
local dx, dy, dz = get_movement_delta(code, 1, 10)
local ncur = xyz2pos(cx+dx, cy+dy, cz+dz)
if dfhack.maps.isValidTilePos(ncur) then
setCursorPos(ncur)
self:getViewport():reveal(ncur,4,10,6,true):set()
return code
end
end
end
end
function DwarfOverlay:onAboutToShow(below)
local screen = dfhack.gui.getCurViewscreen()
if below then screen = below.parent end

@ -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

@ -324,7 +324,7 @@ df::building *Buildings::allocInstance(df::coord pos, df::building_type type, in
{
auto obj = (df::building_trapst*)bld;
if (obj->trap_type == trap_type::PressurePlate)
obj->unk_cc = 500;
obj->ready_timeout = 500;
break;
}
default:

@ -43,6 +43,7 @@ using namespace DFHack;
#include "modules/Job.h"
#include "modules/Screen.h"
#include "modules/Maps.h"
#include "DataDefs.h"
#include "df/world.h"
@ -81,6 +82,8 @@ using namespace DFHack;
#include "df/graphic.h"
#include "df/layer_object_listst.h"
#include "df/assign_trade_status.h"
#include "df/announcement_flags.h"
#include "df/announcements.h"
using namespace df::enums;
using df::global::gview;
@ -88,6 +91,9 @@ using df::global::init;
using df::global::gps;
using df::global::ui;
using df::global::world;
using df::global::selection_rect;
using df::global::ui_menu_width;
using df::global::ui_area_map_width;
static df::layer_object_listst *getLayerList(df::viewscreen_layerst *layer, int idx)
{
@ -167,10 +173,9 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
else if (id == &df::building_trapst::_identity)
{
auto trap = (df::building_trapst*)selected;
if (trap->trap_type == trap_type::Lever) {
focus += "/Lever";
focus += "/" + enum_item_key(trap->trap_type);
if (trap->trap_type == trap_type::Lever)
jobs = true;
}
}
else if (ui_building_in_assign && *ui_building_in_assign &&
ui_building_assign_type && ui_building_assign_units &&
@ -183,6 +188,8 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
focus += unit ? "/Unit" : "/None";
}
}
else
focus += "/" + enum_item_key(selected->getType());
if (jobs)
{
@ -205,7 +212,14 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode)
if (ui_build_selector->building_type < 0)
focus += "/Type";
else if (ui_build_selector->stage != 2)
focus += "/Position";
{
if (ui_build_selector->stage != 1)
focus += "/NoMaterials";
else
focus += "/Position";
focus += "/" + enum_item_key(ui_build_selector->building_type);
}
else
{
focus += "/Material";
@ -921,8 +935,9 @@ df::item *Gui::getSelectedItem(color_ostream &out, bool quiet)
//
void Gui::showAnnouncement(std::string message, int color, bool bright)
{
static void doShowAnnouncement(
df::announcement_type type, df::coord pos, std::string message, int color, bool bright
) {
using df::global::world;
using df::global::cur_year;
using df::global::cur_year_tick;
@ -948,6 +963,9 @@ void Gui::showAnnouncement(std::string message, int color, bool bright)
{
df::report *new_rep = new df::report();
new_rep->type = type;
new_rep->pos = pos;
new_rep->color = color;
new_rep->bright = bright;
new_rep->year = year;
@ -969,7 +987,17 @@ void Gui::showAnnouncement(std::string message, int color, bool bright)
world->status.announcements.push_back(new_rep);
world->status.display_timer = 2000;
}
}
void Gui::showAnnouncement(std::string message, int color, bool bright)
{
doShowAnnouncement(df::announcement_type(0), df::coord(), message, color, bright);
}
void Gui::showZoomAnnouncement(
df::announcement_type type, df::coord pos, std::string message, int color, bool bright
) {
doShowAnnouncement(type, pos, message, color, bright);
}
void Gui::showPopupAnnouncement(std::string message, int color, bool bright)
@ -983,6 +1011,29 @@ void Gui::showPopupAnnouncement(std::string message, int color, bool bright)
world->status.popups.push_back(popup);
}
void Gui::showAutoAnnouncement(
df::announcement_type type, df::coord pos, std::string message, int color, bool bright
) {
using df::global::announcements;
df::announcement_flags flags;
if (is_valid_enum_item(type) && announcements)
flags = announcements->flags[type];
doShowAnnouncement(type, pos, message, color, bright);
if (flags.bits.DO_MEGA || flags.bits.PAUSE || flags.bits.RECENTER)
{
resetDwarfmodeView(flags.bits.DO_MEGA || flags.bits.PAUSE);
if (flags.bits.RECENTER && pos.isValid())
revealInDwarfmodeMap(pos, true);
}
if (flags.bits.DO_MEGA)
showPopupAnnouncement(message, color, bright);
}
df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed)
{
df::viewscreen * ws = &gview->view;
@ -998,6 +1049,127 @@ df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed)
return ws;
}
df::coord Gui::getViewportPos()
{
if (!df::global::window_x || !df::global::window_y || !df::global::window_z)
return df::coord(0,0,0);
return df::coord(*df::global::window_x, *df::global::window_y, *df::global::window_z);
}
df::coord Gui::getCursorPos()
{
using df::global::cursor;
if (!cursor)
return df::coord();
return df::coord(cursor->x, cursor->y, cursor->z);
}
Gui::DwarfmodeDims Gui::getDwarfmodeViewDims()
{
DwarfmodeDims dims;
auto ws = Screen::getWindowSize();
dims.y1 = 1;
dims.y2 = ws.y-2;
dims.map_x1 = 1;
dims.map_x2 = ws.x-2;
dims.area_x1 = dims.area_x2 = dims.menu_x1 = dims.menu_x2 = -1;
dims.menu_forced = false;
int menu_pos = (ui_menu_width ? *ui_menu_width : 2);
int area_pos = (ui_area_map_width ? *ui_area_map_width : 3);
if (ui && ui->main.mode && menu_pos >= area_pos)
{
dims.menu_forced = true;
menu_pos = area_pos-1;
}
dims.area_on = (area_pos < 3);
dims.menu_on = (menu_pos < area_pos);
if (dims.menu_on)
{
dims.menu_x2 = ws.x - 2;
dims.menu_x1 = dims.menu_x2 - Gui::MENU_WIDTH + 1;
if (menu_pos == 1)
dims.menu_x1 -= Gui::AREA_MAP_WIDTH + 1;
dims.map_x2 = dims.menu_x1 - 2;
}
if (dims.area_on)
{
dims.area_x2 = ws.x-2;
dims.area_x1 = dims.area_x2 - Gui::AREA_MAP_WIDTH + 1;
if (dims.menu_on)
dims.menu_x2 = dims.area_x1 - 2;
else
dims.map_x2 = dims.area_x1 - 2;
}
return dims;
}
void Gui::resetDwarfmodeView(bool pause)
{
using df::global::cursor;
if (ui)
{
ui->follow_unit = -1;
ui->follow_item = -1;
ui->main.mode = ui_sidebar_mode::Default;
}
if (selection_rect)
{
selection_rect->start_x = -30000;
selection_rect->end_x = -30000;
}
if (cursor)
cursor->x = cursor->y = cursor->z = -30000;
if (pause && df::global::pause_state)
*df::global::pause_state = true;
}
bool Gui::revealInDwarfmodeMap(df::coord pos, bool center)
{
using df::global::window_x;
using df::global::window_y;
using df::global::window_z;
if (!window_x || !window_y || !window_z || !world)
return false;
if (!Maps::isValidTilePos(pos))
return false;
auto dims = getDwarfmodeViewDims();
int w = dims.map_x2 - dims.map_x1 + 1;
int h = dims.y2 - dims.y1 + 1;
*window_z = pos.z;
if (center)
{
*window_x = pos.x - w/2;
*window_y = pos.y - h/2;
}
else
{
while (*window_x + w < pos.x+5) *window_x += 10;
while (*window_y + h < pos.y+5) *window_y += 10;
while (*window_x + 5 > pos.x) *window_x -= 10;
while (*window_y + 5 > pos.y) *window_y -= 10;
}
*window_x = std::max(0, std::min(*window_x, world->map.x_count-w));
*window_y = std::max(0, std::min(*window_y, world->map.y_count-h));
return true;
}
bool Gui::getViewCoords (int32_t &x, int32_t &y, int32_t &z)
{
x = *df::global::window_x;

@ -57,6 +57,8 @@ using namespace std;
#include "df/builtin_mats.h"
#include "df/block_square_event_grassst.h"
#include "df/z_level_flags.h"
#include "df/region_map_entry.h"
#include "df/flow_info.h"
using namespace DFHack;
using namespace df::enums;
@ -137,17 +139,57 @@ df::map_block *Maps::getBlock (int32_t blockx, int32_t blocky, int32_t blockz)
return world->map.block_index[blockx][blocky][blockz];
}
df::map_block *Maps::getTileBlock (int32_t x, int32_t y, int32_t z)
bool Maps::isValidTilePos(int32_t x, int32_t y, int32_t z)
{
if (!IsValid())
return NULL;
return false;
if ((x < 0) || (y < 0) || (z < 0))
return NULL;
return false;
if ((x >= world->map.x_count) || (y >= world->map.y_count) || (z >= world->map.z_count))
return false;
return true;
}
df::map_block *Maps::getTileBlock (int32_t x, int32_t y, int32_t z)
{
if (!isValidTilePos(x,y,z))
return NULL;
return world->map.block_index[x >> 4][y >> 4][z];
}
df::map_block *Maps::ensureTileBlock (int32_t x, int32_t y, int32_t z)
{
if (!isValidTilePos(x,y,z))
return NULL;
auto column = world->map.block_index[x >> 4][y >> 4];
auto &slot = column[z];
if (slot)
return slot;
// Find another block below
int z2 = z;
while (z2 >= 0 && !column[z2]) z2--;
if (z2 < 0)
return NULL;
slot = new df::map_block();
slot->region_pos = column[z2]->region_pos;
slot->map_pos = column[z2]->map_pos;
slot->map_pos.z = z;
// Assume sky
df::tile_designation dsgn(0);
dsgn.bits.light = true;
dsgn.bits.outside = true;
for (int tx = 0; tx < 16; tx++)
for (int ty = 0; ty < 16; ty++)
slot->designation[tx][ty] = dsgn;
return slot;
}
df::tiletype *Maps::getTileType(int32_t x, int32_t y, int32_t z)
{
df::map_block *block = getTileBlock(x,y,z);
@ -166,7 +208,7 @@ df::tile_occupancy *Maps::getTileOccupancy(int32_t x, int32_t y, int32_t z)
return block ? &block->occupancy[x&15][y&15] : NULL;
}
df::world_data::T_region_map *Maps::getRegionBiome(df::coord2d rgn_pos)
df::region_map_entry *Maps::getRegionBiome(df::coord2d rgn_pos)
{
auto data = world->world_data;
if (!data)
@ -203,6 +245,26 @@ void Maps::enableBlockUpdates(df::map_block *blk, bool flow, bool temperature)
}
}
df::flow_info *Maps::spawnFlow(df::coord pos, df::flow_type type, int mat_type, int mat_index, int density)
{
using df::global::flows;
auto block = getTileBlock(pos);
if (!flows || !block)
return NULL;
auto flow = new df::flow_info();
flow->type = type;
flow->mat_type = mat_type;
flow->mat_index = mat_index;
flow->density = std::min(100, density);
flow->pos = pos;
block->flows.push_back(flow);
flows->push_back(flow);
return flow;
}
df::feature_init *Maps::getGlobalInitFeature(int32_t index)
{
auto data = world->world_data;
@ -484,8 +546,14 @@ MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent)
valid = false;
bcoord = _bcoord;
block = Maps::getBlock(bcoord);
item_counts = NULL;
tags = NULL;
init();
}
void MapExtras::Block::init()
{
item_counts = NULL;
tiles = NULL;
basemats = NULL;
@ -508,6 +576,23 @@ MapExtras::Block::Block(MapCache *parent, DFCoord _bcoord) : parent(parent)
}
}
bool MapExtras::Block::Allocate()
{
if (block)
return true;
block = Maps::ensureTileBlock(bcoord.x*16, bcoord.y*16, bcoord.z);
if (!block)
return false;
delete item_counts;
delete tiles;
delete basemats;
init();
return true;
}
MapExtras::Block::~Block()
{
delete[] item_counts;

@ -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;

@ -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)

@ -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);
}

@ -1 +1 @@
Subproject commit abcb667bc832048552d8cbc8f4830936f8b63399
Subproject commit d55f1cf43dd71d3abee724bfa88a0a401b4ccaa3

@ -44,6 +44,9 @@ endif()
install(DIRECTORY lua/
DESTINATION ${DFHACK_LUA_DESTINATION}/plugins
FILES_MATCHING PATTERN "*.lua")
install(DIRECTORY raw/
DESTINATION ${DFHACK_DATA_DESTINATION}/raw
FILES_MATCHING PATTERN "*.txt")
# Protobuf
FILE(GLOB PROJECT_PROTOS ${CMAKE_CURRENT_SOURCE_DIR}/proto/*.proto)
@ -81,7 +84,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(weather weather.cpp)
DFHACK_PLUGIN(colonies colonies.cpp)
DFHACK_PLUGIN(mode mode.cpp)
DFHACK_PLUGIN(liquids liquids.cpp Brushes.h)
DFHACK_PLUGIN(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua)
DFHACK_PLUGIN(tiletypes tiletypes.cpp Brushes.h)
DFHACK_PLUGIN(tubefill tubefill.cpp)
DFHACK_PLUGIN(autodump autodump.cpp)
@ -92,7 +95,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(seedwatch seedwatch.cpp)
DFHACK_PLUGIN(initflags initflags.cpp)
DFHACK_PLUGIN(stockpiles stockpiles.cpp)
DFHACK_PLUGIN(rename rename.cpp PROTOBUFS rename)
DFHACK_PLUGIN(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename)
DFHACK_PLUGIN(jobutils jobutils.cpp)
DFHACK_PLUGIN(workflow workflow.cpp)
DFHACK_PLUGIN(showmood showmood.cpp)
@ -114,6 +117,8 @@ if (BUILD_SUPPORTED)
# this one exports functions to lua
DFHACK_PLUGIN(burrows burrows.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(sort sort.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(steam-engine steam-engine.cpp)
DFHACK_PLUGIN(power-meter power-meter.cpp LINK_LIBRARIES lua)
# not yet. busy with other crud again...
#DFHACK_PLUGIN(versionosd versionosd.cpp)
endif()

@ -19,6 +19,7 @@
#include "df/world_data.h"
#include "df/world_geo_biome.h"
#include "df/world_geo_layer.h"
#include "df/region_map_entry.h"
using namespace DFHack;
using namespace df::enums;

@ -18,3 +18,7 @@ DFHACK_PLUGIN(stripcaged stripcaged.cpp)
DFHACK_PLUGIN(rprobe rprobe.cpp)
DFHACK_PLUGIN(nestboxes nestboxes.cpp)
DFHACK_PLUGIN(vshook vshook.cpp)
DFHACK_PLUGIN(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
IF(UNIX)
DFHACK_PLUGIN(ref-index ref-index.cpp)
ENDIF()

@ -0,0 +1,149 @@
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <modules/Gui.h>
#include <modules/Screen.h>
#include <vector>
#include <cstdio>
#include <stack>
#include <string>
#include <cmath>
#include <VTableInterpose.h>
#include "df/item.h"
#include "df/unit.h"
#include "df/world.h"
#include "df/general_ref_item.h"
#include "df/general_ref_unit.h"
using std::vector;
using std::string;
using std::stack;
using namespace DFHack;
using df::global::gps;
DFHACK_PLUGIN("ref-index");
#define global_id id
template<class T>
T get_from_global_id_vector(int32_t id, const std::vector<T> &vect, int32_t *cache)
{
size_t size = vect.size();
int32_t start=0;
int32_t end=(int32_t)size-1;
// Check the cached location. If it is a match, this provides O(1) lookup.
// Otherwise it works like one binsearch iteration.
if (size_t(*cache) < size)
{
T cptr = vect[*cache];
if (cptr->global_id == id)
return cptr;
if (cptr->global_id < id)
start = *cache+1;
else
end = *cache-1;
}
// Regular binsearch. The end check provides O(1) caching for missing item.
if (start <= end && vect[end]->global_id >= id)
{
do {
int32_t mid=(start+end)>>1;
T cptr=vect[mid];
if(cptr->global_id==id)
{
*cache = mid;
return cptr;
}
else if(cptr->global_id>id)end=mid-1;
else start=mid+1;
} while(start<=end);
}
*cache = end+1;
return NULL;
}
template<class T> T *find_object(int32_t id, int32_t *cache);
template<> df::item *find_object<df::item>(int32_t id, int32_t *cache) {
return get_from_global_id_vector(id, df::global::world->items.all, cache);
}
template<> df::unit *find_object<df::unit>(int32_t id, int32_t *cache) {
return get_from_global_id_vector(id, df::global::world->units.all, cache);
}
template<class T>
struct CachedRef {
int32_t id;
int32_t cache;
CachedRef(int32_t id = -1) : id(id), cache(-1) {}
T *target() { return find_object<T>(id, &cache); }
};
#ifdef LINUX_BUILD
struct item_hook : df::general_ref_item {
typedef df::general_ref_item interpose_base;
DEFINE_VMETHOD_INTERPOSE(df::item*, getItem, ())
{
// HUGE HACK: ASSUMES THERE ARE 4 USABLE BYTES AFTER THE OBJECT
// This actually is true with glibc allocator due to granularity.
return find_object<df::item>(item_id, 1+&item_id);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(item_hook, getItem);
struct unit_hook : df::general_ref_unit {
typedef df::general_ref_unit interpose_base;
DEFINE_VMETHOD_INTERPOSE(df::unit*, getUnit, ())
{
// HUGE HACK: ASSUMES THERE ARE 4 USABLE BYTES AFTER THE OBJECT
// This actually is true with glibc allocator due to granularity.
return find_object<df::unit>(unit_id, 1+&unit_id);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(unit_hook, getUnit);
command_result hook_refs(color_ostream &out, vector <string> & parameters)
{
auto &hook = INTERPOSE_HOOK(item_hook, getItem);
if (hook.is_applied())
{
hook.remove();
INTERPOSE_HOOK(unit_hook, getUnit).remove();
}
else
{
hook.apply();
INTERPOSE_HOOK(unit_hook, getUnit).apply();
}
if (hook.is_applied())
out.print("Hook is applied.\n");
else
out.print("Hook is not applied.\n");
return CR_OK;
}
#endif
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
#ifdef LINUX_BUILD
commands.push_back(PluginCommand("hook-refs","Inject O(1) cached lookup into general refs.",hook_refs));
#endif
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}

@ -27,6 +27,7 @@ using namespace std;
#include "df/world_region_details.h"
#include "df/world_geo_biome.h"
#include "df/world_geo_layer.h"
#include "df/region_map_entry.h"
#include "df/inclusion_type.h"
#include "df/viewscreen_choose_start_sitest.h"
@ -79,7 +80,7 @@ command_result rprobe (color_ostream &out, vector <string> & parameters)
if (parameters.size() == 2)
{
if (parameters[0] == "wet")
if (parameters[0] == "rai")
set_field = 0;
else if (parameters[0] == "veg")
set_field = 1;
@ -87,7 +88,7 @@ command_result rprobe (color_ostream &out, vector <string> & parameters)
set_field = 2;
else if (parameters[0] == "evi")
set_field = 3;
else if (parameters[0] == "hil")
else if (parameters[0] == "dra")
set_field = 4;
else if (parameters[0] == "sav")
set_field = 5;
@ -113,11 +114,11 @@ command_result rprobe (color_ostream &out, vector <string> & parameters)
{
coord2d rg = screen->biome_rgn[i];
df::world_data::T_region_map* rd = &data->region_map[rg.x][rg.y];
auto rd = &data->region_map[rg.x][rg.y];
if (set && i == to_set) {
if (set_field == 0)
rd->wetness = set_val;
rd->rainfall = set_val;
else if (set_field == 1)
rd->vegetation = set_val;
else if (set_field == 2)
@ -125,11 +126,11 @@ command_result rprobe (color_ostream &out, vector <string> & parameters)
else if (set_field == 3)
rd->evilness = set_val;
else if (set_field == 4)
rd->hilliness = set_val;
rd->drainage = set_val;
else if (set_field == 5)
rd->savagery = set_val;
else if (set_field == 6)
rd->saltiness = set_val;
rd->salinity = set_val;
}
out << i << ": x = " << rg.x << ", y = " << rg.y;
@ -140,13 +141,13 @@ command_result rprobe (color_ostream &out, vector <string> & parameters)
" landmass_id: " << rd->landmass_id <<
" flags: " << hex << rd->flags.as_int() << dec << endl;
out <<
"wet: " << rd->wetness << " " <<
"rai: " << rd->rainfall << " " <<
"veg: " << rd->vegetation << " " <<
"tem: " << rd->temperature << " " <<
"evi: " << rd->evilness << " " <<
"hil: " << rd->hilliness << " " <<
"dra: " << rd->drainage << " " <<
"sav: " << rd->savagery << " " <<
"sal: " << rd->saltiness;
"sal: " << rd->salinity;
int32_t *p = (int32_t *)rd;
int c = sizeof(*rd) / sizeof(int32_t);

File diff suppressed because it is too large Load Diff

@ -27,6 +27,7 @@
#include <set>
#include <cstdlib>
#include <sstream>
#include <memory>
using std::vector;
using std::string;
using std::endl;
@ -41,6 +42,7 @@ using std::set;
#include "modules/Gui.h"
#include "TileTypes.h"
#include "modules/MapCache.h"
#include "LuaTools.h"
#include "Brushes.h"
using namespace MapExtras;
using namespace DFHack;
@ -50,7 +52,6 @@ CommandHistory liquids_hist;
command_result df_liquids (color_ostream &out, vector <string> & parameters);
command_result df_liquids_here (color_ostream &out, vector <string> & parameters);
command_result df_liquids_execute (color_ostream &out);
DFHACK_PLUGIN("liquids");
@ -74,13 +75,79 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
return CR_OK;
}
// static stuff to be remembered between sessions
static string brushname = "point";
static string mode="magma";
static string flowmode="f+";
static string _setmode ="s.";
static unsigned int amount = 7;
static int width = 1, height = 1, z_levels = 1;
enum BrushType {
B_POINT, B_RANGE, B_BLOCK, B_COLUMN, B_FLOOD
};
static const char *brush_name[] = {
"point", "range", "block", "column", "flood", NULL
};
enum PaintMode {
P_WATER, P_MAGMA, P_OBSIDIAN, P_OBSIDIAN_FLOOR,
P_RIVER_SOURCE, P_FLOW_BITS, P_WCLEAN
};
static const char *paint_mode_name[] = {
"water", "magma", "obsidian", "obsidian_floor",
"riversource", "flowbits", "wclean", NULL
};
enum ModifyMode {
M_INC, M_KEEP, M_DEC
};
static const char *modify_mode_name[] = {
"+", ".", "-", NULL
};
enum PermaflowMode {
PF_KEEP, PF_NONE,
PF_NORTH, PF_SOUTH, PF_EAST, PF_WEST,
PF_NORTHEAST, PF_NORTHWEST, PF_SOUTHEAST, PF_SOUTHWEST
};
static const char *permaflow_name[] = {
".", "-", "N", "S", "E", "W",
"NE", "NW", "SE", "SW", NULL
};
#define X(name) tile_liquid_flow_dir::name
static const df::tile_liquid_flow_dir permaflow_id[] = {
X(none), X(none), X(north), X(south), X(east), X(west),
X(northeast), X(northwest), X(southeast), X(southwest)
};
#undef X
struct OperationMode {
BrushType brush;
PaintMode paint;
ModifyMode flowmode;
ModifyMode setmode;
PermaflowMode permaflow;
unsigned int amount;
df::coord size;
OperationMode() :
brush(B_POINT), paint(P_MAGMA),
flowmode(M_INC), setmode(M_KEEP), permaflow(PF_KEEP), amount(7),
size(1,1,1)
{}
} cur_mode;
command_result df_liquids_execute(color_ostream &out);
command_result df_liquids_execute(color_ostream &out, OperationMode &mode, df::coord pos);
static void print_prompt(std::ostream &str, OperationMode &cur_mode)
{
str <<"[" << paint_mode_name[cur_mode.paint] << ":" << brush_name[cur_mode.brush];
if (cur_mode.brush == B_RANGE)
str << "(w" << cur_mode.size.x << ":h" << cur_mode.size.y << ":z" << cur_mode.size.z << ")";
str << ":" << cur_mode.amount << ":f" << modify_mode_name[cur_mode.flowmode]
<< ":s" << modify_mode_name[cur_mode.setmode]
<< ":pf" << permaflow_name[cur_mode.permaflow]
<< "]";
}
command_result df_liquids (color_ostream &out_, vector <string> & parameters)
{
@ -117,10 +184,8 @@ command_result df_liquids (color_ostream &out_, vector <string> & parameters)
string input = "";
std::stringstream str;
str <<"[" << mode << ":" << brushname;
if (brushname == "range")
str << "(w" << width << ":h" << height << ":z" << z_levels << ")";
str << ":" << amount << ":" << flowmode << ":" << _setmode << "]#";
print_prompt(str, cur_mode);
str << "# ";
if(out.lineedit(str.str(),input,liquids_hist) == -1)
return CR_FAILURE;
liquids_hist.add(input);
@ -147,6 +212,10 @@ command_result df_liquids (color_ostream &out_, vector <string> & parameters)
<< "f+ - make the spawned liquid flow" << endl
<< "f. - don't change flow state (read state in flow mode)" << endl
<< "f- - make the spawned liquid static" << endl
<< "Permaflow (only for water):" << endl
<< "pf. - don't change permaflow state" << endl
<< "pf- - make the spawned liquid static" << endl
<< "pf[NS][EW] - make the spawned liquid permanently flow" << endl
<< "0-7 - set liquid amount" << endl
<< "Brush:" << endl
<< "point - single tile [p]" << endl
@ -168,38 +237,39 @@ command_result df_liquids (color_ostream &out_, vector <string> & parameters)
}
else if(command == "m")
{
mode = "magma";
cur_mode.paint = P_MAGMA;
}
else if(command == "o")
{
mode = "obsidian";
cur_mode.paint = P_OBSIDIAN;
}
else if(command == "of")
{
mode = "obsidian_floor";
cur_mode.paint = P_OBSIDIAN_FLOOR;
}
else if(command == "w")
{
mode = "water";
cur_mode.paint = P_WATER;
}
else if(command == "f")
{
mode = "flowbits";
cur_mode.paint = P_FLOW_BITS;
}
else if(command == "rs")
{
mode = "riversource";
cur_mode.paint = P_RIVER_SOURCE;
}
else if(command == "wclean")
{
mode = "wclean";
cur_mode.paint = P_WCLEAN;
}
else if(command == "point" || command == "p")
{
brushname = "point";
cur_mode.brush = B_POINT;
}
else if(command == "range" || command == "r")
{
int width, height, z_levels;
command_result res = parseRectangle(out, commands, 1, commands.size(),
width, height, z_levels);
if (res != CR_OK)
@ -209,24 +279,26 @@ command_result df_liquids (color_ostream &out_, vector <string> & parameters)
if (width == 1 && height == 1 && z_levels == 1)
{
brushname = "point";
cur_mode.brush = B_POINT;
cur_mode.size = df::coord(1, 1, 1);
}
else
{
brushname = "range";
cur_mode.brush = B_RANGE;
cur_mode.size = df::coord(width, height, z_levels);
}
}
else if(command == "block")
{
brushname = "block";
cur_mode.brush = B_BLOCK;
}
else if(command == "column")
{
brushname = "column";
cur_mode.brush = B_COLUMN;
}
else if(command == "flood")
{
brushname = "flood";
cur_mode.brush = B_FLOOD;
}
else if(command == "q")
{
@ -234,45 +306,59 @@ command_result df_liquids (color_ostream &out_, vector <string> & parameters)
}
else if(command == "f+")
{
flowmode = "f+";
cur_mode.flowmode = M_INC;
}
else if(command == "f-")
{
flowmode = "f-";
cur_mode.flowmode = M_DEC;
}
else if(command == "f.")
{
flowmode = "f.";
cur_mode.flowmode = M_KEEP;
}
else if(command == "s+")
{
_setmode = "s+";
cur_mode.setmode = M_INC;
}
else if(command == "s-")
{
_setmode = "s-";
cur_mode.setmode = M_DEC;
}
else if(command == "s.")
{
_setmode = "s.";
cur_mode.setmode = M_KEEP;
}
else if (command.size() > 2 && memcmp(command.c_str(), "pf", 2) == 0)
{
auto *tail = command.c_str()+2;
for (int pm = PF_KEEP; pm <= PF_SOUTHWEST; pm++)
{
if (strcmp(tail, permaflow_name[pm]) != 0)
continue;
cur_mode.permaflow = PermaflowMode(pm);
tail = NULL;
break;
}
if (tail)
out << command << " : invalid permaflow mode" << endl;
}
// blah blah, bad code, bite me.
else if(command == "0")
amount = 0;
cur_mode.amount = 0;
else if(command == "1")
amount = 1;
cur_mode.amount = 1;
else if(command == "2")
amount = 2;
cur_mode.amount = 2;
else if(command == "3")
amount = 3;
cur_mode.amount = 3;
else if(command == "4")
amount = 4;
cur_mode.amount = 4;
else if(command == "5")
amount = 5;
cur_mode.amount = 5;
else if(command == "6")
amount = 6;
cur_mode.amount = 6;
else if(command == "7")
amount = 7;
cur_mode.amount = 7;
else if(command.empty())
{
df_liquids_execute(out);
@ -298,78 +384,75 @@ command_result df_liquids_here (color_ostream &out, vector <string> & parameters
}
out.print("Run liquids-here with these parameters: ");
out << "[" << mode << ":" << brushname;
if (brushname == "range")
out << "(w" << width << ":h" << height << ":z" << z_levels << ")";
out << ":" << amount << ":" << flowmode << ":" << _setmode << "]\n";
print_prompt(out, cur_mode);
out << endl;
return df_liquids_execute(out);
}
command_result df_liquids_execute(color_ostream &out)
{
// create brush type depending on old parameters
Brush * brush;
CoreSuspender suspend;
if (brushname == "point")
auto cursor = Gui::getCursorPos();
if (!cursor.isValid())
{
brush = new RectangleBrush(1,1,1,0,0,0);
//width = 1;
//height = 1;
//z_levels = 1;
out.printerr("Can't get cursor coords! Make sure you have a cursor active in DF.\n");
return CR_WRONG_USAGE;
}
else if (brushname == "range")
{
brush = new RectangleBrush(width,height,z_levels,0,0,0);
}
else if(brushname == "block")
auto rv = df_liquids_execute(out, cur_mode, cursor);
if (rv == CR_OK)
out << "OK" << endl;
return rv;
}
command_result df_liquids_execute(color_ostream &out, OperationMode &cur_mode, df::coord cursor)
{
// create brush type depending on old parameters
Brush *brush;
switch (cur_mode.brush)
{
case B_POINT:
brush = new RectangleBrush(1,1,1,0,0,0);
break;
case B_RANGE:
brush = new RectangleBrush(cur_mode.size.x,cur_mode.size.y,cur_mode.size.z,0,0,0);
break;
case B_BLOCK:
brush = new BlockBrush();
}
else if(brushname == "column")
{
break;
case B_COLUMN:
brush = new ColumnBrush();
}
else if(brushname == "flood")
{
break;
case B_FLOOD:
brush = new FloodBrush(&Core::getInstance());
}
else
{
break;
default:
// this should never happen!
out << "Old brushtype is invalid! Resetting to point brush.\n";
brushname = "point";
width = 1;
height = 1;
z_levels = 1;
brush = new RectangleBrush(width,height,z_levels,0,0,0);
cur_mode.brush = B_POINT;
brush = new RectangleBrush(1,1,1,0,0,0);
}
CoreSuspender suspend;
std::auto_ptr<Brush> brush_ref(brush);
do
if (!Maps::IsValid())
{
if (!Maps::IsValid())
{
out << "Can't see any DF map loaded." << endl;
break;;
}
int32_t x,y,z;
if(!Gui::getCursorCoords(x,y,z))
{
out << "Can't get cursor coords! Make sure you have a cursor active in DF." << endl;
break;
}
out << "cursor coords: " << x << "/" << y << "/" << z << endl;
MapCache mcache;
DFHack::DFCoord cursor(x,y,z);
coord_vec all_tiles = brush->points(mcache,cursor);
out << "working..." << endl;
out << "Can't see any DF map loaded." << endl;
return CR_FAILURE;
}
// Force the game to recompute its walkability cache
df::global::world->reindex_pathfinding = true;
MapCache mcache;
coord_vec all_tiles = brush->points(mcache,cursor);
if(mode == "obsidian")
// Force the game to recompute its walkability cache
df::global::world->reindex_pathfinding = true;
switch (cur_mode.paint)
{
case P_OBSIDIAN:
{
coord_vec::iterator iter = all_tiles.begin();
while (iter != all_tiles.end())
@ -383,8 +466,9 @@ command_result df_liquids_execute(color_ostream &out)
mcache.setDesignationAt(*iter, des);
iter ++;
}
break;
}
if(mode == "obsidian_floor")
case P_OBSIDIAN_FLOOR:
{
coord_vec::iterator iter = all_tiles.begin();
while (iter != all_tiles.end())
@ -392,8 +476,9 @@ command_result df_liquids_execute(color_ostream &out)
mcache.setTiletypeAt(*iter, findRandomVariant(tiletype::LavaFloor1));
iter ++;
}
break;
}
else if(mode == "riversource")
case P_RIVER_SOURCE:
{
coord_vec::iterator iter = all_tiles.begin();
while (iter != all_tiles.end())
@ -413,8 +498,9 @@ command_result df_liquids_execute(color_ostream &out)
iter++;
}
break;
}
else if(mode=="wclean")
case P_WCLEAN:
{
coord_vec::iterator iter = all_tiles.begin();
while (iter != all_tiles.end())
@ -426,8 +512,11 @@ command_result df_liquids_execute(color_ostream &out)
mcache.setDesignationAt(current,des);
iter++;
}
break;
}
else if(mode== "magma" || mode== "water" || mode == "flowbits")
case P_MAGMA:
case P_WATER:
case P_FLOW_BITS:
{
set <Block *> seen_blocks;
coord_vec::iterator iter = all_tiles.begin();
@ -442,6 +531,7 @@ command_result df_liquids_execute(color_ostream &out)
iter ++;
continue;
}
auto raw_block = block->getRaw();
df::tile_designation des = mcache.designationAt(current);
df::tiletype tt = mcache.tiletypeAt(current);
// don't put liquids into places where they don't belong...
@ -450,30 +540,29 @@ command_result df_liquids_execute(color_ostream &out)
iter++;
continue;
}
if(mode != "flowbits")
if(cur_mode.paint != P_FLOW_BITS)
{
unsigned old_amount = des.bits.flow_size;
unsigned new_amount = old_amount;
df::tile_liquid old_liquid = des.bits.liquid_type;
df::tile_liquid new_liquid = old_liquid;
// Compute new liquid type and amount
if(_setmode == "s.")
{
new_amount = amount;
}
else if(_setmode == "s+")
{
if(old_amount < amount)
new_amount = amount;
}
else if(_setmode == "s-")
switch (cur_mode.setmode)
{
if (old_amount > amount)
new_amount = amount;
case M_KEEP:
new_amount = cur_mode.amount;
break;
case M_INC:
if(old_amount < cur_mode.amount)
new_amount = cur_mode.amount;
break;
case M_DEC:
if (old_amount > cur_mode.amount)
new_amount = cur_mode.amount;
}
if (mode == "magma")
if (cur_mode.paint == P_MAGMA)
new_liquid = tile_liquid::Magma;
else if (mode == "water")
else if (cur_mode.paint == P_WATER)
new_liquid = tile_liquid::Water;
// Store new amount and type
des.bits.flow_size = new_amount;
@ -502,40 +591,77 @@ command_result df_liquids_execute(color_ostream &out)
// request flow engine updates
block->enableBlockUpdates(new_amount != old_amount, new_liquid != old_liquid);
}
if (cur_mode.permaflow != PF_KEEP && raw_block)
{
auto &flow = raw_block->liquid_flow[current.x&15][current.y&15];
flow.bits.perm_flow_dir = permaflow_id[cur_mode.permaflow];
flow.bits.temp_flow_timer = 0;
}
seen_blocks.insert(block);
iter++;
}
set <Block *>::iterator biter = seen_blocks.begin();
while (biter != seen_blocks.end())
{
if(flowmode == "f+")
switch (cur_mode.flowmode)
{
case M_INC:
(*biter)->enableBlockUpdates(true);
}
else if(flowmode == "f-")
{
break;
case M_DEC:
if (auto block = (*biter)->getRaw())
{
block->flags.bits.update_liquid = false;
block->flags.bits.update_liquid_twice = false;
}
}
else
{
auto bflags = (*biter)->BlockFlags();
out << "flow bit 1 = " << bflags.bits.update_liquid << endl;
out << "flow bit 2 = " << bflags.bits.update_liquid_twice << endl;
break;
case M_KEEP:
{
auto bflags = (*biter)->BlockFlags();
out << "flow bit 1 = " << bflags.bits.update_liquid << endl;
out << "flow bit 2 = " << bflags.bits.update_liquid_twice << endl;
}
}
biter ++;
}
break;
}
if(mcache.WriteAll())
out << "OK" << endl;
else
out << "Something failed horribly! RUN!" << endl;
} while (0);
}
if(!mcache.WriteAll())
{
out << "Something failed horribly! RUN!" << endl;
return CR_FAILURE;
}
// cleanup
delete brush;
return CR_OK;
}
static int paint(lua_State *L)
{
df::coord pos;
OperationMode mode;
lua_settop(L, 8);
Lua::CheckDFAssign(L, &pos, 1);
if (!pos.isValid())
luaL_argerror(L, 1, "invalid cursor position");
mode.brush = (BrushType)luaL_checkoption(L, 2, NULL, brush_name);
mode.paint = (PaintMode)luaL_checkoption(L, 3, NULL, paint_mode_name);
mode.amount = luaL_optint(L, 4, 7);
if (mode.amount < 0 || mode.amount > 7)
luaL_argerror(L, 4, "invalid liquid amount");
if (!lua_isnil(L, 5))
Lua::CheckDFAssign(L, &mode.size, 5);
mode.setmode = (ModifyMode)luaL_checkoption(L, 6, ".", modify_mode_name);
mode.flowmode = (ModifyMode)luaL_checkoption(L, 7, "+", modify_mode_name);
mode.permaflow = (PermaflowMode)luaL_checkoption(L, 8, ".", permaflow_name);
lua_pushboolean(L, df_liquids_execute(*Lua::GetOutput(L), mode, pos));
return 1;
}
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(paint),
DFHACK_LUA_END
};

@ -0,0 +1,11 @@
local _ENV = mkmodule('plugins.liquids')
--[[
Native functions:
* paint(pos,brush,paint,amount,size,setmode,flowmode)
--]]
return _ENV

@ -0,0 +1,11 @@
local _ENV = mkmodule('plugins.power-meter')
--[[
Native functions:
* makePowerMeter(plate_info,min_power,max_power,invert)
--]]
return _ENV

@ -0,0 +1,13 @@
local _ENV = mkmodule('plugins.rename')
--[[
Native functions:
* canRenameBuilding(building)
* isRenamingBuilding(building)
* renameBuilding(building, name)
--]]
return _ENV

@ -0,0 +1,13 @@
local _ENV = mkmodule('plugins.siege-engine')
--[[
Native functions:
* getTargetArea(building) -> point1, point2
* clearTargetArea(building)
* setTargetArea(building, point1, point2) -> true/false
--]]
return _ENV

@ -11,6 +11,7 @@
#include <vector>
#include <string>
#include <set>
#include <algorithm>
#include <VTableInterpose.h>
#include "df/world.h"
@ -22,6 +23,7 @@
#include "df/unit.h"
#include "df/unit_soul.h"
#include "df/unit_skill.h"
#include "df/creature_graphics_role.h"
#include "df/creature_raw.h"
#include "df/caste_raw.h"
@ -37,204 +39,204 @@ using df::global::ui;
using df::global::gps;
using df::global::enabler;
DFHACK_PLUGIN("manipulator");
struct SkillLevel
{
const char *name;
int points;
char abbrev;
const char *name;
int points;
char abbrev;
};
#define NUM_SKILL_LEVELS (sizeof(skill_levels) / sizeof(SkillLevel))
#define NUM_SKILL_LEVELS (sizeof(skill_levels) / sizeof(SkillLevel))
// The various skill rankings. Zero skill is hardcoded to "Not" and '-'.
const SkillLevel skill_levels[] = {
{"Dabbling", 500, '0'},
{"Novice", 600, '1'},
{"Adequate", 700, '2'},
{"Competent", 800, '3'},
{"Skilled", 900, '4'},
{"Proficient", 1000, '5'},
{"Talented", 1100, '6'},
{"Adept", 1200, '7'},
{"Expert", 1300, '8'},
{"Professional",1400, '9'},
{"Accomplished",1500, 'A'},
{"Great", 1600, 'B'},
{"Master", 1700, 'C'},
{"High Master", 1800, 'D'},
{"Grand Master",1900, 'E'},
{"Legendary", 2000, 'U'},
{"Legendary+1", 2100, 'V'},
{"Legendary+2", 2200, 'W'},
{"Legendary+3", 2300, 'X'},
{"Legendary+4", 2400, 'Y'},
{"Legendary+5", 0, 'Z'}
{"Dabbling", 500, '0'},
{"Novice", 600, '1'},
{"Adequate", 700, '2'},
{"Competent", 800, '3'},
{"Skilled", 900, '4'},
{"Proficient", 1000, '5'},
{"Talented", 1100, '6'},
{"Adept", 1200, '7'},
{"Expert", 1300, '8'},
{"Professional",1400, '9'},
{"Accomplished",1500, 'A'},
{"Great", 1600, 'B'},
{"Master", 1700, 'C'},
{"High Master", 1800, 'D'},
{"Grand Master",1900, 'E'},
{"Legendary", 2000, 'U'},
{"Legendary+1", 2100, 'V'},
{"Legendary+2", 2200, 'W'},
{"Legendary+3", 2300, 'X'},
{"Legendary+4", 2400, 'Y'},
{"Legendary+5", 0, 'Z'}
};
struct SkillColumn
{
df::profession profession;
df::unit_labor labor;
df::job_skill skill;
char label[3];
int group; // for navigation and mass toggling
int8_t color; // for column headers
df::profession profession; // to display graphical tiles
df::unit_labor labor; // toggled when pressing Enter
df::job_skill skill; // displayed rating
char label[3]; // column header
bool special; // specified labor is mutually exclusive with all other special labors
};
#define NUM_COLUMNS (sizeof(columns) / sizeof(SkillColumn))
#define NUM_COLUMNS (sizeof(columns) / sizeof(SkillColumn))
// All of the skill/labor columns we want to display. Includes profession (for color), labor, skill, and 2 character label
// All of the skill/labor columns we want to display.
const SkillColumn columns[] = {
// Mining
{profession::MINER, unit_labor::MINE, job_skill::MINING, "Mi", true},
{0, 7, profession::MINER, unit_labor::MINE, job_skill::MINING, "Mi", true},
// Woodworking
{profession::WOODWORKER, unit_labor::CARPENTER, job_skill::CARPENTRY, "Ca"},
{profession::WOODWORKER, unit_labor::BOWYER, job_skill::BOWYER, "Bw"},
{profession::WOODWORKER, unit_labor::CUTWOOD, job_skill::WOODCUTTING, "WC", true},
{1, 14, profession::CARPENTER, unit_labor::CARPENTER, job_skill::CARPENTRY, "Ca"},
{1, 14, profession::BOWYER, unit_labor::BOWYER, job_skill::BOWYER, "Bw"},
{1, 14, profession::WOODCUTTER, unit_labor::CUTWOOD, job_skill::WOODCUTTING, "WC", true},
// Stoneworking
{profession::STONEWORKER, unit_labor::MASON, job_skill::MASONRY, "Ma"},
{profession::STONEWORKER, unit_labor::DETAIL, job_skill::DETAILSTONE, "En"},
{2, 15, profession::MASON, unit_labor::MASON, job_skill::MASONRY, "Ma"},
{2, 15, profession::ENGRAVER, unit_labor::DETAIL, job_skill::DETAILSTONE, "En"},
// Hunting/Related
{profession::RANGER, unit_labor::ANIMALTRAIN, job_skill::ANIMALTRAIN, "Tr"},
{profession::RANGER, unit_labor::ANIMALCARE, job_skill::ANIMALCARE, "Ca"},
{profession::RANGER, unit_labor::HUNT, job_skill::SNEAK, "Hu", true},
{profession::RANGER, unit_labor::TRAPPER, job_skill::TRAPPING, "Tr"},
{profession::RANGER, unit_labor::DISSECT_VERMIN, job_skill::DISSECT_VERMIN, "Di"},
{3, 2, profession::ANIMAL_TRAINER, unit_labor::ANIMALTRAIN, job_skill::ANIMALTRAIN, "Tn"},
{3, 2, profession::ANIMAL_CARETAKER, unit_labor::ANIMALCARE, job_skill::ANIMALCARE, "Ca"},
{3, 2, profession::HUNTER, unit_labor::HUNT, job_skill::SNEAK, "Hu", true},
{3, 2, profession::TRAPPER, unit_labor::TRAPPER, job_skill::TRAPPING, "Tr"},
{3, 2, profession::ANIMAL_DISSECTOR, unit_labor::DISSECT_VERMIN, job_skill::DISSECT_VERMIN, "Di"},
// Healthcare
{profession::DOCTOR, unit_labor::DIAGNOSE, job_skill::DIAGNOSE, "Di"},
{profession::DOCTOR, unit_labor::SURGERY, job_skill::SURGERY, "Su"},
{profession::DOCTOR, unit_labor::BONE_SETTING, job_skill::SET_BONE, "Bo"},
{profession::DOCTOR, unit_labor::SUTURING, job_skill::SUTURE, "St"},
{profession::DOCTOR, unit_labor::DRESSING_WOUNDS, job_skill::DRESS_WOUNDS, "Dr"},
{profession::DOCTOR, unit_labor::FEED_WATER_CIVILIANS, job_skill::NONE, "Fd"},
{profession::DOCTOR, unit_labor::RECOVER_WOUNDED, job_skill::NONE, "Re"},
{4, 5, profession::DIAGNOSER, unit_labor::DIAGNOSE, job_skill::DIAGNOSE, "Di"},
{4, 5, profession::SURGEON, unit_labor::SURGERY, job_skill::SURGERY, "Su"},
{4, 5, profession::BONE_SETTER, unit_labor::BONE_SETTING, job_skill::SET_BONE, "Bo"},
{4, 5, profession::SUTURER, unit_labor::SUTURING, job_skill::SUTURE, "St"},
{4, 5, profession::DOCTOR, unit_labor::DRESSING_WOUNDS, job_skill::DRESS_WOUNDS, "Dr"},
{4, 5, profession::NONE, unit_labor::FEED_WATER_CIVILIANS, job_skill::NONE, "Fd"},
{4, 5, profession::NONE, unit_labor::RECOVER_WOUNDED, job_skill::NONE, "Re"},
// Farming/Related
{profession::FARMER, unit_labor::BUTCHER, job_skill::BUTCHER, "Bu"},
{profession::FARMER, unit_labor::TANNER, job_skill::TANNER, "Ta"},
{profession::FARMER, unit_labor::PLANT, job_skill::PLANT, "Gr"},
{profession::FARMER, unit_labor::DYER, job_skill::DYER, "Dy"},
{profession::FARMER, unit_labor::SOAP_MAKER, job_skill::SOAP_MAKING, "So"},
{profession::FARMER, unit_labor::BURN_WOOD, job_skill::WOOD_BURNING, "WB"},
{profession::FARMER, unit_labor::POTASH_MAKING, job_skill::POTASH_MAKING, "Po"},
{profession::FARMER, unit_labor::LYE_MAKING, job_skill::LYE_MAKING, "Ly"},
{profession::FARMER, unit_labor::MILLER, job_skill::MILLING, "Ml"},
{profession::FARMER, unit_labor::BREWER, job_skill::BREWING, "Br"},
{profession::FARMER, unit_labor::HERBALIST, job_skill::HERBALISM, "He"},
{profession::FARMER, unit_labor::PROCESS_PLANT, job_skill::PROCESSPLANTS, "Th"},
{profession::FARMER, unit_labor::MAKE_CHEESE, job_skill::CHEESEMAKING, "Ch"},
{profession::FARMER, unit_labor::MILK, job_skill::MILK, "Mk"},
{profession::FARMER, unit_labor::SHEARER, job_skill::SHEARING, "Sh"},
{profession::FARMER, unit_labor::SPINNER, job_skill::SPINNING, "Sp"},
{profession::FARMER, unit_labor::COOK, job_skill::COOK, "Co"},
{profession::FARMER, unit_labor::PRESSING, job_skill::PRESSING, "Pr"},
{profession::FARMER, unit_labor::BEEKEEPING, job_skill::BEEKEEPING, "Be"},
{5, 6, profession::BUTCHER, unit_labor::BUTCHER, job_skill::BUTCHER, "Bu"},
{5, 6, profession::TANNER, unit_labor::TANNER, job_skill::TANNER, "Ta"},
{5, 6, profession::PLANTER, unit_labor::PLANT, job_skill::PLANT, "Gr"},
{5, 6, profession::DYER, unit_labor::DYER, job_skill::DYER, "Dy"},
{5, 6, profession::SOAP_MAKER, unit_labor::SOAP_MAKER, job_skill::SOAP_MAKING, "So"},
{5, 6, profession::WOOD_BURNER, unit_labor::BURN_WOOD, job_skill::WOOD_BURNING, "WB"},
{5, 6, profession::POTASH_MAKER, unit_labor::POTASH_MAKING, job_skill::POTASH_MAKING, "Po"},
{5, 6, profession::LYE_MAKER, unit_labor::LYE_MAKING, job_skill::LYE_MAKING, "Ly"},
{5, 6, profession::MILLER, unit_labor::MILLER, job_skill::MILLING, "Ml"},
{5, 6, profession::BREWER, unit_labor::BREWER, job_skill::BREWING, "Br"},
{5, 6, profession::HERBALIST, unit_labor::HERBALIST, job_skill::HERBALISM, "He"},
{5, 6, profession::THRESHER, unit_labor::PROCESS_PLANT, job_skill::PROCESSPLANTS, "Th"},
{5, 6, profession::CHEESE_MAKER, unit_labor::MAKE_CHEESE, job_skill::CHEESEMAKING, "Ch"},
{5, 6, profession::MILKER, unit_labor::MILK, job_skill::MILK, "Mk"},
{5, 6, profession::SHEARER, unit_labor::SHEARER, job_skill::SHEARING, "Sh"},
{5, 6, profession::SPINNER, unit_labor::SPINNER, job_skill::SPINNING, "Sp"},
{5, 6, profession::COOK, unit_labor::COOK, job_skill::COOK, "Co"},
{5, 6, profession::PRESSER, unit_labor::PRESSING, job_skill::PRESSING, "Pr"},
{5, 6, profession::BEEKEEPER, unit_labor::BEEKEEPING, job_skill::BEEKEEPING, "Be"},
// Fishing/Related
{profession::FISHERMAN, unit_labor::FISH, job_skill::FISH, "Fi"},
{profession::FISHERMAN, unit_labor::CLEAN_FISH, job_skill::PROCESSFISH, "Cl"},
{profession::FISHERMAN, unit_labor::DISSECT_FISH, job_skill::DISSECT_FISH, "Di"},
{6, 1, profession::FISHERMAN, unit_labor::FISH, job_skill::FISH, "Fi"},
{6, 1, profession::FISH_CLEANER, unit_labor::CLEAN_FISH, job_skill::PROCESSFISH, "Cl"},
{6, 1, profession::FISH_DISSECTOR, unit_labor::DISSECT_FISH, job_skill::DISSECT_FISH, "Di"},
// Metalsmithing
{profession::METALSMITH, unit_labor::SMELT, job_skill::SMELT, "Fu"},
{profession::METALSMITH, unit_labor::FORGE_WEAPON, job_skill::FORGE_WEAPON, "We"},
{profession::METALSMITH, unit_labor::FORGE_ARMOR, job_skill::FORGE_ARMOR, "Ar"},
{profession::METALSMITH, unit_labor::FORGE_FURNITURE, job_skill::FORGE_FURNITURE, "Bl"},
{profession::METALSMITH, unit_labor::METAL_CRAFT, job_skill::METALCRAFT, "Cr"},
{7, 8, profession::FURNACE_OPERATOR, unit_labor::SMELT, job_skill::SMELT, "Fu"},
{7, 8, profession::WEAPONSMITH, unit_labor::FORGE_WEAPON, job_skill::FORGE_WEAPON, "We"},
{7, 8, profession::ARMORER, unit_labor::FORGE_ARMOR, job_skill::FORGE_ARMOR, "Ar"},
{7, 8, profession::BLACKSMITH, unit_labor::FORGE_FURNITURE, job_skill::FORGE_FURNITURE, "Bl"},
{7, 8, profession::METALCRAFTER, unit_labor::METAL_CRAFT, job_skill::METALCRAFT, "Cr"},
// Jewelry
{profession::JEWELER, unit_labor::CUT_GEM, job_skill::CUTGEM, "Cu"},
{profession::JEWELER, unit_labor::ENCRUST_GEM, job_skill::ENCRUSTGEM, "Se"},
{8, 10, profession::GEM_CUTTER, unit_labor::CUT_GEM, job_skill::CUTGEM, "Cu"},
{8, 10, profession::GEM_SETTER, unit_labor::ENCRUST_GEM, job_skill::ENCRUSTGEM, "Se"},
// Crafts
{profession::CRAFTSMAN, unit_labor::LEATHER, job_skill::LEATHERWORK, "Le"},
{profession::CRAFTSMAN, unit_labor::WOOD_CRAFT, job_skill::WOODCRAFT, "Wo"},
{profession::CRAFTSMAN, unit_labor::STONE_CRAFT, job_skill::STONECRAFT, "St"},
{profession::CRAFTSMAN, unit_labor::BONE_CARVE, job_skill::BONECARVE, "Bo"},
{profession::CRAFTSMAN, unit_labor::GLASSMAKER, job_skill::GLASSMAKER, "Gl"},
{profession::CRAFTSMAN, unit_labor::WEAVER, job_skill::WEAVING, "We"},
{profession::CRAFTSMAN, unit_labor::CLOTHESMAKER, job_skill::CLOTHESMAKING, "Cl"},
{profession::CRAFTSMAN, unit_labor::EXTRACT_STRAND, job_skill::EXTRACT_STRAND, "Ad"},
{profession::CRAFTSMAN, unit_labor::POTTERY, job_skill::POTTERY, "Po"},
{profession::CRAFTSMAN, unit_labor::GLAZING, job_skill::GLAZING, "Gl"},
{profession::CRAFTSMAN, unit_labor::WAX_WORKING, job_skill::WAX_WORKING, "Wx"},
{9, 9, profession::LEATHERWORKER, unit_labor::LEATHER, job_skill::LEATHERWORK, "Le"},
{9, 9, profession::WOODCRAFTER, unit_labor::WOOD_CRAFT, job_skill::WOODCRAFT, "Wo"},
{9, 9, profession::STONECRAFTER, unit_labor::STONE_CRAFT, job_skill::STONECRAFT, "St"},
{9, 9, profession::BONE_CARVER, unit_labor::BONE_CARVE, job_skill::BONECARVE, "Bo"},
{9, 9, profession::GLASSMAKER, unit_labor::GLASSMAKER, job_skill::GLASSMAKER, "Gl"},
{9, 9, profession::WEAVER, unit_labor::WEAVER, job_skill::WEAVING, "We"},
{9, 9, profession::CLOTHIER, unit_labor::CLOTHESMAKER, job_skill::CLOTHESMAKING, "Cl"},
{9, 9, profession::STRAND_EXTRACTOR, unit_labor::EXTRACT_STRAND, job_skill::EXTRACT_STRAND, "Ad"},
{9, 9, profession::POTTER, unit_labor::POTTERY, job_skill::POTTERY, "Po"},
{9, 9, profession::GLAZER, unit_labor::GLAZING, job_skill::GLAZING, "Gl"},
{9, 9, profession::WAX_WORKER, unit_labor::WAX_WORKING, job_skill::WAX_WORKING, "Wx"},
// Engineering
{profession::ENGINEER, unit_labor::SIEGECRAFT, job_skill::SIEGECRAFT, "En"},
{profession::ENGINEER, unit_labor::SIEGEOPERATE, job_skill::SIEGEOPERATE, "Op"},
{profession::ENGINEER, unit_labor::MECHANIC, job_skill::MECHANICS, "Me"},
{profession::ENGINEER, unit_labor::OPERATE_PUMP, job_skill::OPERATE_PUMP, "Pu"},
{10, 12, profession::SIEGE_ENGINEER, unit_labor::SIEGECRAFT, job_skill::SIEGECRAFT, "En"},
{10, 12, profession::SIEGE_OPERATOR, unit_labor::SIEGEOPERATE, job_skill::SIEGEOPERATE, "Op"},
{10, 12, profession::MECHANIC, unit_labor::MECHANIC, job_skill::MECHANICS, "Me"},
{10, 12, profession::PUMP_OPERATOR, unit_labor::OPERATE_PUMP, job_skill::OPERATE_PUMP, "Pu"},
// Hauling
{profession::STANDARD, unit_labor::HAUL_STONE, job_skill::NONE, "St"},
{profession::STANDARD, unit_labor::HAUL_WOOD, job_skill::NONE, "Wo"},
{profession::STANDARD, unit_labor::HAUL_ITEM, job_skill::NONE, "It"},
{profession::STANDARD, unit_labor::HAUL_BODY, job_skill::NONE, "Bu"},
{profession::STANDARD, unit_labor::HAUL_FOOD, job_skill::NONE, "Fo"},
{profession::STANDARD, unit_labor::HAUL_REFUSE, job_skill::NONE, "Re"},
{profession::STANDARD, unit_labor::HAUL_FURNITURE, job_skill::NONE, "Fu"},
{profession::STANDARD, unit_labor::HAUL_ANIMAL, job_skill::NONE, "An"},
{profession::STANDARD, unit_labor::PUSH_HAUL_VEHICLE, job_skill::NONE, "Ve"},
{11, 3, profession::NONE, unit_labor::HAUL_STONE, job_skill::NONE, "St"},
{11, 3, profession::NONE, unit_labor::HAUL_WOOD, job_skill::NONE, "Wo"},
{11, 3, profession::NONE, unit_labor::HAUL_ITEM, job_skill::NONE, "It"},
{11, 3, profession::NONE, unit_labor::HAUL_BODY, job_skill::NONE, "Bu"},
{11, 3, profession::NONE, unit_labor::HAUL_FOOD, job_skill::NONE, "Fo"},
{11, 3, profession::NONE, unit_labor::HAUL_REFUSE, job_skill::NONE, "Re"},
{11, 3, profession::NONE, unit_labor::HAUL_FURNITURE, job_skill::NONE, "Fu"},
{11, 3, profession::NONE, unit_labor::HAUL_ANIMAL, job_skill::NONE, "An"},
{11, 3, profession::NONE, unit_labor::PUSH_HAUL_VEHICLE, job_skill::NONE, "Ve"},
// Other Jobs
{profession::CHILD, unit_labor::ARCHITECT, job_skill::DESIGNBUILDING, "Ar"},
{profession::CHILD, unit_labor::ALCHEMIST, job_skill::ALCHEMY, "Al"},
{profession::CHILD, unit_labor::CLEAN, job_skill::NONE, "Cl"},
// Military
{profession::WRESTLER, unit_labor::NONE, job_skill::WRESTLING, "Wr"},
{profession::WRESTLER, unit_labor::NONE, job_skill::AXE, "Ax"},
{profession::WRESTLER, unit_labor::NONE, job_skill::SWORD, "Sw"},
{profession::WRESTLER, unit_labor::NONE, job_skill::MACE, "Mc"},
{profession::WRESTLER, unit_labor::NONE, job_skill::HAMMER, "Ha"},
{profession::WRESTLER, unit_labor::NONE, job_skill::SPEAR, "Sp"},
{profession::WRESTLER, unit_labor::NONE, job_skill::DAGGER, "Kn"},
{profession::WRESTLER, unit_labor::NONE, job_skill::CROSSBOW, "Cb"},
{profession::WRESTLER, unit_labor::NONE, job_skill::BOW, "Bo"},
{profession::WRESTLER, unit_labor::NONE, job_skill::BLOWGUN, "Bl"},
{profession::WRESTLER, unit_labor::NONE, job_skill::PIKE, "Pk"},
{profession::WRESTLER, unit_labor::NONE, job_skill::WHIP, "La"},
{profession::WRESTLER, unit_labor::NONE, job_skill::ARMOR, "Ar"},
{profession::WRESTLER, unit_labor::NONE, job_skill::SHIELD, "Sh"},
{profession::WRESTLER, unit_labor::NONE, job_skill::BITE, "Bi"},
{profession::WRESTLER, unit_labor::NONE, job_skill::GRASP_STRIKE, "Pu"},
{profession::WRESTLER, unit_labor::NONE, job_skill::STANCE_STRIKE, "Ki"},
{profession::WRESTLER, unit_labor::NONE, job_skill::DODGING, "Do"},
{profession::WRESTLER, unit_labor::NONE, job_skill::MISC_WEAPON, "Mi"},
{profession::WRESTLER, unit_labor::NONE, job_skill::MELEE_COMBAT, "Fi"},
{profession::WRESTLER, unit_labor::NONE, job_skill::RANGED_COMBAT, "Ar"},
{profession::RECRUIT, unit_labor::NONE, job_skill::LEADERSHIP, "Le"},
{profession::RECRUIT, unit_labor::NONE, job_skill::TEACHING, "Te"},
{profession::RECRUIT, unit_labor::NONE, job_skill::KNOWLEDGE_ACQUISITION, "Lr"},
{profession::RECRUIT, unit_labor::NONE, job_skill::DISCIPLINE, "Di"},
{profession::RECRUIT, unit_labor::NONE, job_skill::CONCENTRATION, "Co"},
{profession::RECRUIT, unit_labor::NONE, job_skill::SITUATIONAL_AWARENESS, "Aw"},
{profession::RECRUIT, unit_labor::NONE, job_skill::COORDINATION, "Cr"},
{profession::RECRUIT, unit_labor::NONE, job_skill::BALANCE, "Ba"},
{12, 4, profession::ARCHITECT, unit_labor::ARCHITECT, job_skill::DESIGNBUILDING, "Ar"},
{12, 4, profession::ALCHEMIST, unit_labor::ALCHEMIST, job_skill::ALCHEMY, "Al"},
{12, 4, profession::NONE, unit_labor::CLEAN, job_skill::NONE, "Cl"},
// Military - Weapons
{13, 7, profession::WRESTLER, unit_labor::NONE, job_skill::WRESTLING, "Wr"},
{13, 7, profession::AXEMAN, unit_labor::NONE, job_skill::AXE, "Ax"},
{13, 7, profession::SWORDSMAN, unit_labor::NONE, job_skill::SWORD, "Sw"},
{13, 7, profession::MACEMAN, unit_labor::NONE, job_skill::MACE, "Mc"},
{13, 7, profession::HAMMERMAN, unit_labor::NONE, job_skill::HAMMER, "Ha"},
{13, 7, profession::SPEARMAN, unit_labor::NONE, job_skill::SPEAR, "Sp"},
{13, 7, profession::CROSSBOWMAN, unit_labor::NONE, job_skill::CROSSBOW, "Cb"},
{13, 7, profession::THIEF, unit_labor::NONE, job_skill::DAGGER, "Kn"},
{13, 7, profession::BOWMAN, unit_labor::NONE, job_skill::BOW, "Bo"},
{13, 7, profession::BLOWGUNMAN, unit_labor::NONE, job_skill::BLOWGUN, "Bl"},
{13, 7, profession::PIKEMAN, unit_labor::NONE, job_skill::PIKE, "Pk"},
{13, 7, profession::LASHER, unit_labor::NONE, job_skill::WHIP, "La"},
// Military - Other Combat
{14, 15, profession::NONE, unit_labor::NONE, job_skill::BITE, "Bi"},
{14, 15, profession::NONE, unit_labor::NONE, job_skill::GRASP_STRIKE, "St"},
{14, 15, profession::NONE, unit_labor::NONE, job_skill::STANCE_STRIKE, "Ki"},
{14, 15, profession::NONE, unit_labor::NONE, job_skill::MISC_WEAPON, "Mi"},
{14, 15, profession::NONE, unit_labor::NONE, job_skill::MELEE_COMBAT, "Fg"},
{14, 15, profession::NONE, unit_labor::NONE, job_skill::RANGED_COMBAT, "Ac"},
{14, 15, profession::NONE, unit_labor::NONE, job_skill::ARMOR, "Ar"},
{14, 15, profession::NONE, unit_labor::NONE, job_skill::SHIELD, "Sh"},
{14, 15, profession::NONE, unit_labor::NONE, job_skill::DODGING, "Do"},
// Military - Misc
{15, 8, profession::NONE, unit_labor::NONE, job_skill::LEADERSHIP, "Ld"},
{15, 8, profession::NONE, unit_labor::NONE, job_skill::TEACHING, "Te"},
{15, 8, profession::NONE, unit_labor::NONE, job_skill::KNOWLEDGE_ACQUISITION, "St"},
{15, 8, profession::NONE, unit_labor::NONE, job_skill::DISCIPLINE, "Di"},
{15, 8, profession::NONE, unit_labor::NONE, job_skill::CONCENTRATION, "Co"},
{15, 8, profession::NONE, unit_labor::NONE, job_skill::SITUATIONAL_AWARENESS, "Ob"},
{15, 8, profession::NONE, unit_labor::NONE, job_skill::COORDINATION, "Cr"},
{15, 8, profession::NONE, unit_labor::NONE, job_skill::BALANCE, "Ba"},
// Social
{profession::STANDARD, unit_labor::NONE, job_skill::PERSUASION, "Pe"},
{profession::STANDARD, unit_labor::NONE, job_skill::NEGOTIATION, "Ne"},
{profession::STANDARD, unit_labor::NONE, job_skill::JUDGING_INTENT, "Ju"},
{profession::STANDARD, unit_labor::NONE, job_skill::LYING, "Ly"},
{profession::STANDARD, unit_labor::NONE, job_skill::INTIMIDATION, "In"},
{profession::STANDARD, unit_labor::NONE, job_skill::CONVERSATION, "Cn"},
{profession::STANDARD, unit_labor::NONE, job_skill::COMEDY, "Cm"},
{profession::STANDARD, unit_labor::NONE, job_skill::FLATTERY, "Fl"},
{profession::STANDARD, unit_labor::NONE, job_skill::CONSOLE, "Cs"},
{profession::STANDARD, unit_labor::NONE, job_skill::PACIFY, "Pc"},
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::APPRAISAL, "Ap"},
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::ORGANIZATION, "Or"},
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::RECORD_KEEPING, "RK"},
{16, 3, profession::NONE, unit_labor::NONE, job_skill::PERSUASION, "Pe"},
{16, 3, profession::NONE, unit_labor::NONE, job_skill::NEGOTIATION, "Ne"},
{16, 3, profession::NONE, unit_labor::NONE, job_skill::JUDGING_INTENT, "Ju"},
{16, 3, profession::NONE, unit_labor::NONE, job_skill::LYING, "Li"},
{16, 3, profession::NONE, unit_labor::NONE, job_skill::INTIMIDATION, "In"},
{16, 3, profession::NONE, unit_labor::NONE, job_skill::CONVERSATION, "Cn"},
{16, 3, profession::NONE, unit_labor::NONE, job_skill::COMEDY, "Cm"},
{16, 3, profession::NONE, unit_labor::NONE, job_skill::FLATTERY, "Fl"},
{16, 3, profession::NONE, unit_labor::NONE, job_skill::CONSOLE, "Cs"},
{16, 3, profession::NONE, unit_labor::NONE, job_skill::PACIFY, "Pc"},
// Noble
{17, 5, profession::TRADER, unit_labor::NONE, job_skill::APPRAISAL, "Ap"},
{17, 5, profession::ADMINISTRATOR, unit_labor::NONE, job_skill::ORGANIZATION, "Or"},
{17, 5, profession::CLERK, unit_labor::NONE, job_skill::RECORD_KEEPING, "RK"},
// Miscellaneous
{profession::STANDARD, unit_labor::NONE, job_skill::THROW, "Th"},
{profession::STANDARD, unit_labor::NONE, job_skill::CRUTCH_WALK, "CW"},
{profession::STANDARD, unit_labor::NONE, job_skill::SWIMMING, "Sw"},
{profession::STANDARD, unit_labor::NONE, job_skill::KNAPPING, "Kn"},
{profession::DRUNK, unit_labor::NONE, job_skill::WRITING, "Wr"},
{profession::DRUNK, unit_labor::NONE, job_skill::PROSE, "Pr"},
{profession::DRUNK, unit_labor::NONE, job_skill::POETRY, "Po"},
{profession::DRUNK, unit_labor::NONE, job_skill::READING, "Rd"},
{profession::DRUNK, unit_labor::NONE, job_skill::SPEAKING, "Sp"},
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::MILITARY_TACTICS, "MT"},
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::TRACKING, "Tr"},
{profession::ADMINISTRATOR, unit_labor::NONE, job_skill::MAGIC_NATURE, "Dr"},
{18, 3, profession::NONE, unit_labor::NONE, job_skill::THROW, "Th"},
{18, 3, profession::NONE, unit_labor::NONE, job_skill::CRUTCH_WALK, "CW"},
{18, 3, profession::NONE, unit_labor::NONE, job_skill::SWIMMING, "Sw"},
{18, 3, profession::NONE, unit_labor::NONE, job_skill::KNAPPING, "Kn"},
{19, 6, profession::NONE, unit_labor::NONE, job_skill::WRITING, "Wr"},
{19, 6, profession::NONE, unit_labor::NONE, job_skill::PROSE, "Pr"},
{19, 6, profession::NONE, unit_labor::NONE, job_skill::POETRY, "Po"},
{19, 6, profession::NONE, unit_labor::NONE, job_skill::READING, "Rd"},
{19, 6, profession::NONE, unit_labor::NONE, job_skill::SPEAKING, "Sp"},
{20, 5, profession::NONE, unit_labor::NONE, job_skill::MILITARY_TACTICS, "MT"},
{20, 5, profession::NONE, unit_labor::NONE, job_skill::TRACKING, "Tr"},
{20, 5, profession::NONE, unit_labor::NONE, job_skill::MAGIC_NATURE, "Dr"},
};
struct UnitInfo
@ -247,17 +249,69 @@ struct UnitInfo
int8_t color;
};
#define FILTER_NONWORKERS 0x0001
#define FILTER_NONDWARVES 0x0002
#define FILTER_NONCIV 0x0004
#define FILTER_ANIMALS 0x0008
#define FILTER_LIVING 0x0010
#define FILTER_DEAD 0x0020
enum altsort_mode {
ALTSORT_NAME,
ALTSORT_PROFESSION,
ALTSORT_MAX
};
bool descending;
df::job_skill sort_skill;
df::unit_labor sort_labor;
bool sortByName (const UnitInfo *d1, const UnitInfo *d2)
{
if (descending)
return (d1->name > d2->name);
else
return (d1->name < d2->name);
}
bool sortByProfession (const UnitInfo *d1, const UnitInfo *d2)
{
if (descending)
return (d1->profession > d2->profession);
else
return (d1->profession < d2->profession);
}
bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2)
{
if (sort_skill != job_skill::NONE)
{
df::unit_skill *s1 = binsearch_in_vector<df::unit_skill,df::enum_field<df::job_skill,int16_t>>(d1->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill);
df::unit_skill *s2 = binsearch_in_vector<df::unit_skill,df::enum_field<df::job_skill,int16_t>>(d2->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill);
int l1 = s1 ? s1->rating : 0;
int l2 = s2 ? s2->rating : 0;
int e1 = s1 ? s1->experience : 0;
int e2 = s2 ? s2->experience : 0;
if (descending)
{
if (l1 != l2)
return l1 > l2;
if (e1 != e2)
return e1 > e2;
}
else
{
if (l1 != l2)
return l1 < l2;
if (e1 != e2)
return e1 < e2;
}
}
if (sort_labor != unit_labor::NONE)
{
if (descending)
return d1->unit->status.labors[sort_labor] > d2->unit->status.labors[sort_labor];
else
return d1->unit->status.labors[sort_labor] < d2->unit->status.labors[sort_labor];
}
return sortByName(d1, d2);
}
class viewscreen_unitlaborsst : public dfhack_viewscreen {
public:
static viewscreen_unitlaborsst *create (char pushtype, df::viewscreen *scr = NULL);
void feed(set<df::interface_key> *events);
void render();
@ -267,86 +321,41 @@ public:
std::string getFocusString() { return "unitlabors"; }
viewscreen_unitlaborsst();
viewscreen_unitlaborsst(vector<df::unit*> &src);
~viewscreen_unitlaborsst() { };
protected:
vector<UnitInfo *> units;
int filter;
altsort_mode altsort;
int first_row, sel_row;
int first_column, sel_column;
int height, name_width, prof_width, labors_width;
// bool descending;
// int sort_skill;
// int sort_labor;
void readUnits ();
void calcSize ();
};
viewscreen_unitlaborsst::viewscreen_unitlaborsst()
{
filter = FILTER_LIVING;
first_row = sel_row = 0;
first_column = sel_column = 0;
calcSize();
readUnits();
}
void viewscreen_unitlaborsst::readUnits ()
viewscreen_unitlaborsst::viewscreen_unitlaborsst(vector<df::unit*> &src)
{
for (size_t i = 0; i < units.size(); i++)
delete units[i];
units.clear();
UnitInfo *cur = new UnitInfo;
for (size_t i = 0; i < world->units.active.size(); i++)
for (size_t i = 0; i < src.size(); i++)
{
df::unit *unit = world->units.active[i];
UnitInfo *cur = new UnitInfo;
df::unit *unit = src[i];
cur->unit = unit;
cur->allowEdit = true;
if (unit->race != ui->race_id)
{
cur->allowEdit = false;
if (!(filter & FILTER_NONDWARVES))
continue;
}
if (unit->civ_id != ui->civ_id)
{
cur->allowEdit = false;
if (!(filter & FILTER_NONCIV))
continue;
}
if (unit->flags1.bits.dead)
{
cur->allowEdit = false;
if (!(filter & FILTER_DEAD))
continue;
}
else
{
if (!(filter & FILTER_LIVING))
continue;
}
if (!ENUM_ATTR(profession, can_assign_labor, unit->profession))
{
cur->allowEdit = false;
if (!(filter & FILTER_NONWORKERS))
continue;
}
if (!unit->name.first_name.length())
{
if (!(filter & FILTER_ANIMALS))
continue;
}
cur->name = Translation::TranslateName(&unit->name, false);
cur->transname = Translation::TranslateName(&unit->name, true);
@ -354,9 +363,13 @@ void viewscreen_unitlaborsst::readUnits ()
cur->color = Units::getProfessionColor(unit);
units.push_back(cur);
cur = new UnitInfo;
}
delete cur;
std::sort(units.begin(), units.end(), sortByName);
altsort = ALTSORT_NAME;
first_row = sel_row = 0;
first_column = sel_column = 0;
calcSize();
}
void viewscreen_unitlaborsst::calcSize()
@ -421,8 +434,6 @@ void viewscreen_unitlaborsst::feed(set<df::interface_key> *events)
return;
}
// TODO - allow modifying filters
if (!units.size())
return;
@ -458,16 +469,16 @@ void viewscreen_unitlaborsst::feed(set<df::interface_key> *events)
{
// go to beginning of current column group; if already at the beginning, go to the beginning of the previous one
sel_column--;
df::profession cur = columns[sel_column].profession;
while ((sel_column > 0) && columns[sel_column - 1].profession == cur)
int cur = columns[sel_column].group;
while ((sel_column > 0) && columns[sel_column - 1].group == cur)
sel_column--;
}
if ((sel_column != NUM_COLUMNS - 1) && events->count(interface_key::CURSOR_DOWN_Z))
{
// go to end of current column group; if already at the end, go to the end of the next one
sel_column++;
df::profession cur = columns[sel_column].profession;
while ((sel_column < NUM_COLUMNS - 1) && columns[sel_column + 1].profession == cur)
int cur = columns[sel_column].group;
while ((sel_column < NUM_COLUMNS - 1) && columns[sel_column + 1].group == cur)
sel_column++;
}
@ -486,9 +497,10 @@ void viewscreen_unitlaborsst::feed(set<df::interface_key> *events)
{
df::unit *unit = cur->unit;
const SkillColumn &col = columns[sel_column];
bool newstatus = !unit->status.labors[col.labor];
if (col.special)
{
if (!unit->status.labors[col.labor])
if (newstatus)
{
for (int i = 0; i < NUM_COLUMNS; i++)
{
@ -498,12 +510,91 @@ void viewscreen_unitlaborsst::feed(set<df::interface_key> *events)
}
unit->military.pickup_flags.bits.update = true;
}
unit->status.labors[col.labor] = !unit->status.labors[col.labor];
unit->status.labors[col.labor] = newstatus;
}
if (events->count(interface_key::SELECT_ALL) && (cur->allowEdit))
{
df::unit *unit = cur->unit;
const SkillColumn &col = columns[sel_column];
bool newstatus = !unit->status.labors[col.labor];
for (int i = 0; i < NUM_COLUMNS; i++)
{
if (columns[i].group != col.group)
continue;
if (columns[i].special)
{
if (newstatus)
{
for (int j = 0; j < NUM_COLUMNS; j++)
{
if ((columns[j].labor != unit_labor::NONE) && columns[j].special)
unit->status.labors[columns[j].labor] = false;
}
}
unit->military.pickup_flags.bits.update = true;
}
unit->status.labors[columns[i].labor] = newstatus;
}
}
// TODO: add sorting
if (events->count(interface_key::SECONDSCROLL_UP) || events->count(interface_key::SECONDSCROLL_DOWN))
{
descending = events->count(interface_key::SECONDSCROLL_UP);
sort_skill = columns[sel_column].skill;
sort_labor = columns[sel_column].labor;
std::sort(units.begin(), units.end(), sortBySkill);
}
if (events->count(interface_key::SECONDSCROLL_PAGEUP) || events->count(interface_key::SECONDSCROLL_PAGEDOWN))
{
descending = events->count(interface_key::SECONDSCROLL_PAGEUP);
switch (altsort)
{
case ALTSORT_NAME:
std::sort(units.begin(), units.end(), sortByName);
break;
case ALTSORT_PROFESSION:
std::sort(units.begin(), units.end(), sortByProfession);
break;
}
}
if (events->count(interface_key::CHANGETAB))
{
switch (altsort)
{
case ALTSORT_NAME:
altsort = ALTSORT_PROFESSION;
break;
case ALTSORT_PROFESSION:
altsort = ALTSORT_NAME;
break;
}
}
if (VIRTUAL_CAST_VAR(unitlist, df::viewscreen_unitlistst, parent))
{
if (events->count(interface_key::UNITJOB_VIEW) || events->count(interface_key::UNITJOB_ZOOM_CRE))
{
for (int i = 0; i < unitlist->units[unitlist->page].size(); i++)
{
if (unitlist->units[unitlist->page][i] == units[sel_row]->unit)
{
unitlist->cursor_pos[unitlist->page] = i;
unitlist->feed(events);
if (Screen::isDismissed(unitlist))
Screen::dismiss(this);
break;
}
}
}
}
}
void OutputString(int8_t color, int &x, int y, const std::string &text)
{
Screen::paintString(Screen::Pen(' ', color, 0), x, y, text);
x += text.length();
}
void viewscreen_unitlaborsst::render()
{
if (Screen::isDismissed(this))
@ -520,7 +611,7 @@ void viewscreen_unitlaborsst::render()
if (col_offset >= NUM_COLUMNS)
break;
int8_t fg = Units::getCasteProfessionColor(ui->race_id, -1, columns[col_offset].profession);
int8_t fg = columns[col_offset].color;
int8_t bg = 0;
if (col_offset == sel_column)
@ -528,8 +619,19 @@ void viewscreen_unitlaborsst::render()
fg = 0;
bg = 7;
}
Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 1);
Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 2);
df::profession profession = columns[col_offset].profession;
if (profession != profession::NONE)
{
auto graphics = world->raws.creatures.all[ui->race_id]->graphics;
Screen::paintTile(
Screen::Pen(' ', fg, 0,
graphics.profession_add_color[creature_graphics_role::DEFAULT][profession],
graphics.profession_texpos[creature_graphics_role::DEFAULT][profession]),
1 + name_width + 1 + prof_width + 1 + col, 3);
}
}
for (int row = 0; row < height; row++)
@ -537,6 +639,7 @@ void viewscreen_unitlaborsst::render()
int row_offset = row + first_row;
if (row_offset >= units.size())
break;
UnitInfo *cur = units[row_offset];
df::unit *unit = cur->unit;
int8_t fg = 15, bg = 0;
@ -548,13 +651,14 @@ void viewscreen_unitlaborsst::render()
string name = cur->name;
name.resize(name_width);
Screen::paintString(Screen::Pen(' ', fg, bg), 1, 3 + row, name);
Screen::paintString(Screen::Pen(' ', fg, bg), 1, 4 + row, name);
string profession = cur->profession;
profession.resize(prof_width);
fg = cur->color;
bg = 0;
Screen::paintString(Screen::Pen(' ', fg, bg), 1 + prof_width + 1, 3 + row, profession);
Screen::paintString(Screen::Pen(' ', fg, bg), 1 + name_width + 1, 4 + row, profession);
// Print unit's skills and labor assignments
for (int col = 0; col < labors_width; col++)
@ -562,11 +666,9 @@ void viewscreen_unitlaborsst::render()
int col_offset = col + first_column;
fg = 15;
bg = 0;
uint8_t c = 0xFA;
if ((col_offset == sel_column) && (row_offset == sel_row))
fg = 9;
if ((columns[col_offset].labor != unit_labor::NONE) && (unit->status.labors[columns[col_offset].labor]))
bg = 7;
char c = '-';
if (columns[col_offset].skill != job_skill::NONE)
{
df::unit_skill *skill = binsearch_in_vector<df::unit_skill,df::enum_field<df::job_skill,int16_t>>(unit->status.current_soul->skills, &df::unit_skill::id, columns[col_offset].skill);
@ -577,24 +679,44 @@ void viewscreen_unitlaborsst::render()
level = NUM_SKILL_LEVELS - 1;
c = skill_levels[level].abbrev;
}
else
c = '-';
}
if (columns[col_offset].labor != unit_labor::NONE)
{
if (unit->status.labors[columns[col_offset].labor])
{
bg = 7;
if (columns[col_offset].skill == job_skill::NONE)
c = 0xF9;
}
}
Screen::paintTile(Screen::Pen(c, fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 3 + row);
else
bg = 4;
Screen::paintTile(Screen::Pen(c, fg, bg), 1 + name_width + 1 + prof_width + 1 + col, 4 + row);
}
}
UnitInfo *cur = units[sel_row];
bool canToggle = false;
if (cur != NULL)
{
df::unit *unit = cur->unit;
string str = cur->transname;
if (str.length())
str += ", ";
str += cur->profession;
str += ":";
int x = 1;
Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->transname);
x += cur->transname.length();
Screen::paintString(Screen::Pen(' ', 15, 0), 1, 3 + height + 2, str);
int y = 1 + str.length() + 1;
if (cur->transname.length())
{
Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ", ");
x += 2;
}
Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, cur->profession);
x += cur->profession.length();
Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + height + 2, ": ");
x += 2;
string str;
if (columns[sel_column].skill == job_skill::NONE)
{
str = ENUM_ATTR_STR(unit_labor, caption, columns[sel_column].labor);
@ -602,7 +724,6 @@ void viewscreen_unitlaborsst::render()
str += " Enabled";
else
str += " Not Enabled";
Screen::paintString(Screen::Pen(' ', 9, 0), y, 3 + height + 2, str);
}
else
{
@ -618,11 +739,50 @@ void viewscreen_unitlaborsst::render()
}
else
str = stl_sprintf("Not %s (0/500)", ENUM_ATTR_STR(job_skill, caption_noun, columns[sel_column].skill));
Screen::paintString(Screen::Pen(' ', 9, 0), y, 3 + height + 2, str);
}
Screen::paintString(Screen::Pen(' ', 9, 0), x, 3 + height + 2, str);
canToggle = (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE);
}
// TODO - print command help info
int x = 2;
OutputString(10, x, gps->dimy - 3, "Enter"); // SELECT key
OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle labor, ");
OutputString(10, x, gps->dimy - 3, "Shift+Enter"); // SELECT_ALL key
OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle Group, ");
OutputString(10, x, gps->dimy - 3, "v"); // UNITJOB_VIEW key
OutputString(15, x, gps->dimy - 3, ": ViewCre, ");
OutputString(10, x, gps->dimy - 3, "c"); // UNITJOB_ZOOM_CRE key
OutputString(15, x, gps->dimy - 3, ": Zoom-Cre");
x = 2;
OutputString(10, x, gps->dimy - 2, "Esc"); // LEAVESCREEN key
OutputString(15, x, gps->dimy - 2, ": Done, ");
OutputString(10, x, gps->dimy - 2, "+"); // SECONDSCROLL_DOWN key
OutputString(10, x, gps->dimy - 2, "-"); // SECONDSCROLL_UP key
OutputString(15, x, gps->dimy - 2, ": Sort by Skill, ");
OutputString(10, x, gps->dimy - 2, "*"); // SECONDSCROLL_PAGEDOWN key
OutputString(10, x, gps->dimy - 2, "/"); // SECONDSCROLL_PAGEUP key
OutputString(15, x, gps->dimy - 2, ": Sort by (");
OutputString(10, x, gps->dimy - 2, "Tab"); // CHANGETAB key
OutputString(15, x, gps->dimy - 2, ") ");
switch (altsort)
{
case ALTSORT_NAME:
OutputString(15, x, gps->dimy - 2, "Name");
break;
case ALTSORT_PROFESSION:
OutputString(15, x, gps->dimy - 2, "Profession");
break;
default:
OutputString(15, x, gps->dimy - 2, "Unknown");
break;
}
}
struct unitlist_hook : df::viewscreen_unitlistst
@ -633,29 +793,43 @@ struct unitlist_hook : df::viewscreen_unitlistst
{
if (input->count(interface_key::UNITVIEW_PRF_PROF))
{
Screen::dismiss(this);
Screen::show(new viewscreen_unitlaborsst());
return;
if (units[page].size())
{
Screen::show(new viewscreen_unitlaborsst(units[page]));
return;
}
}
INTERPOSE_NEXT(feed)(input);
}
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
if (units[page].size())
{
int x = 2;
OutputString(12, x, gps->dimy - 2, "l"); // UNITVIEW_PRF_PROF key
OutputString(15, x, gps->dimy - 2, ": Manage labors");
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, render);
DFHACK_PLUGIN("manipulator");
DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCommand> &commands)
{
if (gps)
{
if (!INTERPOSE_HOOK(unitlist_hook, feed).apply())
out.printerr("Could not interpose viewscreen_unitlistst::feed\n");
}
if (!gps || !INTERPOSE_HOOK(unitlist_hook, feed).apply() || !INTERPOSE_HOOK(unitlist_hook, render).apply())
out.printerr("Could not insert Dwarf Manipulator hooks!\n");
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
INTERPOSE_HOOK(unitlist_hook, feed).remove();
INTERPOSE_HOOK(unitlist_hook, render).remove();
return CR_OK;
}

@ -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;
}

@ -27,6 +27,7 @@ using namespace std;
#include "df/world.h"
#include "df/world_raws.h"
#include "df/building_def.h"
#include "df/region_map_entry.h"
using std::vector;
using std::string;
@ -224,8 +225,7 @@ command_result df_probe (color_ostream &out, vector <string> & parameters)
int bx = clip_range(block.region_pos.x + (offset % 3) - 1, 0, world->world_data->world_width-1);
int by = clip_range(block.region_pos.y + (offset / 3) - 1, 0, world->world_data->world_height-1);
df::world_data::T_region_map* biome =
&world->world_data->region_map[bx][by];
auto biome = &world->world_data->region_map[bx][by];
int sav = biome->savagery;
int evi = biome->evilness;

@ -25,8 +25,12 @@ using namespace std;
#include "df/world.h"
#include "df/world_data.h"
#include "df/world_region_details.h"
#include "df/world_region_feature.h"
#include "df/world_geo_biome.h"
#include "df/world_geo_layer.h"
#include "df/world_underground_region.h"
#include "df/feature_init.h"
#include "df/region_map_entry.h"
#include "df/inclusion_type.h"
#include "df/viewscreen_choose_start_sitest.h"
@ -108,9 +112,10 @@ struct compare_pair_second
}
};
static void printMatdata(color_ostream &con, const matdata &data)
static void printMatdata(color_ostream &con, const matdata &data, bool only_z = false)
{
con << std::setw(9) << data.count;
if (!only_z)
con << std::setw(9) << data.count;
if(data.lower_z != data.upper_z)
con <<" Z:" << std::setw(4) << data.lower_z << ".." << data.upper_z << std::endl;
@ -225,116 +230,247 @@ static coord2d biome_delta[] = {
coord2d(-1,-1), coord2d(0,-1), coord2d(1,-1)
};
static command_result embark_prospector(color_ostream &out, df::viewscreen_choose_start_sitest *screen,
bool showHidden, bool showValue)
struct EmbarkTileLayout {
coord2d biome_off, biome_pos;
df::region_map_entry *biome;
int elevation, max_soil_depth;
int min_z, base_z;
std::map<int, float> penalty;
};
bool estimate_underground(color_ostream &out, EmbarkTileLayout &tile, df::world_region_details *details, int x, int y)
{
if (!world || !world->world_data)
// Find actual biome
int bv = clip_range(details->biome[x][y] & 15, 1, 9);
tile.biome_off = biome_delta[bv-1];
df::world_data *data = world->world_data;
int bx = clip_range(details->pos.x + tile.biome_off.x, 0, data->world_width-1);
int by = clip_range(details->pos.y + tile.biome_off.y, 0, data->world_height-1);
tile.biome_pos = coord2d(bx, by);
tile.biome = &data->region_map[bx][by];
// Compute surface elevation
tile.elevation = (
details->elevation[x][y] + details->elevation[x][y+1] +
details->elevation[x+1][y] + details->elevation[x+1][y+1]
) / 4;
tile.max_soil_depth = std::max((154-tile.biome->elevation)/5,0);
tile.base_z = tile.elevation;
tile.penalty.clear();
auto &features = details->features[x][y];
// Collect global feature layer depths and apply penalties
std::map<int, int> layer_bottom, layer_top;
bool sea_found = false;
for (size_t i = 0; i < features.size(); i++)
{
out.printerr("World data is not available.\n");
return CR_FAILURE;
auto feature = features[i];
auto layer = df::world_underground_region::find(feature->layer);
if (!layer || feature->min_z == -30000) continue;
layer_bottom[layer->layer_depth] = feature->min_z;
layer_top[layer->layer_depth] = feature->max_z;
tile.base_z = std::min(tile.base_z, (int)feature->min_z);
float penalty = 1.0f;
switch (layer->type) {
case df::world_underground_region::Cavern:
penalty = 0.75f;
break;
case df::world_underground_region::MagmaSea:
sea_found = true;
tile.min_z = feature->min_z;
for (int i = feature->min_z; i <= feature->max_z; i++)
tile.penalty[i] = 0.2 + 0.6f*(i-feature->min_z)/(feature->max_z-feature->min_z+1);
break;
case df::world_underground_region::Underworld:
penalty = 0.0f;
break;
}
if (penalty != 1.0f)
{
for (int i = feature->min_z; i <= feature->max_z; i++)
tile.penalty[i] = penalty;
}
}
df::world_data *data = world->world_data;
coord2d cur_region = screen->region_pos;
int d_idx = linear_index(data->region_details, &df::world_region_details::pos, cur_region);
auto cur_details = vector_get(data->region_details, d_idx);
if (!sea_found)
{
out.printerr("Could not find magma sea.\n");
return false;
}
if (!cur_details)
// Scan for big local features and apply their penalties
for (size_t i = 0; i < features.size(); i++)
{
out.printerr("Current region details are not available.\n");
return CR_FAILURE;
auto feature = features[i];
auto lfeature = Maps::getLocalInitFeature(details->pos, feature->feature_idx);
if (!lfeature)
continue;
switch (lfeature->getType())
{
case feature_type::pit:
case feature_type::magma_pool:
case feature_type::volcano:
for (int i = layer_bottom[lfeature->end_depth];
i <= layer_top[lfeature->start_depth]; i++)
tile.penalty[i] = std::min(0.4f, map_find(tile.penalty, i, 1.0f));
break;
default:
break;
}
}
// Compute biomes
std::map<coord2d, int> biomes;
return true;
}
void add_materials(EmbarkTileLayout &tile, matdata &data, float amount, int min_z, int max_z)
{
for (int z = min_z; z <= max_z; z++)
data.add(z, int(map_find(tile.penalty, z, 1)*amount));
}
bool estimate_materials(color_ostream &out, EmbarkTileLayout &tile, MatMap &layerMats, MatMap &veinMats)
{
using namespace geo_layer_type;
df::world_geo_biome *geo_biome = df::world_geo_biome::find(tile.biome->geo_index);
if (screen->biome_highlighted)
if (!geo_biome)
{
out.print("Processing one embark tile of biome F%d.\n\n", screen->biome_idx+1);
biomes[screen->biome_rgn[screen->biome_idx]]++;
out.printerr("Region geo-biome not found: (%d,%d)\n",
tile.biome_pos.x, tile.biome_pos.y);
return false;
}
else
// soil depth increases by 1 every 5 levels below 150
int top_z_level = tile.elevation - tile.max_soil_depth;
for (unsigned i = 0; i < geo_biome->layers.size(); i++)
{
for (int x = screen->embark_pos_min.x; x <= screen->embark_pos_max.x; x++)
auto layer = geo_biome->layers[i];
switch (layer->type)
{
for (int y = screen->embark_pos_min.y; y <= screen->embark_pos_max.y; y++)
{
int bv = clip_range(cur_details->biome[x][y], 1, 9);
biomes[cur_region + biome_delta[bv-1]]++;
}
case SOIL:
case SOIL_OCEAN:
case SOIL_SAND:
top_z_level += layer->top_height - layer->bottom_height + 1;
break;
default:;
}
}
// Compute material maps
MatMap layerMats;
MatMap veinMats;
top_z_level = std::max(top_z_level, tile.elevation)-1;
for (auto biome_it = biomes.begin(); biome_it != biomes.end(); ++biome_it)
for (unsigned i = 0; i < geo_biome->layers.size(); i++)
{
int bx = clip_range(biome_it->first.x, 0, data->world_width-1);
int by = clip_range(biome_it->first.y, 0, data->world_height-1);
auto &region = data->region_map[bx][by];
df::world_geo_biome *geo_biome = df::world_geo_biome::find(region.geo_index);
auto layer = geo_biome->layers[i];
if (!geo_biome)
{
out.printerr("Region geo-biome not found: (%d,%d)\n", bx, by);
return CR_FAILURE;
}
int top_z = std::min(layer->top_height + top_z_level, tile.elevation-1);
int bottom_z = std::max(layer->bottom_height + top_z_level, tile.min_z);
if (i+1 == geo_biome->layers.size()) // stretch layer if needed
bottom_z = tile.min_z;
if (top_z < bottom_z)
continue;
float layer_size = 48*48;
int sums[ENUM_LAST_ITEM(inclusion_type)+1] = { 0 };
int cnt = biome_it->second;
for (unsigned j = 0; j < layer->vein_mat.size(); j++)
if (is_valid_enum_item<df::inclusion_type>(layer->vein_type[j]))
sums[layer->vein_type[j]] += layer->vein_unk_38[j];
for (unsigned i = 0; i < geo_biome->layers.size(); i++)
for (unsigned j = 0; j < layer->vein_mat.size(); j++)
{
auto layer = geo_biome->layers[i];
// TODO: find out how to estimate the real density
// this code assumes that vein_unk_38 is the weight
// used when choosing the vein material
float size = float(layer->vein_unk_38[j]);
df::inclusion_type type = layer->vein_type[j];
layerMats[layer->mat_index].add(layer->bottom_height, 0);
switch (type)
{
case inclusion_type::VEIN:
// 3 veins of 80 tiles avg
size = size * 80 * 3 / sums[type];
break;
case inclusion_type::CLUSTER:
// 1 cluster of 700 tiles avg
size = size * 700 * 1 / sums[type];
break;
case inclusion_type::CLUSTER_SMALL:
size = size * 6 * 7 / sums[type];
break;
case inclusion_type::CLUSTER_ONE:
size = size * 1 * 5 / sums[type];
break;
default:
// shouldn't actually happen
size = 1;
}
int level_cnt = layer->top_height - layer->bottom_height + 1;
int layer_size = 48*48*cnt*level_cnt;
layer_size -= size;
int sums[(int)ENUM_LAST_ITEM(inclusion_type)+1] = { 0 };
add_materials(tile, veinMats[layer->vein_mat[j]], size, bottom_z, top_z);
}
for (unsigned j = 0; j < layer->vein_mat.size(); j++)
if (is_valid_enum_item<df::inclusion_type>(layer->vein_type[j]))
sums[layer->vein_type[j]] += layer->vein_unk_38[j];
add_materials(tile, layerMats[layer->mat_index], layer_size, bottom_z, top_z);
}
for (unsigned j = 0; j < layer->vein_mat.size(); j++)
{
// TODO: find out how to estimate the real density
// this code assumes that vein_unk_38 is the weight
// used when choosing the vein material
int size = layer->vein_unk_38[j]*cnt*level_cnt;
df::inclusion_type type = layer->vein_type[j];
return true;
}
switch (type)
{
case inclusion_type::VEIN:
// 3 veins of 80 tiles avg
size = size * 80 * 3 / sums[type];
break;
case inclusion_type::CLUSTER:
// 1 cluster of 700 tiles avg
size = size * 700 * 1 / sums[type];
break;
case inclusion_type::CLUSTER_SMALL:
size = size * 6 * 7 / sums[type];
break;
case inclusion_type::CLUSTER_ONE:
size = size * 1 * 5 / sums[type];
break;
default:
// shouldn't actually happen
size = cnt*level_cnt;
}
static command_result embark_prospector(color_ostream &out, df::viewscreen_choose_start_sitest *screen,
bool showHidden, bool showValue)
{
if (!world || !world->world_data)
{
out.printerr("World data is not available.\n");
return CR_FAILURE;
}
df::world_data *data = world->world_data;
coord2d cur_region = screen->region_pos;
int d_idx = linear_index(data->region_details, &df::world_region_details::pos, cur_region);
auto cur_details = vector_get(data->region_details, d_idx);
veinMats[layer->vein_mat[j]].add(layer->bottom_height, 0);
veinMats[layer->vein_mat[j]].add(layer->top_height, size);
if (!cur_details)
{
out.printerr("Current region details are not available.\n");
return CR_FAILURE;
}
layer_size -= size;
}
// Compute material maps
MatMap layerMats;
MatMap veinMats;
matdata world_bottom;
layerMats[layer->mat_index].add(layer->top_height, std::max(0,layer_size));
// Compute biomes
std::map<coord2d, int> biomes;
/*if (screen->biome_highlighted)
{
out.print("Processing one embark tile of biome F%d.\n\n", screen->biome_idx+1);
biomes[screen->biome_rgn[screen->biome_idx]]++;
}*/
for (int x = screen->embark_pos_min.x; x <= screen->embark_pos_max.x; x++)
{
for (int y = screen->embark_pos_min.y; y <= screen->embark_pos_max.y; y++)
{
EmbarkTileLayout tile;
if (!estimate_underground(out, tile, cur_details, x, y) ||
!estimate_materials(out, tile, layerMats, veinMats))
return CR_FAILURE;
world_bottom.add(tile.base_z, 0);
world_bottom.add(tile.elevation-1, 0);
}
}
@ -348,7 +484,10 @@ static command_result embark_prospector(color_ostream &out, df::viewscreen_choos
mats->Finish();
}
out << "Warning: the above data is only a very rough estimate." << std::endl;
out << "Embark depth: " << (world_bottom.upper_z-world_bottom.lower_z+1) << " ";
printMatdata(out, world_bottom, true);
out << std::endl << "Warning: the above data is only a very rough estimate." << std::endl;
return CR_OK;
}
@ -536,6 +675,8 @@ command_result prospector (color_ostream &con, vector <string> & parameters)
case tiletype_material::LAVA_STONE:
// TODO ?
break;
default:
break;
}
}
}

@ -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;
}

@ -0,0 +1,92 @@
building_steam_engine
[OBJECT:BUILDING]
[BUILDING_WORKSHOP:STEAM_ENGINE]
[NAME:Steam Engine]
[NAME_COLOR:4:0:1]
[DIM:3:3]
[WORK_LOCATION:2:3]
[BUILD_LABOR:MECHANIC]
[BUILD_KEY:CUSTOM_ALT_S]
[BLOCK:1:1:1:1]
[BLOCK:2:1:1:1]
[BLOCK:3:1:0:1]
[TILE:0:1:240:' ':254]
[TILE:0:2:' ':' ':128]
[TILE:0:3:246:' ':' ']
[COLOR:0:1:6:0:0:0:0:0:7:0:0]
[COLOR:0:2:0:0:0:0:0:0:7:0:0]
[COLOR:0:3:MAT:0:0:0:0:0:0]
[TILE:1:1:246:128:' ']
[TILE:1:2:' ':' ':254]
[TILE:1:3:254:'/':240]
[COLOR:1:1:MAT:7:0:0:0:0:0]
[COLOR:1:2:0:0:0:0:0:0:7:0:0]
[COLOR:1:3:7:0:0:6:0:0:6:0:0]
[TILE:2:1:21:' ':128]
[TILE:2:2:128:' ':246]
[TILE:2:3:177:19:177]
[COLOR:2:1:6:0:0:0:0:0:7:0:0]
[COLOR:2:2:7:0:0:0:0:0:MAT]
[COLOR:2:3:7:0:0:6:0:0:7:0:0]
Tile 15 marks places where machines can connect.
Tile 19 marks the hearth (color changed to reflect power).
[TILE:3:1:15:246:15]
[TILE:3:2:'\':19:'/']
[TILE:3:3:7:' ':7]
Color 1:?:1 water indicator, 4:?:1 magma indicator:
[COLOR:3:1:7:0:0:MAT:7:0:0]
[COLOR:3:2:6:0:0:0:0:1:6:0:0]
[COLOR:3:3:1:7:1:0:0:0:4:7:1]
[BUILD_ITEM:1:BARREL:NONE:INORGANIC:NONE][EMPTY][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:PIPE_SECTION:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON:INORGANIC:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:CHAIN:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:TRAPPARTS:NONE:NONE:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:BLOCKS:NONE:NONE:NONE][BUILDMAT][FIRE_BUILD_SAFE]
[BUILDING_WORKSHOP:MAGMA_STEAM_ENGINE]
[NAME:Magma Steam Engine]
[NAME_COLOR:4:0:1]
[DIM:3:3]
[WORK_LOCATION:2:3]
[BUILD_LABOR:MECHANIC]
[BUILD_KEY:CUSTOM_ALT_E]
[NEEDS_MAGMA]
[BLOCK:1:1:1:1]
[BLOCK:2:1:1:1]
[BLOCK:3:1:0:1]
[TILE:0:1:240:' ':254]
[TILE:0:2:' ':' ':128]
[TILE:0:3:246:' ':' ']
[COLOR:0:1:6:0:0:0:0:0:7:0:0]
[COLOR:0:2:0:0:0:0:0:0:7:0:0]
[COLOR:0:3:MAT:0:0:0:0:0:0]
[TILE:1:1:246:128:' ']
[TILE:1:2:' ':' ':254]
[TILE:1:3:254:'/':240]
[COLOR:1:1:MAT:7:0:0:0:0:0]
[COLOR:1:2:0:0:0:0:0:0:7:0:0]
[COLOR:1:3:7:0:0:6:0:0:6:0:0]
[TILE:2:1:21:' ':128]
[TILE:2:2:128:' ':246]
[TILE:2:3:177:19:177]
[COLOR:2:1:6:0:0:0:0:0:7:0:0]
[COLOR:2:2:7:0:0:0:0:0:MAT]
[COLOR:2:3:7:0:0:6:0:0:7:0:0]
Tile 15 marks places where machines can connect.
Tile 19 marks the hearth (color changed to reflect power).
[TILE:3:1:15:246:15]
[TILE:3:2:'\':19:'/']
[TILE:3:3:7:' ':7]
Color 1:?:1 water indicator, 4:?:1 magma indicator:
[COLOR:3:1:7:0:0:MAT:7:0:0]
[COLOR:3:2:6:0:0:0:0:1:6:0:0]
[COLOR:3:3:1:7:1:0:0:0:4:7:1]
[BUILD_ITEM:1:BARREL:NONE:INORGANIC:NONE][EMPTY][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:PIPE_SECTION:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON:INORGANIC:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:CHAIN:NONE:INORGANIC:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:TRAPPARTS:NONE:NONE:NONE][CAN_USE_ARTIFACT]
[BUILD_ITEM:1:BLOCKS:NONE:NONE:NONE][BUILDMAT][MAGMA_BUILD_SAFE]

@ -0,0 +1,12 @@
item_trapcomp_steam_engine
[OBJECT:ITEM]
[ITEM_TRAPCOMP:ITEM_TRAPCOMP_STEAM_PISTON]
[NAME:piston:pistons]
[ADJECTIVE:heavy]
[SIZE:1800]
[HITS:1]
[MATERIAL_SIZE:6]
[METAL]
[ATTACK:BLUNT:40:200:bash:bashes:NO_SUB:2000]

@ -0,0 +1,14 @@
reaction_steam_engine
[OBJECT:REACTION]
[REACTION:STOKE_BOILER]
[NAME:stoke the boiler]
[BUILDING:STEAM_ENGINE:CUSTOM_S]
[BUILDING:MAGMA_STEAM_ENGINE:CUSTOM_S]
[FUEL]
[SKILL:SMELT]
Dimension is the number of days it can produce 100 power * 100.
I.e. with 2000 it means energy of 1 job = 1 water wheel for 20 days.
[PRODUCT:100:1:LIQUID_MISC:NONE:WATER][PRODUCT_DIMENSION:2000]

@ -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 != "?")

File diff suppressed because it is too large Load Diff

@ -7,12 +7,14 @@
#include "PluginManager.h"
#include "modules/Gui.h"
#include "modules/Screen.h"
#include "modules/Units.h"
#include "modules/Items.h"
#include "MiscUtils.h"
#include "DataDefs.h"
#include <VTableInterpose.h>
#include "df/ui.h"
#include "df/world.h"
#include "df/squad.h"
@ -26,6 +28,12 @@
#include "df/death_info.h"
#include "df/criminal_case.h"
#include "df/unit_inventory_item.h"
#include "df/viewscreen_dwarfmodest.h"
#include "df/squad_order_trainst.h"
#include "df/ui_build_selector.h"
#include "df/building_trapst.h"
#include "df/item_actual.h"
#include "df/contaminant.h"
#include <stdlib.h>
@ -37,6 +45,9 @@ using namespace df::enums;
using df::global::ui;
using df::global::world;
using df::global::ui_build_selector;
using df::global::ui_menu_width;
using df::global::ui_area_map_width;
using namespace DFHack::Gui;
@ -67,6 +78,17 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector <Plugi
" (humans, elves) can be put to work, but you can't assign rooms\n"
" to them and they don't show up in DwarfTherapist because the\n"
" game treats them like pets.\n"
" tweak stable-cursor [disable]\n"
" Keeps exact position of dwarfmode cursor during exits to main menu.\n"
" E.g. allows switching between t/q/k/d without losing position.\n"
" tweak patrol-duty [disable]\n"
" Causes 'Train' orders to no longer be considered 'patrol duty' so\n"
" soldiers will stop getting unhappy thoughts. Does NOT fix the problem\n"
" when soldiers go off-duty (i.e. civilian).\n"
" tweak readable-build-plate [disable]\n"
" Fixes rendering of creature weight limits in pressure plate build menu.\n"
" tweak stable-temp [disable]\n"
" Fixes performance bug 6012 by squashing jitter in temperature updates.\n"
));
return CR_OK;
}
@ -76,9 +98,6 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out)
return CR_OK;
}
static command_result lair(color_ostream &out, std::vector<std::string> & params);
// to be called by tweak-fixmigrant
// units forced into the fort by removing the flags do not own their clothes
// which has the result that they drop all their clothes and become unhappy because they are naked
@ -136,6 +155,160 @@ command_result fix_clothing_ownership(color_ostream &out, df::unit* unit)
return CR_OK;
}
/*
* Save or restore cursor position on change to/from main dwarfmode menu.
*/
static df::coord last_view, last_cursor;
struct stable_cursor_hook : df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
bool was_default = (ui->main.mode == df::ui_sidebar_mode::Default);
df::coord view = Gui::getViewportPos();
df::coord cursor = Gui::getCursorPos();
INTERPOSE_NEXT(feed)(input);
bool is_default = (ui->main.mode == df::ui_sidebar_mode::Default);
df::coord cur_cursor = Gui::getCursorPos();
if (is_default && !was_default)
{
last_view = view; last_cursor = cursor;
}
else if (!is_default && was_default &&
Gui::getViewportPos() == last_view &&
last_cursor.isValid() && cur_cursor.isValid())
{
Gui::setCursorCoords(last_cursor.x, last_cursor.y, last_cursor.z);
// Force update of ui state
set<df::interface_key> tmp;
tmp.insert(interface_key::CURSOR_DOWN_Z);
INTERPOSE_NEXT(feed)(&tmp);
tmp.clear();
tmp.insert(interface_key::CURSOR_UP_Z);
INTERPOSE_NEXT(feed)(&tmp);
}
else if (cur_cursor.isValid())
{
last_cursor = df::coord();
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(stable_cursor_hook, feed);
struct patrol_duty_hook : df::squad_order_trainst
{
typedef df::squad_order_trainst interpose_base;
DEFINE_VMETHOD_INTERPOSE(bool, isPatrol, ())
{
return false;
}
};
IMPLEMENT_VMETHOD_INTERPOSE(patrol_duty_hook, isPatrol);
struct readable_build_plate_hook : df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
if (ui->main.mode == ui_sidebar_mode::Build &&
ui_build_selector->stage == 1 &&
ui_build_selector->building_type == building_type::Trap &&
ui_build_selector->building_subtype == trap_type::PressurePlate &&
ui_build_selector->plate_info.flags.bits.units)
{
auto dims = Gui::getDwarfmodeViewDims();
int x = dims.menu_x1;
Screen::Pen pen(' ',COLOR_WHITE);
int minv = ui_build_selector->plate_info.unit_min;
if ((minv % 1000) == 0)
Screen::paintString(pen, x+11, 14, stl_sprintf("%3dK ", minv/1000));
int maxv = ui_build_selector->plate_info.unit_max;
if (maxv < 200000 && (maxv % 1000) == 0)
Screen::paintString(pen, x+24, 14, stl_sprintf("%3dK ", maxv/1000));
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(readable_build_plate_hook, render);
struct stable_temp_hook : df::item_actual {
typedef df::item_actual interpose_base;
DEFINE_VMETHOD_INTERPOSE(bool, adjustTemperature, (uint16_t temp, int32_t rate_mult))
{
if (temperature != temp)
{
// Bug 6012 is caused by fixed-point precision mismatch jitter
// when an item is being pushed by two sources at N and N+1.
// This check suppresses it altogether.
if (temp == temperature+1 ||
(temp == temperature-1 && temperature_fraction == 0))
temp = temperature;
// When SPEC_HEAT is NONE, the original function seems to not
// change the temperature, yet return true, which is silly.
else if (getSpecHeat() == 60001)
temp = temperature;
}
return INTERPOSE_NEXT(adjustTemperature)(temp, rate_mult);
}
DEFINE_VMETHOD_INTERPOSE(bool, updateContaminants, ())
{
if (contaminants)
{
// Force 1-degree difference in contaminant temperature to 0
for (size_t i = 0; i < contaminants->size(); i++)
{
auto obj = (*contaminants)[i];
if (abs(obj->temperature - temperature) == 1)
{
obj->temperature = temperature;
obj->temperature_fraction = temperature_fraction;
}
}
}
return INTERPOSE_NEXT(updateContaminants)();
}
};
IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, adjustTemperature);
IMPLEMENT_VMETHOD_INTERPOSE(stable_temp_hook, updateContaminants);
static void enable_hook(color_ostream &out, VMethodInterposeLinkBase &hook, vector <string> &parameters)
{
if (vector_get(parameters, 1) == "disable")
{
hook.remove();
out.print("Disabled tweak %s\n", parameters[0].c_str());
}
else
{
if (hook.apply())
out.print("Enabled tweak %s\n", parameters[0].c_str());
else
out.printerr("Could not activate tweak %s\n", parameters[0].c_str());
}
}
static command_result tweak(color_ostream &out, vector <string> &parameters)
{
CoreSuspender suspend;
@ -234,6 +407,29 @@ static command_result tweak(color_ostream &out, vector <string> &parameters)
unit->profession2 = df::profession::TRADER;
return fix_clothing_ownership(out, unit);
}
else if (cmd == "stable-cursor")
{
enable_hook(out, INTERPOSE_HOOK(stable_cursor_hook, feed), parameters);
}
else if (cmd == "patrol-duty")
{
enable_hook(out, INTERPOSE_HOOK(patrol_duty_hook, isPatrol), parameters);
}
else if (cmd == "readable-build-plate")
{
if (!ui_build_selector || !ui_menu_width || !ui_area_map_width)
{
out.printerr("Necessary globals not known.\n");
return CR_FAILURE;
}
enable_hook(out, INTERPOSE_HOOK(readable_build_plate_hook, render), parameters);
}
else if (cmd == "stable-temp")
{
enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, adjustTemperature), parameters);
enable_hook(out, INTERPOSE_HOOK(stable_temp_hook, updateContaminants), parameters);
}
else
return CR_WRONG_USAGE;

@ -0,0 +1,14 @@
-- Prints memory ranges of the process.
for _,v in ipairs(dfhack.internal.getMemRanges()) do
local access = { '-', '-', '-', 'p' }
if v.read then access[1] = 'r' end
if v.write then access[2] = 'w' end
if v.execute then access[3] = 'x' end
if not v.valid then
access[4] = '?'
elseif v.shared then
access[4] = 's'
end
print(string.format('%08x-%08x %s %s', v.start_addr, v.end_addr, table.concat(access), v.name))
end

@ -0,0 +1,9 @@
-- Force a migrants event in next 10 ticks.
df.global.timed_events:insert('#',{
new = true,
type = df.timed_event_type.Migrants,
season = df.global.cur_season,
season_ticks = df.global.cur_season_tick+1,
entity = df.historical_entity.find(df.global.ui.civ_id)
})

@ -0,0 +1,3 @@
-- For killing bugged out gui script screens.
dfhack.screen.dismiss(dfhack.gui.getCurViewscreen())

@ -0,0 +1,40 @@
-- Communicates current population to mountainhomes to avoid cap overshooting.
-- The reason for population cap problems is that the population value it
-- is compared against comes from the last dwarven caravan that successfully
-- left for mountainhomes. This script instantly updates it.
-- Note that a migration wave can still overshoot the limit by 1-2 dwarves because
-- of the last migrant bringing his family. Likewise, king arrival ignores cap.
local args = {...}
local ui = df.global.ui
local ui_stats = ui.tasks
local civ = df.historical_entity.find(ui.civ_id)
if not civ then
qerror('No active fortress.')
end
local civ_stats = civ.activity_stats
if not civ_stats then
if args[1] ~= 'force' then
qerror('No caravan report object; use "fix/population-cap force" to create one')
end
print('Creating an empty statistics structure...')
civ.activity_stats = {
new = true,
created_weapons = { resize = #ui_stats.created_weapons },
known_creatures1 = { resize = #ui_stats.known_creatures1 },
known_creatures = { resize = #ui_stats.known_creatures },
known_plants1 = { resize = #ui_stats.known_plants1 },
known_plants = { resize = #ui_stats.known_plants },
}
civ_stats = civ.activity_stats
end
-- Use max to keep at least some of the original caravan communication idea
civ_stats.population = math.max(civ_stats.population, ui_stats.population)
print('Home civ notified about current population.')

@ -0,0 +1,302 @@
-- Interface front-end for liquids plugin.
local utils = require 'utils'
local gui = require 'gui'
local guidm = require 'gui.dwarfmode'
local dlg = require 'gui.dialogs'
local liquids = require('plugins.liquids')
local sel_rect = df.global.selection_rect
local brushes = {
{ tag = 'range', caption = 'Rectangle', range = true },
{ tag = 'block', caption = '16x16 block' },
{ tag = 'column', caption = 'Column' },
{ tag = 'flood', caption = 'Flood' },
}
local paints = {
{ tag = 'water', caption = 'Water', liquid = true, flow = true, key = 'w' },
{ tag = 'magma', caption = 'Magma', liquid = true, flow = true, key = 'l' },
{ tag = 'obsidian', caption = 'Obsidian Wall' },
{ tag = 'obsidian_floor', caption = 'Obsidian Floor' },
{ tag = 'riversource', caption = 'River Source' },
{ tag = 'flowbits', caption = 'Flow Updates', flow = true },
{ tag = 'wclean', caption = 'Clean Salt/Stagnant' },
}
local flowbits = {
{ tag = '+', caption = 'Enable Updates' },
{ tag = '-', caption = 'Disable Updates' },
{ tag = '.', caption = 'Keep Updates' },
}
local setmode = {
{ tag = '.', caption = 'Set Exactly' },
{ tag = '+', caption = 'Only Increase' },
{ tag = '-', caption = 'Only Decrease' },
}
local permaflows = {
{ tag = '.', caption = "Keep Permaflow" },
{ tag = '-', caption = 'Remove Permaflow' },
{ tag = 'N', caption = 'Set Permaflow N' },
{ tag = 'S', caption = 'Set Permaflow S' },
{ tag = 'E', caption = 'Set Permaflow E' },
{ tag = 'W', caption = 'Set Permaflow W' },
{ tag = 'NE', caption = 'Set Permaflow NE' },
{ tag = 'NW', caption = 'Set Permaflow NW' },
{ tag = 'SE', caption = 'Set Permaflow SE' },
{ tag = 'SW', caption = 'Set Permaflow SW' },
}
Toggle = defclass(Toggle)
function Toggle:init(items)
self:init_fields{
items = items,
selected = 1
}
return self
end
function Toggle:get()
return self.items[self.selected]
end
function Toggle:render(dc)
local item = self:get()
if item then
dc:string(item.caption)
if item.key then
dc:string(" ("):string(item.key, COLOR_LIGHTGREEN):string(")")
end
else
dc:string('NONE', COLOR_RED)
end
end
function Toggle:step(delta)
if #self.items > 1 then
delta = delta or 1
self.selected = 1 + (self.selected + delta - 1) % #self.items
end
end
LiquidsUI = defclass(LiquidsUI, guidm.MenuOverlay)
LiquidsUI.focus_path = 'liquids'
function LiquidsUI:init()
self:init_fields{
brush = mkinstance(Toggle):init(brushes),
paint = mkinstance(Toggle):init(paints),
flow = mkinstance(Toggle):init(flowbits),
set = mkinstance(Toggle):init(setmode),
permaflow = mkinstance(Toggle):init(permaflows),
amount = 7,
}
guidm.MenuOverlay.init(self)
return self
end
function LiquidsUI:onDestroy()
guidm.clearSelection()
end
function render_liquid(dc, block, x, y)
local dsgn = block.designation[x%16][y%16]
if dsgn.flow_size > 0 then
if dsgn.liquid_type == df.tile_liquid.Magma then
dc:pen(COLOR_RED):string("Magma")
else
dc:pen(COLOR_BLUE)
if dsgn.water_stagnant then dc:string("Stagnant ") end
if dsgn.water_salt then dc:string("Salty ") end
dc:string("Water")
end
dc:string(" ["..dsgn.flow_size.."/7]")
else
dc:string('No Liquid')
end
end
local permaflow_abbr = {
north = 'N', south = 'S', east = 'E', west = 'W',
northeast = 'NE', northwest = 'NW', southeast = 'SE', southwest = 'SW'
}
function render_flow_state(dc, block, x, y)
local flow = block.liquid_flow[x%16][y%16]
if block.flags.update_liquid then
dc:string("Updating", COLOR_GREEN)
else
dc:string("Static")
end
dc:string(", ")
if flow.perm_flow_dir ~= 0 then
local tag = df.tile_liquid_flow_dir[flow.perm_flow_dir]
dc:string("Permaflow "..(permaflow_abbr[tag] or tag), COLOR_CYAN)
elseif flow.temp_flow_timer > 0 then
dc:string("Flowing "..flow.temp_flow_timer, COLOR_GREEN)
else
dc:string("No Flow")
end
end
function LiquidsUI:onRenderBody(dc)
dc:clear():seek(1,1):string("Paint Liquids Cheat", COLOR_WHITE)
local cursor = guidm.getCursorPos()
local block = dfhack.maps.getTileBlock(cursor)
if block then
local x, y = pos2xyz(cursor)
local tile = block.tiletype[x%16][y%16]
dc:seek(2,3):string(df.tiletype.attrs[tile].caption, COLOR_CYAN)
dc:newline(2):pen(COLOR_DARKGREY)
render_liquid(dc, block, x, y)
dc:newline(2):pen(COLOR_DARKGREY)
render_flow_state(dc, block, x, y)
else
dc:seek(2,3):string("No map data", COLOR_RED):advance(0,2)
end
dc:newline():pen(COLOR_GREY)
dc:newline(1):string("b", COLOR_LIGHTGREEN):string(": ")
self.brush:render(dc)
dc:newline(1):string("p", COLOR_LIGHTGREEN):string(": ")
self.paint:render(dc)
local paint = self.paint:get()
dc:newline()
if paint.liquid then
dc:newline(1):string("Amount: "..self.amount)
dc:advance(1):string("("):string("-+", COLOR_LIGHTGREEN):string(")")
dc:newline(3):string("s", COLOR_LIGHTGREEN):string(": ")
self.set:render(dc)
else
dc:advance(0,2)
end
dc:newline()
if paint.flow then
dc:newline(1):string("f", COLOR_LIGHTGREEN):string(": ")
self.flow:render(dc)
dc:newline(1):string("r", COLOR_LIGHTGREEN):string(": ")
self.permaflow:render(dc)
else
dc:advance(0,2)
end
dc:newline():newline(1):pen(COLOR_WHITE)
dc:string("Esc", COLOR_LIGHTGREEN):string(": Back, ")
dc:string("Enter", COLOR_LIGHTGREEN):string(": Paint")
end
function ensure_blocks(cursor, size, cb)
local cx,cy,cz = pos2xyz(cursor)
local all = true
for x=1,size.x or 1,16 do
for y=1,size.y or 1,16 do
for z=1,size.z do
if not dfhack.maps.getTileBlock(cx+x-1, cy+y-1, cz+z-1) then
all = false
end
end
end
end
if all then
cb()
return
end
dlg.showYesNoPrompt(
'Instantiate Blocks',
'Not all map blocks are allocated - instantiate?\n\nWarning: new untested feature.',
COLOR_YELLOW,
function()
for x=1,size.x or 1,16 do
for y=1,size.y or 1,16 do
for z=1,size.z do
dfhack.maps.ensureTileBlock(cx+x-1, cy+y-1, cz+z-1)
end
end
end
cb()
end,
function()
cb()
end
)
end
function LiquidsUI:onInput(keys)
local paint = self.paint:get()
local liquid = paint.liquid
if keys.CUSTOM_B then
self.brush:step()
elseif keys.CUSTOM_P then
self.paint:step()
elseif liquid and keys.SECONDSCROLL_UP then
self.amount = math.max(0, self.amount-1)
elseif liquid and keys.SECONDSCROLL_DOWN then
self.amount = math.min(7, self.amount+1)
elseif liquid and keys.CUSTOM_S then
self.set:step()
elseif paint.flow and keys.CUSTOM_F then
self.flow:step()
elseif paint.flow and keys.CUSTOM_R then
self.permaflow:step()
elseif keys.LEAVESCREEN then
if guidm.getSelection() then
guidm.clearSelection()
return
end
self:dismiss()
self:sendInputToParent('CURSOR_DOWN_Z')
self:sendInputToParent('CURSOR_UP_Z')
elseif keys.SELECT then
local cursor = guidm.getCursorPos()
local sp = guidm.getSelection()
local size = nil
if self.brush:get().range then
if not sp then
guidm.setSelectionStart(cursor)
return
else
guidm.clearSelection()
cursor, size = guidm.getSelectionRange(cursor, sp)
end
else
guidm.clearSelection()
end
local cb = curry(
liquids.paint,
cursor,
self.brush:get().tag, self.paint:get().tag,
self.amount, size,
self.set:get().tag, self.flow:get().tag,
self.permaflow:get().tag
)
ensure_blocks(cursor, size, cb)
elseif self:propagateMoveKeys(keys) then
return
elseif keys.D_LOOK_ARENA_WATER then
self.paint.selected = 1
elseif keys.D_LOOK_ARENA_MAGMA then
self.paint.selected = 2
end
end
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/LookAround') then
qerror("This script requires the main dwarfmode view in 'k' mode")
end
local list = mkinstance(LiquidsUI):init()
list:show()

@ -122,7 +122,7 @@ function MechanismList:onInput(keys)
end
end
if dfhack.gui.getCurFocus() ~= 'dwarfmode/QueryBuilding/Some' then
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then
qerror("This script requires the main dwarfmode view in 'q' mode")
end

@ -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()

@ -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

@ -0,0 +1,308 @@
-- Front-end for the siege engine plugin.
local utils = require 'utils'
local gui = require 'gui'
local guidm = require 'gui.dwarfmode'
local dlg = require 'gui.dialogs'
local plugin = require 'plugins.siege-engine'
local wmap = df.global.world.map
-- Globals kept between script calls
last_target_min = last_target_min or nil
last_target_max = last_target_max or nil
SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay)
SiegeEngine.focus_path = 'siege-engine'
function SiegeEngine:init(building)
self:init_fields{
building = building,
center = utils.getBuildingCenter(building),
links = {}, selected = 1,
}
guidm.MenuOverlay.init(self)
self.mode_main = {
render = self:callback 'onRenderBody_main',
input = self:callback 'onInput_main',
}
self.mode_aim = {
render = self:callback 'onRenderBody_aim',
input = self:callback 'onInput_aim',
}
return self
end
function SiegeEngine:onShow()
guidm.MenuOverlay.onShow(self)
self.old_cursor = guidm.getCursorPos()
self.old_viewport = self:getViewport()
self.mode = self.mode_main
self:showCursor(false)
end
function SiegeEngine:onDestroy()
self:selectBuilding(self.building, self.old_cursor, self.old_viewport, 10)
end
function SiegeEngine:showCursor(enable)
local cursor = guidm.getCursorPos()
if cursor and not enable then
self.cursor = cursor
self.target_select_first = nil
guidm.clearCursorPos()
elseif not cursor and enable then
local view = self:getViewport()
cursor = self.cursor
if not cursor or not view:isVisible(cursor) then
cursor = view:getCenter()
end
self.cursor = nil
guidm.setCursorPos(cursor)
end
end
function SiegeEngine:centerViewOn(pos)
local cursor = guidm.getCursorPos()
if cursor then
guidm.setCursorPos(pos)
else
self.cursor = pos
end
self:getViewport():centerOn(pos):set()
end
function SiegeEngine:zoomToTarget()
local target_min, target_max = plugin.getTargetArea(self.building)
if target_min then
local cx = math.floor((target_min.x + target_max.x)/2)
local cy = math.floor((target_min.y + target_max.y)/2)
local cz = math.floor((target_min.z + target_max.z)/2)
for z = cz,target_max.z do
if plugin.getTileStatus(self.building, xyz2pos(cx,cy,z)) ~= 'blocked' then
cz = z
break
end
end
self:centerViewOn(xyz2pos(cx,cy,cz))
end
end
function paint_target_grid(dc, view, origin, p1, p2)
local r1, sz, r2 = guidm.getSelectionRange(p1, p2)
if view.z < r1.z or view.z > r2.z then
return
end
local p1 = view:tileToScreen(r1)
local p2 = view:tileToScreen(r2)
local org = view:tileToScreen(origin)
dc:pen{ fg = COLOR_CYAN, bg = COLOR_CYAN, ch = '+', bold = true }
-- Frame
dc:fill(p1.x,p1.y,p1.x,p2.y)
dc:fill(p1.x,p1.y,p2.x,p1.y)
dc:fill(p2.x,p1.y,p2.x,p2.y)
dc:fill(p1.x,p2.y,p2.x,p2.y)
-- Grid
local gxmin = org.x+10*math.ceil((p1.x-org.x)/10)
local gxmax = org.x+10*math.floor((p2.x-org.x)/10)
local gymin = org.y+10*math.ceil((p1.y-org.y)/10)
local gymax = org.y+10*math.floor((p2.y-org.y)/10)
for x = gxmin,gxmax,10 do
for y = gymin,gymax,10 do
dc:fill(p1.x,y,p2.x,y)
dc:fill(x,p1.y,x,p2.y)
end
end
end
function SiegeEngine:renderTargetView(target_min, target_max)
local view = self:getViewport()
local map = self.df_layout.map
local map_dc = gui.Painter.new(map)
plugin.paintAimScreen(
self.building, view:getPos(),
xy2pos(map.x1, map.y1), view:getSize()
)
if target_min and math.floor(dfhack.getTickCount()/500) % 2 == 0 then
paint_target_grid(map_dc, view, self.center, target_min, target_max)
end
local cursor = guidm.getCursorPos()
if cursor then
local cx, cy, cz = pos2xyz(view:tileToScreen(cursor))
if cz == 0 then
map_dc:seek(cx,cy):char('X', COLOR_YELLOW)
end
end
end
function SiegeEngine:onRenderBody_main(dc)
dc:newline(1):pen(COLOR_WHITE):string("Target: ")
local target_min, target_max = plugin.getTargetArea(self.building)
if target_min then
dc:string(
(target_max.x-target_min.x+1).."x"..
(target_max.y-target_min.y+1).."x"..
(target_max.z-target_min.z+1).." Rect"
)
else
dc:string("None (default)")
end
dc:newline(3):string("r",COLOR_LIGHTGREEN):string(": Rectangle")
if last_target_min then
dc:string(", "):string("p",COLOR_LIGHTGREEN):string(": Paste")
end
dc:newline(3)
if target_min then
dc:string("x",COLOR_LIGHTGREEN):string(": Clear, ")
dc:string("z",COLOR_LIGHTGREEN):string(": Zoom")
end
if self.target_select_first then
self:renderTargetView(self.target_select_first, guidm.getCursorPos())
else
self:renderTargetView(target_min, target_max)
end
end
function SiegeEngine:setTargetArea(p1, p2)
self.target_select_first = nil
if not plugin.setTargetArea(self.building, p1, p2) then
dlg.showMessage(
'Set Target Area',
'Could not set the target area', COLOR_LIGHTRED
)
else
last_target_min = p1
last_target_max = p2
end
end
function SiegeEngine:onInput_main(keys)
if keys.CUSTOM_R then
self:showCursor(true)
self.target_select_first = nil
self.mode = self.mode_aim
elseif keys.CUSTOM_P and last_target_min then
self:setTargetArea(last_target_min, last_target_max)
elseif keys.CUSTOM_Z then
self:zoomToTarget()
elseif keys.CUSTOM_X then
plugin.clearTargetArea(self.building)
elseif self:simulateViewScroll(keys) then
self.cursor = nil
else
return false
end
return true
end
local status_table = {
ok = { pen = COLOR_GREEN, msg = "Target accessible" },
out_of_range = { pen = COLOR_CYAN, msg = "Target out of range" },
blocked = { pen = COLOR_RED, msg = "Target obstructed" },
}
function SiegeEngine:onRenderBody_aim(dc)
local cursor = guidm.getCursorPos()
local first = self.target_select_first
dc:newline(1):string('Select target rectangle'):newline()
local info = status_table[plugin.getTileStatus(self.building, cursor)]
if info then
dc:newline(2):string(info.msg, info.pen)
else
dc:newline(2):string('ERROR', COLOR_RED)
end
dc:newline():newline(1):string("Enter",COLOR_LIGHTGREEN)
if first then
dc:string(": Finish rectangle")
else
dc:string(": Start rectangle")
end
dc:newline()
local target_min, target_max = plugin.getTargetArea(self.building)
if target_min then
dc:newline(1):string("z",COLOR_LIGHTGREEN):string(": Zoom to current target")
end
if first then
self:renderTargetView(first, cursor)
else
local target_min, target_max = plugin.getTargetArea(self.building)
self:renderTargetView(target_min, target_max)
end
end
function SiegeEngine:onInput_aim(keys)
if keys.SELECT then
local cursor = guidm.getCursorPos()
if self.target_select_first then
self:setTargetArea(self.target_select_first, cursor)
self.mode = self.mode_main
self:showCursor(false)
else
self.target_select_first = cursor
end
elseif keys.CUSTOM_Z then
self:zoomToTarget()
elseif keys.LEAVESCREEN then
self.mode = self.mode_main
self:showCursor(false)
elseif self:simulateCursorMovement(keys) then
self.cursor = nil
else
return false
end
return true
end
function SiegeEngine:onRenderBody(dc)
dc:clear()
dc:seek(1,1):pen(COLOR_WHITE):string(utils.getBuildingName(self.building)):newline()
self.mode.render(dc)
dc:seek(1, math.max(dc:localY(), 21)):pen(COLOR_WHITE)
dc:string("ESC", COLOR_LIGHTGREEN):string(": Back, ")
dc:string("c", COLOR_LIGHTGREEN):string(": Recenter")
end
function SiegeEngine:onInput(keys)
if self.mode.input(keys) then
--
elseif keys.CUSTOM_C then
self:centerViewOn(self.center)
elseif keys.LEAVESCREEN then
self:dismiss()
end
end
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/SiegeEngine') then
qerror("This script requires a siege engine selected in 'q' mode")
end
local building = df.global.world.selected_building
if not df.building_siegeenginest:is_instance(building) then
qerror("A siege engine must be selected")
end
local list = mkinstance(SiegeEngine):init(df.global.world.selected_building)
list:show()