Conflicts:
	scripts/autofarm.rb (resolved with added bug correction)
develop
Kelly Martin 2012-11-30 11:09:03 -06:00
commit 57233cb350
57 changed files with 2148 additions and 525 deletions

@ -2853,6 +2853,9 @@ this may be extended with mouse click support.</p>
<tr class="field"><th class="field-name">on_submit:</th><td class="field-body">Enter key callback; if specified, the list reacts to the key <tr class="field"><th class="field-name">on_submit:</th><td class="field-body">Enter key callback; if specified, the list reacts to the key
and calls it as <tt class="docutils literal">on_submit(index,choice)</tt>.</td> and calls it as <tt class="docutils literal">on_submit(index,choice)</tt>.</td>
</tr> </tr>
<tr class="field"><th class="field-name">on_submit2:</th><td class="field-body">Shift-Enter key callback; if specified, the list reacts to the key
and calls it as <tt class="docutils literal">on_submit2(index,choice)</tt>.</td>
</tr>
<tr class="field"><th class="field-name">row_height:</th><td class="field-body">Height of every row in text lines.</td> <tr class="field"><th class="field-name">row_height:</th><td class="field-body">Height of every row in text lines.</td>
</tr> </tr>
<tr class="field"><th class="field-name">icon_width:</th><td class="field-body">If not <em>nil</em>, the specified number of character columns <tr class="field"><th class="field-name">icon_width:</th><td class="field-body">If not <em>nil</em>, the specified number of character columns
@ -2908,6 +2911,9 @@ with the following fields:</p>
<li><p class="first"><tt class="docutils literal">list:submit()</tt></p> <li><p class="first"><tt class="docutils literal">list:submit()</tt></p>
<p>Call the <tt class="docutils literal">on_submit</tt> callback, as if the Enter key was handled.</p> <p>Call the <tt class="docutils literal">on_submit</tt> callback, as if the Enter key was handled.</p>
</li> </li>
<li><p class="first"><tt class="docutils literal">list:submit2()</tt></p>
<p>Call the <tt class="docutils literal">on_submit2</tt> callback, as if the Shift-Enter key was handled.</p>
</li>
</ul> </ul>
</div> </div>
<div class="section" id="filteredlist-class"> <div class="section" id="filteredlist-class">

@ -2710,6 +2710,16 @@ containing newlines, or a table with the following possible fields:
Specifies a pen to paint as one tile before the main part of the token. Specifies a pen to paint as one tile before the main part of the token.
* ``token.width = ...``
If specified either as a value or a callback, the text field is padded
or truncated to the specified number.
* ``token.pad_char = '?'``
If specified together with ``width``, the padding area is filled with
this character instead of just being skipped over.
* ``token.key = '...'`` * ``token.key = '...'``
Specifies the keycode associated with the token. The string description Specifies the keycode associated with the token. The string description
@ -2777,6 +2787,8 @@ It has the following attributes:
:on_select: Selection change callback; called as ``on_select(index,choice)``. :on_select: Selection change callback; called as ``on_select(index,choice)``.
:on_submit: Enter key callback; if specified, the list reacts to the key :on_submit: Enter key callback; if specified, the list reacts to the key
and calls it as ``on_submit(index,choice)``. and calls it as ``on_submit(index,choice)``.
:on_submit2: Shift-Enter key callback; if specified, the list reacts to the key
and calls it as ``on_submit2(index,choice)``.
:row_height: Height of every row in text lines. :row_height: Height of every row in text lines.
:icon_width: If not *nil*, the specified number of character columns :icon_width: If not *nil*, the specified number of character columns
are reserved to the left of the list item for the icons. are reserved to the left of the list item for the icons.
@ -2826,6 +2838,10 @@ The list supports the following methods:
Call the ``on_submit`` callback, as if the Enter key was handled. Call the ``on_submit`` callback, as if the Enter key was handled.
* ``list:submit2()``
Call the ``on_submit2`` callback, as if the Shift-Enter key was handled.
FilteredList class FilteredList class
------------------ ------------------
@ -2836,6 +2852,7 @@ In addition to passing through all attributes supported by List, it
supports: supports:
:edit_pen: If specified, used instead of ``cursor_pen`` for the edit field. :edit_pen: If specified, used instead of ``cursor_pen`` for the edit field.
:edit_below: If true, the edit field is placed below the list instead of above.
:not_found_label: Specifies the text of the label shown when no items match the filter. :not_found_label: Specifies the text of the label shown when no items match the filter.
The list choices may include the following attributes: The list choices may include the following attributes:

20
NEWS

@ -10,15 +10,18 @@ DFHack future
- fastdwarf: new mode using debug flags, and some internal consistency fixes. - fastdwarf: new mode using debug flags, and some internal consistency fixes.
- added a small stand-alone utility for applying and removing binary patches. - added a small stand-alone utility for applying and removing binary patches.
- removebadthoughts: add --dry-run option - removebadthoughts: add --dry-run option
New tweaks:
- tweak military-training: speed up melee squad training up to 10x (depends on unit count).
- superdwarf: work in adventure mode too - superdwarf: work in adventure mode too
- tweak stable-cursor: carries cursor location from/to Build menu.
New tweaks:
- tweak military-training: speed up melee squad training up to 10x (normally 3-5x).
New scripts: New scripts:
- binpatch: the same as the stand-alone binpatch.exe, but works at runtime. - binpatch: the same as the stand-alone binpatch.exe, but works at runtime.
- region-pops: displays animal populations of the region and allows tweaking them. - region-pops: displays animal populations of the region and allows tweaking them.
- lua: lua interpreter. - lua: lua interpreter front-end converted to a script from a native command.
- dfusion: misc scripts with a text based menu. - dfusion: misc scripts with a text based menu.
- embark: lets you embark anywhere. - embark: lets you embark anywhere.
- lever: list and pull fort levers from the dfhack console.
- stripcaged: mark items inside cages for dumping, eg caged goblin weapons.
New GUI scripts: New GUI scripts:
- gui/guide-path: displays the cached path for minecart Guide orders. - gui/guide-path: displays the cached path for minecart Guide orders.
- gui/workshop-job: displays inputs of a workshop job and allows tweaking them. - gui/workshop-job: displays inputs of a workshop job and allows tweaking them.
@ -26,6 +29,14 @@ DFHack future
- gui/assign-rack: works together with a binary patch to fix weapon racks. - gui/assign-rack: works together with a binary patch to fix weapon racks.
- gui/gm-editor: an universal editor for lots of dfhack things. - gui/gm-editor: an universal editor for lots of dfhack things.
- gui/companion-order: a adventure mode command interface for your companions. - gui/companion-order: a adventure mode command interface for your companions.
New binary patches:
- armorstand-capacity
- custom-reagent-size
- deconstruct-heapfall
- deconstruct-teleport
- hospital-overstocking
- training-ammo
- weaponrack-unassign
Workflow plugin: Workflow plugin:
- properly considers minecarts assigned to routes busy. - properly considers minecarts assigned to routes busy.
- code for deducing job outputs rewritten in lua for flexibility. - code for deducing job outputs rewritten in lua for flexibility.
@ -38,6 +49,9 @@ DFHack future
properly designated barracks be used again for storage of squad equipment. properly designated barracks be used again for storage of squad equipment.
New Search plugin by falconne: New Search plugin by falconne:
Adds an incremental search function to the Stocks, Trading and Unit List screens. Adds an incremental search function to the Stocks, Trading and Unit List screens.
New AutoMaterial plugin by falconne:
Makes building constructions (walls, floors, fortifications, etc) a little bit easier by
saving you from having to trawl through long lists of materials each time you place one.
Dfusion plugin: Dfusion plugin:
Reworked to make use of lua modules, now all the scripts can be used from other scripts. Reworked to make use of lua modules, now all the scripts can be used from other scripts.

File diff suppressed because it is too large Load Diff

@ -144,6 +144,16 @@ system console:
The patches are expected to be encoded in text format used by IDA. The patches are expected to be encoded in text format used by IDA.
Live patching
-------------
As an alternative, you can use the ``binpatch`` dfhack command to apply/remove
patches live in memory during a DF session.
In this case, updating symbols.xml is not necessary.
============================= =============================
Something doesn't work, help! Something doesn't work, help!
============================= =============================
@ -1077,6 +1087,9 @@ Subcommands that persist until disabled or DF quit:
:patrol-duty: Makes Train orders not count as patrol duty to stop unhappy thoughts. :patrol-duty: Makes Train orders not count as patrol duty to stop unhappy thoughts.
Does NOT fix the problem when soldiers go off-duty (i.e. civilian). Does NOT fix the problem when soldiers go off-duty (i.e. civilian).
:readable-build-plate: Fixes rendering of creature weight limits in pressure plate build menu. :readable-build-plate: Fixes rendering of creature weight limits in pressure plate build menu.
.. image:: images/tweak-plate.png
:stable-temp: Fixes performance bug 6012 by squashing jitter in temperature updates. :stable-temp: Fixes performance bug 6012 by squashing jitter in temperature updates.
In very item-heavy forts with big stockpiles this can improve FPS by 50-100% In very item-heavy forts with big stockpiles this can improve FPS by 50-100%
:fast-heat: Further improves temperature update performance by ensuring that 1 degree :fast-heat: Further improves temperature update performance by ensuring that 1 degree
@ -1095,9 +1108,16 @@ Subcommands that persist until disabled or DF quit:
:military-stable-assign: Preserve list order and cursor position when assigning to squad, :military-stable-assign: Preserve list order and cursor position when assigning to squad,
i.e. stop the rightmost list of the Positions page of the military i.e. stop the rightmost list of the Positions page of the military
screen from constantly resetting to the top. screen from constantly resetting to the top.
:military-color-assigned: Color squad candidates already assigned to other squads in brown/green :military-color-assigned: Color squad candidates already assigned to other squads in yellow/green
to make them stand out more in the list. to make them stand out more in the list.
.. image:: images/tweak-mil-color.png
:military-training: Speeds up melee squad training by removing an almost certainly
unintended inverse dependency of training speed on unit count
(i.e. the more units you have, the slower it becomes), and making
the units spar more.
fix-armory fix-armory
---------- ----------
@ -1946,6 +1966,41 @@ embark
====== ======
Allows to embark anywhere. Currently windows only. Allows to embark anywhere. Currently windows only.
lever
=====
Allow manipulation of in-game levers from the dfhack console.
Can list levers, including state and links, with::
lever list
To queue a job so that a dwarf will pull the lever 42, use ``lever pull 42``.
This is the same as 'q'uerying the building and queue a 'P'ull request.
To magically toggle the lever immediately, use::
lever pull 42 --now
stripcaged
==========
For dumping items inside cages. Will mark selected items for dumping, then
a dwarf may come and actually dump it. See also ``autodump``.
With the ``items`` argument, only dumps items laying in the cage, excluding
stuff worn by caged creatures. ``weapons`` will dump worn weapons, ``armor``
will dump everything worn by caged creatures (including armor and clothing),
and ``all`` will dump everything, on a creature or not.
``stripcaged list`` will display on the dfhack console the list of all cages
and their item content.
Without further arguments, all commands work on all cages and animal traps on
the map. With the ``here`` argument, considers only the in-game selected cage
(or the cage under the game cursor). To target only specific cages, you can
alternatively pass cage IDs as arguments::
stripcaged weapons 25321 34228
======================= =======================
In-game interface tools In-game interface tools
======================= =======================
@ -1958,6 +2013,9 @@ are mostly implemented by lua scripts.
In order to avoid user confusion, as a matter of policy all these tools In order to avoid user confusion, as a matter of policy all these tools
display the word "DFHack" on the screen somewhere while active. display the word "DFHack" on the screen somewhere while active.
When that is not appropriate because they merely add keybinding hints to
existing DF screens, they deliberately use red instead of green for the key.
As an exception, the tweak plugin described above does not follow this As an exception, the tweak plugin described above does not follow this
guideline because it arguably just fixes small usability bugs in the game UI. guideline because it arguably just fixes small usability bugs in the game UI.
@ -1974,8 +2032,12 @@ This tool implements a Dwarf Therapist-like interface within the game UI. The
far left column displays the unit's Happiness (color-coded based on its far left column displays the unit's Happiness (color-coded based on its
value), and the right half of the screen displays each dwarf's labor settings value), and the right half of the screen displays each dwarf's labor settings
and skill levels (0-9 for Dabbling thru Professional, A-E for Great thru Grand and skill levels (0-9 for Dabbling thru Professional, A-E for Great thru Grand
Master, and U-Z for Legendary thru Legendary+5). Cells with red backgrounds Master, and U-Z for Legendary thru Legendary+5).
denote skills not controlled by labors.
Cells with teal backgrounds denote skills not controlled by labors, e.g.
military and social skills.
.. image:: images/manipulator2.png
Use the arrow keys or number pad to move the cursor around, holding Shift to Use the arrow keys or number pad to move the cursor around, holding Shift to
move 10 tiles at a time. move 10 tiles at a time.
@ -2036,10 +2098,46 @@ Value numbers displayed by the screen. Because of this, pressing the 't'
key while search is active clears the search instead of executing the trade. key while search is active clears the search instead of executing the trade.
AutoMaterial
============
The automaterial plugin makes building constructions (walls, floors, fortifications,
etc) a little bit easier by saving you from having to trawl through long lists of
materials each time you place one.
Firstly, it moves the last used material for a given construction type to the top of
the list, if there are any left. So if you build a wall with chalk blocks, the next
time you place a wall the chalk blocks will be at the top of the list, regardless of
distance (it only does this in "grouped" mode, as individual item lists could be huge).
This should mean you can place most constructions without having to search for your
preferred material type.
.. image:: images/automaterial-mat.png
Pressing 'a' while highlighting any material will enable that material for "auto select"
for this construction type. You can enable multiple materials as autoselect. Now the next
time you place this type of construction, the plugin will automatically choose materials
for you from the kinds you enabled. If there is enough to satisfy the whole placement,
you won't be prompted with the material screen - the construction will be placed and you
will be back in the construction menu as if you did it manually.
When choosing the construction placement, you will see a couple of options:
.. image:: images/automaterial-pos.png
Use 'a' here to temporarily disable the material autoselection, e.g. if you need
to go to the material selection screen so you can toggle some materials on or off.
The other option (auto type selection, off by default) can be toggled on with 't'. If you
toggle this option on, instead of returning you to the main construction menu after selecting
materials, it returns you back to this screen. If you use this along with several autoselect
enabled materials, you should be able to place complex constructions more conveniently.
gui/liquids gui/liquids
=========== ===========
To use, bind to a key and activate in the 'k' mode. To use, bind to a key (the example config uses Alt-L) and activate in the 'k' mode.
.. image:: images/liquids.png .. image:: images/liquids.png
@ -2050,7 +2148,7 @@ to select the target area and apply changes.
gui/mechanisms gui/mechanisms
============== ==============
To use, bind to a key and activate in the 'q' mode. To use, bind to a key (the example config uses Ctrl-M) and activate in the 'q' mode.
.. image:: images/mechanisms.png .. image:: images/mechanisms.png
@ -2088,12 +2186,15 @@ via a simple dialog in the game ui.
The ``building`` or ``unit`` options are automatically assumed when in relevant ui state. The ``building`` or ``unit`` options are automatically assumed when in relevant ui state.
The example config binds building/unit rename to Ctrl-Shift-N, and
unit profession change to Ctrl-Shift-T.
gui/room-list gui/room-list
============= =============
To use, bind to a key and activate in the 'q' mode, either immediately or after opening To use, bind to a key (the example config uses Alt-R) and activate in the 'q' mode,
the assign owner page. either immediately or after opening the assign owner page.
.. image:: images/room-list.png .. image:: images/room-list.png
@ -2104,7 +2205,8 @@ list, and allows unassigning them.
gui/choose-weapons gui/choose-weapons
================== ==================
Bind to a key, and activate in the Equip->View/Customize page of the military screen. Bind to a key (the example config uses Ctrl-W), and activate in the Equip->View/Customize
page of the military screen.
Depending on the cursor location, it rewrites all 'individual choice weapon' entries Depending on the cursor location, it rewrites all 'individual choice weapon' entries
in the selected squad or position to use a specific weapon type matching the assigned in the selected squad or position to use a specific weapon type matching the assigned
@ -2118,7 +2220,8 @@ and may lead to inappropriate weapons being selected.
gui/guide-path gui/guide-path
============== ==============
Bind to a key, and activate in the Hauling menu with the cursor over a Guide order. Bind to a key (the example config uses Alt-P), and activate in the Hauling menu with
the cursor over a Guide order.
.. image:: images/guide-path.png .. image:: images/guide-path.png
@ -2129,7 +2232,8 @@ computes it when the order is executed for the first time.
gui/workshop-job gui/workshop-job
================ ================
Bind to a key, and activate with a job selected in a workshop in the 'q' mode. Bind to a key (the example config uses Alt-A), and activate with a job selected in
a workshop in the 'q' mode.
.. image:: images/workshop-job.png .. image:: images/workshop-job.png
@ -2176,7 +2280,8 @@ you have to unset the material first.
gui/workflow gui/workflow
============ ============
Bind to a key, and activate with a job selected in a workshop in the 'q' mode. Bind to a key (the example config uses Alt-W), and activate with a job selected
in a workshop in the 'q' mode.
.. image:: images/workflow.png .. image:: images/workflow.png
@ -2204,20 +2309,19 @@ can be used for troubleshooting jobs that don't match the right constraints.
.. image:: images/workflow-new1.png .. image:: images/workflow-new1.png
After selecting one of the presented outputs, the interface proceeds to the If you select one of the outputs with Enter, the matching constraint is simply
added to the list. If you use Shift-Enter, the interface proceeds to the
next dialog, which allows you to edit the suggested constraint parameters to next dialog, which allows you to edit the suggested constraint parameters to
suit your need, and set the item count range. suit your need, and set the item count range.
.. image:: images/workflow-new2.png .. image:: images/workflow-new2.png
If you don't need advanced settings, you can just press 'y' to confirm creation.
gui/assign-rack gui/assign-rack
=============== ===============
Bind to a key, and activate when viewing a weapon rack in the 'q' mode. Bind to a key (the example config uses P), and activate when viewing a weapon
rack in the 'q' mode.
.. image:: images/assign-rack.png .. image:: images/assign-rack.png
@ -2278,7 +2382,8 @@ Configuration UI
---------------- ----------------
The configuration front-end to the plugin is implemented by the gui/siege-engine The configuration front-end to the plugin is implemented by the gui/siege-engine
script. Bind it to a key and activate after selecting a siege engine in 'q' mode. script. Bind it to a key (the example config uses Alt-A) and activate after selecting
a siege engine in 'q' mode.
.. image:: images/siege-engine.png .. image:: images/siege-engine.png
@ -2310,7 +2415,8 @@ The power-meter plugin implements a modified pressure plate that detects power b
supplied to gear boxes built in the four adjacent N/S/W/E tiles. supplied to gear boxes built in the four adjacent N/S/W/E tiles.
The configuration front-end is implemented by the gui/power-meter script. Bind it to a The configuration front-end is implemented by the gui/power-meter script. Bind it to a
key and activate after selecting Pressure Plate in the build menu. key (the example config uses Ctrl-Shift-M) and activate after selecting Pressure Plate
in the build menu.
.. image:: images/power-meter.png .. image:: images/power-meter.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

@ -28,7 +28,9 @@ distribution.
#include "BitArray.h" #include "BitArray.h"
#include "ColorText.h" #include "ColorText.h"
#include "Types.h" #include "Types.h"
#include <string> #include <string>
#include <set>
#include "DataDefs.h" #include "DataDefs.h"
#include "df/graphic.h" #include "df/graphic.h"
@ -51,6 +53,8 @@ namespace DFHack
{ {
class Core; class Core;
typedef std::set<df::interface_key> interface_key_set;
/** /**
* The Screen module * The Screen module
* \ingroup grp_modules * \ingroup grp_modules

@ -81,6 +81,55 @@ namespace DFHack
int &ival(int i) { return int_values[i]; } int &ival(int i) { return int_values[i]; }
int ival(int i) const { return int_values[i]; } int ival(int i) const { return int_values[i]; }
// Pack binary data into string field.
// Since DF serialization chokes on NUL bytes,
// use bit magic to ensure none of the bytes is 0.
// Choose the lowest bit for padding so that
// sign-extend can be used normally.
size_t data_size() const { return str_value->size(); }
bool check_data(size_t off, size_t sz = 1) {
return (str_value->size() >= off+sz);
}
void ensure_data(size_t off, size_t sz = 0) {
if (str_value->size() < off+sz) str_value->resize(off+sz, '\x01');
}
uint8_t *pdata(size_t off) { return (uint8_t*)&(*str_value)[off]; }
static const size_t int7_size = 1;
uint8_t get_uint7(size_t off) {
uint8_t *p = pdata(off);
return p[0]>>1;
}
int8_t get_int7(size_t off) {
uint8_t *p = pdata(off);
return int8_t(p[0])>>1;
}
void set_uint7(size_t off, uint8_t val) {
uint8_t *p = pdata(off);
p[0] = uint8_t((val<<1) | 1);
}
void set_int7(size_t off, int8_t val) { set_uint7(off, val); }
static const size_t int28_size = 4;
uint32_t get_uint28(size_t off) {
uint8_t *p = pdata(off);
return (p[0]>>1) | ((p[1]&~1U)<<6) | ((p[2]&~1U)<<13) | ((p[3]&~1U)<<20);
}
int32_t get_int28(size_t off) {
uint8_t *p = pdata(off);
return (p[0]>>1) | ((p[1]&~1U)<<6) | ((p[2]&~1U)<<13) | ((int8_t(p[3])&~1)<<20);
}
void set_uint28(size_t off, uint32_t val) {
uint8_t *p = pdata(off);
p[0] = uint8_t((val<<1) | 1);
p[1] = uint8_t((val>>6) | 1);
p[2] = uint8_t((val>>13) | 1);
p[3] = uint8_t((val>>20) | 1);
}
void set_int28(size_t off, int32_t val) { set_uint28(off, val); }
PersistentDataItem() : id(0), str_value(0), int_values(0) {} PersistentDataItem() : id(0), str_value(0), int_values(0) {}
PersistentDataItem(int id, const std::string &key, std::string *sv, int *iv) PersistentDataItem(int id, const std::string &key, std::string *sv, int *iv)
: id(id), key_value(key), str_value(sv), int_values(iv) {} : id(id), key_value(key), str_value(sv), int_values(iv) {}

@ -10,13 +10,19 @@ local to_pen = dfhack.pen.parse
CLEAR_PEN = to_pen{ch=32,fg=0,bg=0} CLEAR_PEN = to_pen{ch=32,fg=0,bg=0}
local FAKE_INPUT_KEYS = {
_MOUSE_L = true,
_MOUSE_R = true,
_STRING = true,
}
function simulateInput(screen,...) function simulateInput(screen,...)
local keys = {} local keys = {}
local function push_key(arg) local function push_key(arg)
local kv = arg local kv = arg
if type(arg) == 'string' then if type(arg) == 'string' then
kv = df.interface_key[arg] kv = df.interface_key[arg]
if kv == nil then if kv == nil and not FAKE_INPUT_KEYS[arg] then
error('Invalid keycode: '..arg) error('Invalid keycode: '..arg)
end end
end end
@ -106,10 +112,14 @@ function inset_frame(rect, inset, gap)
return mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap) return mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
end end
function compute_frame_body(wavail, havail, spec, inset, gap) function compute_frame_body(wavail, havail, spec, inset, gap, inner_frame)
gap = gap or 0 gap = gap or 0
local l,t,r,b = parse_inset(inset) local l,t,r,b = parse_inset(inset)
local rect = compute_frame_rect(wavail, havail, spec, gap*2+l+r, gap*2+t+b) local xgap,ygap = 0,0
if inner_frame then
xgap,ygap = gap*2+l+r, gap*2+t+b
end
local rect = compute_frame_rect(wavail, havail, spec, xgap, ygap)
local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap) local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
return rect, body return rect, body
end end
@ -617,7 +627,7 @@ end
function FramedScreen:computeFrame(parent_rect) function FramedScreen:computeFrame(parent_rect)
local sw, sh = parent_rect.width, parent_rect.height local sw, sh = parent_rect.width, parent_rect.height
local fw, fh = self:getWantedFrameSize(parent_rect) local fw, fh = self:getWantedFrameSize(parent_rect)
return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1) return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1, true)
end end
function FramedScreen:onRenderFrame(dc, rect) function FramedScreen:onRenderFrame(dc, rect)

@ -152,7 +152,9 @@ ListBox.ATTRS{
with_filter = false, with_filter = false,
cursor_pen = DEFAULT_NIL, cursor_pen = DEFAULT_NIL,
select_pen = DEFAULT_NIL, select_pen = DEFAULT_NIL,
on_select = DEFAULT_NIL on_select = DEFAULT_NIL,
on_select2 = DEFAULT_NIL,
select2_hint = DEFAULT_NIL,
} }
function ListBox:preinit(info) function ListBox:preinit(info)
@ -168,6 +170,16 @@ function ListBox:init(info)
list_widget = widgets.FilteredList list_widget = widgets.FilteredList
end end
local on_submit2
if self.select2_hint or self.on_select2 then
on_submit2 = function(sel, obj)
self:dismiss()
if self.on_select2 then self.on_select2(sel, obj) end
local cb = obj.on_select2
if cb then cb(obj, sel) end
end
end
self:addviews{ self:addviews{
list_widget{ list_widget{
view_id = 'list', view_id = 'list',
@ -182,11 +194,19 @@ function ListBox:init(info)
local cb = obj.on_select or obj[2] local cb = obj.on_select or obj[2]
if cb then cb(obj, sel) end if cb then cb(obj, sel) end
end, end,
on_submit2 = on_submit2,
frame = { l = 0, r = 0 }, frame = { l = 0, r = 0 },
} }
} }
end end
function ListBox:onRenderFrame(dc,rect)
ListBox.super.onRenderFrame(self,dc,rect)
if self.select2_hint then
dc:seek(rect.x1+2,rect.y2):key('SEC_SELECT'):string(': '..self.select2_hint,COLOR_DARKGREY)
end
end
function ListBox:getWantedFrameSize() function ListBox:getWantedFrameSize()
local mw, mh = InputBox.super.getWantedFrameSize(self) local mw, mh = InputBox.super.getWantedFrameSize(self)
local list = self.subviews.list local list = self.subviews.list

@ -60,6 +60,7 @@ Panel = defclass(Panel, Widget)
Panel.ATTRS { Panel.ATTRS {
on_render = DEFAULT_NIL, on_render = DEFAULT_NIL,
on_layout = DEFAULT_NIL,
} }
function Panel:init(args) function Panel:init(args)
@ -70,6 +71,10 @@ function Panel:onRenderBody(dc)
if self.on_render then self.on_render(dc) end if self.on_render then self.on_render(dc) end
end end
function Panel:postComputeFrame(body)
if self.on_layout then self.on_layout(body) end
end
----------- -----------
-- Pages -- -- Pages --
----------- -----------
@ -242,7 +247,7 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled)
end end
if token.text or token.key then if token.text or token.key then
local text = getval(token.text) or '' local text = ''..(getval(token.text) or '')
local keypen local keypen
if dc then if dc then
@ -256,7 +261,23 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled)
end end
end end
x = x + #text local width = getval(token.width)
local padstr
if width then
x = x + width
if #text > width then
text = string.sub(text,1,width)
else
if token.pad_char then
padstr = string.rep(token.pad_char,width-#text)
end
if dc and token.rjustify then
if padstr then dc:string(padstr) else dc:advance(width-#text) end
end
end
else
x = x + #text
end
if token.key then if token.key then
local keystr = gui.getKeyDisplay(token.key) local keystr = gui.getKeyDisplay(token.key)
@ -281,6 +302,10 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled)
dc:string(text) dc:string(text)
end end
end end
if width and dc and not token.rjustify then
if padstr then dc:string(padstr) else dc:advance(width-#text) end
end
end end
token.x2 = x token.x2 = x
@ -384,6 +409,7 @@ List.ATTRS{
inactive_pen = DEFAULT_NIL, inactive_pen = DEFAULT_NIL,
on_select = DEFAULT_NIL, on_select = DEFAULT_NIL,
on_submit = DEFAULT_NIL, on_submit = DEFAULT_NIL,
on_submit2 = DEFAULT_NIL,
row_height = 1, row_height = 1,
scroll_keys = STANDARDSCROLL, scroll_keys = STANDARDSCROLL,
icon_width = DEFAULT_NIL, icon_width = DEFAULT_NIL,
@ -542,10 +568,19 @@ function List:submit()
end end
end end
function List:submit2()
if self.on_submit2 and #self.choices > 0 then
self.on_submit2(self:getSelected())
end
end
function List:onInput(keys) function List:onInput(keys)
if self.on_submit and keys.SELECT then if self.on_submit and keys.SELECT then
self:submit() self:submit()
return true return true
elseif self.on_submit2 and keys.SEC_SELECT then
self:submit2()
return true
else else
for k,v in pairs(self.scroll_keys) do for k,v in pairs(self.scroll_keys) do
if keys[k] then if keys[k] then
@ -581,10 +616,14 @@ end
FilteredList = defclass(FilteredList, Widget) FilteredList = defclass(FilteredList, Widget)
FilteredList.ATTRS {
edit_below = false,
}
function FilteredList:init(info) function FilteredList:init(info)
self.edit = EditField{ self.edit = EditField{
text_pen = info.edit_pen or info.cursor_pen, text_pen = info.edit_pen or info.cursor_pen,
frame = { l = info.icon_width, t = 0 }, frame = { l = info.icon_width, t = 0, h = 1 },
on_change = self:callback('onFilterChange'), on_change = self:callback('onFilterChange'),
on_char = self:callback('onFilterChar'), on_char = self:callback('onFilterChar'),
} }
@ -598,6 +637,10 @@ function FilteredList:init(info)
scroll_keys = info.scroll_keys, scroll_keys = info.scroll_keys,
icon_width = info.icon_width, icon_width = info.icon_width,
} }
if self.edit_below then
self.edit.frame = { l = info.icon_width, b = 0, h = 1 }
self.list.frame = { t = 0, b = 2 }
end
if info.on_select then if info.on_select then
self.list.on_select = function() self.list.on_select = function()
return info.on_select(self:getSelected()) return info.on_select(self:getSelected())
@ -608,11 +651,16 @@ function FilteredList:init(info)
return info.on_submit(self:getSelected()) return info.on_submit(self:getSelected())
end end
end end
if info.on_submit2 then
self.list.on_submit2 = function()
return info.on_submit2(self:getSelected())
end
end
self.not_found = Label{ self.not_found = Label{
visible = false, visible = false,
text = info.not_found_label or 'No matches', text = info.not_found_label or 'No matches',
text_pen = COLOR_LIGHTRED, text_pen = COLOR_LIGHTRED,
frame = { l = info.icon_width, t = 2 }, frame = { l = info.icon_width, t = self.list.frame.t },
} }
self:addviews{ self.edit, self.list, self.not_found } self:addviews{ self.edit, self.list, self.not_found }
self:setChoices(info.choices, info.selected) self:setChoices(info.choices, info.selected)
@ -634,6 +682,10 @@ function FilteredList:submit()
return self.list:submit() return self.list:submit()
end end
function FilteredList:submit2()
return self.list:submit2()
end
function FilteredList:canSubmit() function FilteredList:canSubmit()
return not self.not_found.visible return not self.not_found.visible
end end

@ -110,10 +110,10 @@ bool Screen::paintTile(const Pen &pen, int x, int y)
{ {
if (!gps || !pen.valid()) return false; if (!gps || !pen.valid()) return false;
int dimx = gps->dimx, dimy = gps->dimy; auto dim = getWindowSize();
if (x < 0 || x >= dimx || y < 0 || y >= dimy) return false; if (x < 0 || x >= dim.x || y < 0 || y >= dim.y) return false;
doSetTile(pen, x*dimy + y); doSetTile(pen, x*dim.y + y);
return true; return true;
} }
@ -121,11 +121,11 @@ Pen Screen::readTile(int x, int y)
{ {
if (!gps) return Pen(0,0,0,-1); if (!gps) return Pen(0,0,0,-1);
int dimx = gps->dimx, dimy = gps->dimy; auto dim = getWindowSize();
if (x < 0 || x >= dimx || y < 0 || y >= dimy) if (x < 0 || x >= dim.x || y < 0 || y >= dim.y)
return Pen(0,0,0,-1); return Pen(0,0,0,-1);
int index = x*dimy + y; int index = x*dim.y + y;
auto screen = gps->screen + index*4; auto screen = gps->screen + index*4;
if (screen[3] & 0x80) if (screen[3] & 0x80)
return Pen(0,0,0,-1); return Pen(0,0,0,-1);
@ -154,14 +154,15 @@ Pen Screen::readTile(int x, int y)
bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text) bool Screen::paintString(const Pen &pen, int x, int y, const std::string &text)
{ {
if (!gps || y < 0 || y >= gps->dimy) return false; auto dim = getWindowSize();
if (!gps || y < 0 || y >= dim.y) return false;
Pen tmp(pen); Pen tmp(pen);
bool ok = false; bool ok = false;
for (size_t i = -std::min(0,x); i < text.size(); i++) for (size_t i = -std::min(0,x); i < text.size(); i++)
{ {
if (x + i >= size_t(gps->dimx)) if (x + i >= size_t(dim.x))
break; break;
tmp.ch = text[i]; tmp.ch = text[i];
@ -175,17 +176,18 @@ 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) bool Screen::fillRect(const Pen &pen, int x1, int y1, int x2, int y2)
{ {
auto dim = getWindowSize();
if (!gps || !pen.valid()) return false; if (!gps || !pen.valid()) return false;
if (x1 < 0) x1 = 0; if (x1 < 0) x1 = 0;
if (y1 < 0) y1 = 0; if (y1 < 0) y1 = 0;
if (x2 >= gps->dimx) x2 = gps->dimx-1; if (x2 >= dim.x) x2 = dim.x-1;
if (y2 >= gps->dimy) y2 = gps->dimy-1; if (y2 >= dim.y) y2 = dim.y-1;
if (x1 > x2 || y1 > y2) return false; if (x1 > x2 || y1 > y2) return false;
for (int x = x1; x <= x2; x++) for (int x = x1; x <= x2; x++)
{ {
int index = x*gps->dimy; int index = x*dim.y;
for (int y = y1; y <= y2; y++) for (int y = y1; y <= y2; y++)
doSetTile(pen, index+y); doSetTile(pen, index+y);
@ -198,32 +200,33 @@ bool Screen::drawBorder(const std::string &title)
{ {
if (!gps) return false; if (!gps) return false;
int dimx = gps->dimx, dimy = gps->dimy; auto dim = getWindowSize();
Pen border('\xDB', 8); Pen border('\xDB', 8);
Pen text(0, 0, 7); Pen text(0, 0, 7);
Pen signature(0, 0, 8); Pen signature(0, 0, 8);
for (int x = 0; x < dimx; x++) for (int x = 0; x < dim.x; x++)
{ {
doSetTile(border, x * dimy + 0); doSetTile(border, x * dim.y + 0);
doSetTile(border, x * dimy + dimy - 1); doSetTile(border, x * dim.y + dim.y - 1);
} }
for (int y = 0; y < dimy; y++) for (int y = 0; y < dim.y; y++)
{ {
doSetTile(border, 0 * dimy + y); doSetTile(border, 0 * dim.y + y);
doSetTile(border, (dimx - 1) * dimy + y); doSetTile(border, (dim.x - 1) * dim.y + y);
} }
paintString(signature, dimx-8, dimy-1, "DFHack"); paintString(signature, dim.x-8, dim.y-1, "DFHack");
return paintString(text, (dimx - title.length()) / 2, 0, title); return paintString(text, (dim.x - title.length()) / 2, 0, title);
} }
bool Screen::clear() bool Screen::clear()
{ {
if (!gps) return false; if (!gps) return false;
return fillRect(Pen(' ',0,0,false), 0, 0, gps->dimx-1, gps->dimy-1); auto dim = getWindowSize();
return fillRect(Pen(' ',0,0,false), 0, 0, dim.x-1, dim.y-1);
} }
bool Screen::invalidate() bool Screen::invalidate()

@ -118,6 +118,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(forceequip forceequip.cpp) DFHACK_PLUGIN(forceequip forceequip.cpp)
DFHACK_PLUGIN(manipulator manipulator.cpp) DFHACK_PLUGIN(manipulator manipulator.cpp)
DFHACK_PLUGIN(search search.cpp) DFHACK_PLUGIN(search search.cpp)
DFHACK_PLUGIN(automaterial automaterial.cpp)
# this one exports functions to lua # this one exports functions to lua
DFHACK_PLUGIN(burrows burrows.cpp LINK_LIBRARIES lua) DFHACK_PLUGIN(burrows burrows.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(sort sort.cpp LINK_LIBRARIES lua) DFHACK_PLUGIN(sort sort.cpp LINK_LIBRARIES lua)

@ -161,7 +161,7 @@ static command_result autodump_main(color_ostream &out, vector <string> & parame
|| itm->flags.bits.in_building || itm->flags.bits.in_building
|| itm->flags.bits.in_chest || itm->flags.bits.in_chest
// || itm->flags.bits.in_inventory // || itm->flags.bits.in_inventory
|| itm->flags.bits.artifact1 || itm->flags.bits.artifact
) )
continue; continue;
@ -271,7 +271,7 @@ command_result df_autodump_destroy_item(color_ostream &out, vector <string> & pa
if (item->flags.bits.construction || if (item->flags.bits.construction ||
item->flags.bits.in_building || item->flags.bits.in_building ||
item->flags.bits.artifact1) item->flags.bits.artifact)
{ {
out.printerr("Choosing not to destroy buildings, constructions and artifacts.\n"); out.printerr("Choosing not to destroy buildings, constructions and artifacts.\n");
return CR_FAILURE; return CR_FAILURE;

@ -1556,7 +1556,7 @@ static int stockcheck(color_ostream &out, vector <string> & parameters)
#define F(x) bad_flags.bits.x = true; #define F(x) bad_flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect); F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader); F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(artifact1); F(in_building); F(construction); F(artifact);
F(spider_web); F(owned); F(in_job); F(spider_web); F(owned); F(in_job);
#undef F #undef F

@ -0,0 +1,378 @@
// Auto Material Select
#include <map>
#include <string>
#include <vector>
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <VTableInterpose.h>
// DF data structure definition headers
#include "DataDefs.h"
#include "MiscUtils.h"
#include "df/build_req_choice_genst.h"
#include "df/build_req_choice_specst.h"
#include "df/construction_type.h"
#include "df/item.h"
#include "df/ui.h"
#include "df/ui_build_selector.h"
#include "df/viewscreen_dwarfmodest.h"
#include "modules/Gui.h"
#include "modules/Screen.h"
using std::map;
using std::string;
using std::vector;
using namespace DFHack;
using namespace df::enums;
using df::global::gps;
using df::global::ui;
using df::global::ui_build_selector;
DFHACK_PLUGIN("automaterial");
struct MaterialDescriptor
{
df::item_type item_type;
int16_t item_subtype;
int16_t type;
int32_t index;
bool valid;
bool matches(const MaterialDescriptor &a) const
{
return a.valid && valid &&
a.type == type &&
a.index == index &&
a.item_type == item_type &&
a.item_subtype == item_subtype;
}
};
static map<int16_t, MaterialDescriptor> last_used_material;
static map<int16_t, MaterialDescriptor> last_moved_material;
static map< int16_t, vector<MaterialDescriptor> > preferred_materials;
static map< int16_t, df::interface_key > hotkeys;
static bool last_used_moved = false;
static bool auto_choose_materials = true;
static bool auto_choose_attempted = true;
static bool revert_to_last_used_type = false;
static command_result automaterial_cmd(color_ostream &out, vector <string> & parameters)
{
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}
static inline bool in_material_choice_stage()
{
return Gui::build_selector_hotkey(Core::getTopViewscreen()) &&
ui_build_selector->building_type == df::building_type::Construction &&
ui->main.mode == ui_sidebar_mode::Build &&
ui_build_selector->stage == 2;
}
static inline bool in_placement_stage()
{
return Gui::dwarfmode_hotkey(Core::getTopViewscreen()) &&
ui->main.mode == ui_sidebar_mode::Build &&
ui_build_selector &&
ui_build_selector->building_type == df::building_type::Construction &&
ui_build_selector->stage == 1;
}
static inline bool in_type_choice_stage()
{
return Gui::dwarfmode_hotkey(Core::getTopViewscreen()) &&
ui->main.mode == ui_sidebar_mode::Build &&
ui_build_selector &&
ui_build_selector->building_type < 0;
}
static inline vector<MaterialDescriptor> &get_curr_constr_prefs()
{
if (preferred_materials.find(ui_build_selector->building_subtype) == preferred_materials.end())
preferred_materials[ui_build_selector->building_subtype] = vector<MaterialDescriptor>();
return preferred_materials[ui_build_selector->building_subtype];
}
static inline MaterialDescriptor &get_last_used_material()
{
if (last_used_material.find(ui_build_selector->building_subtype) == last_used_material.end())
last_used_material[ui_build_selector->building_subtype] = MaterialDescriptor();
return last_used_material[ui_build_selector->building_subtype];
}
static void set_last_used_material(const MaterialDescriptor &matetial)
{
last_used_material[ui_build_selector->building_subtype] = matetial;
}
static MaterialDescriptor &get_last_moved_material()
{
if (last_moved_material.find(ui_build_selector->building_subtype) == last_moved_material.end())
last_moved_material[ui_build_selector->building_subtype] = MaterialDescriptor();
return last_moved_material[ui_build_selector->building_subtype];
}
static void set_last_moved_material(const MaterialDescriptor &matetial)
{
last_moved_material[ui_build_selector->building_subtype] = matetial;
}
static MaterialDescriptor get_material_in_list(size_t i)
{
MaterialDescriptor result;
result.valid = false;
if (VIRTUAL_CAST_VAR(gen, df::build_req_choice_genst, ui_build_selector->choices[i]))
{
result.item_type = gen->item_type;
result.item_subtype = gen->item_subtype;
result.type = gen->mat_type;
result.index = gen->mat_index;
result.valid = true;
}
else if (VIRTUAL_CAST_VAR(spec, df::build_req_choice_specst, ui_build_selector->choices[i]))
{
result.item_type = gen->item_type;
result.item_subtype = gen->item_subtype;
result.type = spec->candidate->getActualMaterial();
result.index = spec->candidate->getActualMaterialIndex();
result.valid = true;
}
return result;
}
static bool is_material_in_autoselect(size_t &i, MaterialDescriptor &material)
{
for (i = 0; i < get_curr_constr_prefs().size(); i++)
{
if (get_curr_constr_prefs()[i].matches(material))
return true;
}
return false;
}
static bool is_material_in_list(size_t &i, MaterialDescriptor &material)
{
const size_t size = ui_build_selector->choices.size(); //Just because material list could be very big
for (i = 0; i < size; i++)
{
if (get_material_in_list(i).matches(material))
return true;
}
return false;
}
static bool move_material_to_top(MaterialDescriptor &material)
{
size_t i;
if (is_material_in_list(i, material))
{
auto sel_item = ui_build_selector->choices[i];
ui_build_selector->choices.erase(ui_build_selector->choices.begin() + i);
ui_build_selector->choices.insert(ui_build_selector->choices.begin(), sel_item);
ui_build_selector->sel_index = 0;
set_last_moved_material(material);
return true;
}
set_last_moved_material(MaterialDescriptor());
return false;
}
static bool check_autoselect(MaterialDescriptor &material, bool toggle)
{
size_t idx;
if (is_material_in_autoselect(idx, material))
{
if (toggle)
vector_erase_at(get_curr_constr_prefs(), idx);
return true;
}
else
{
if (toggle)
get_curr_constr_prefs().push_back(material);
return false;
}
}
struct jobutils_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
bool choose_materials()
{
size_t size = ui_build_selector->choices.size();
for (size_t i = 0; i < size; i++)
{
MaterialDescriptor material = get_material_in_list(i);
size_t j;
if (is_material_in_autoselect(j, material))
{
ui_build_selector->sel_index = i;
std::set< df::interface_key > keys;
keys.insert(df::interface_key::SELECT_ALL);
this->feed(&keys);
if (!in_material_choice_stage())
return true;
}
}
return false;
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
if (in_material_choice_stage())
{
MaterialDescriptor material = get_material_in_list(ui_build_selector->sel_index);
if (material.valid)
{
if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT))
{
if (get_last_moved_material().matches(material))
last_used_moved = false;
set_last_used_material(material);
}
else if (input->count(interface_key::CUSTOM_A))
{
check_autoselect(material, true);
input->clear();
}
}
}
else if (in_placement_stage())
{
if (input->count(interface_key::CUSTOM_A))
{
auto_choose_materials = !auto_choose_materials;
}
else if (input->count(interface_key::CUSTOM_T))
{
revert_to_last_used_type = !revert_to_last_used_type;
}
}
int16_t last_used_constr_subtype = (in_material_choice_stage()) ? ui_build_selector->building_subtype : -1;
INTERPOSE_NEXT(feed)(input);
if (revert_to_last_used_type &&
last_used_constr_subtype >= 0 &&
!in_material_choice_stage() &&
hotkeys.find(last_used_constr_subtype) != hotkeys.end())
{
interface_key_set keys;
keys.insert(hotkeys[last_used_constr_subtype]);
INTERPOSE_NEXT(feed)(&keys);
}
}
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
if (in_material_choice_stage())
{
if (!last_used_moved)
{
if (auto_choose_materials && get_curr_constr_prefs().size() > 0)
{
last_used_moved = true;
if (choose_materials())
{
return;
}
}
else if (ui_build_selector->is_grouped)
{
last_used_moved = true;
move_material_to_top(get_last_used_material());
}
}
else if (!ui_build_selector->is_grouped)
{
last_used_moved = false;
}
}
else
{
last_used_moved = false;
}
INTERPOSE_NEXT(render)();
if (in_material_choice_stage())
{
MaterialDescriptor material = get_material_in_list(ui_build_selector->sel_index);
if (material.valid)
{
string title = "Disabled";
if (check_autoselect(material, false))
{
title = "Enabled";
}
auto dims = Gui::getDwarfmodeViewDims();
Screen::Painter dc(dims.menu());
dc.seek(1,24).key_pen(COLOR_LIGHTRED).pen(COLOR_WHITE);
dc.key(interface_key::CUSTOM_A).string(": Autoselect "+title);
}
}
else if (in_placement_stage() && ui_build_selector->building_subtype < construction_type::TrackN)
{
string autoselect_toggle = (auto_choose_materials) ? "Disable" : "Enable";
string revert_toggle = (revert_to_last_used_type) ? "Disable" : "Enable";
auto dims = Gui::getDwarfmodeViewDims();
Screen::Painter dc(dims.menu());
dc.seek(1,23).key_pen(COLOR_LIGHTRED).pen(COLOR_WHITE);
dc.key(interface_key::CUSTOM_A).string(": "+autoselect_toggle+" Auto Mat-Select").newline(1);
dc.key(interface_key::CUSTOM_T).string(": "+revert_toggle+" Auto Type-Select");
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(jobutils_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(jobutils_hook, render);
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
if (!gps || !ui_build_selector ||
!INTERPOSE_HOOK(jobutils_hook, feed).apply() ||
!INTERPOSE_HOOK(jobutils_hook, render).apply())
out.printerr("Could not insert jobutils hooks!\n");
hotkeys[construction_type::Wall] = df::interface_key::HOTKEY_BUILDING_CONSTRUCTION_WALL;
hotkeys[construction_type::Floor] = df::interface_key::HOTKEY_BUILDING_CONSTRUCTION_FLOOR;
hotkeys[construction_type::Ramp] = df::interface_key::HOTKEY_BUILDING_CONSTRUCTION_RAMP;
hotkeys[construction_type::UpStair] = df::interface_key::HOTKEY_BUILDING_CONSTRUCTION_STAIR_UP;
hotkeys[construction_type::DownStair] = df::interface_key::HOTKEY_BUILDING_CONSTRUCTION_STAIR_DOWN;
hotkeys[construction_type::UpDownStair] = df::interface_key::HOTKEY_BUILDING_CONSTRUCTION_STAIR_UPDOWN;
hotkeys[construction_type::Fortification] = df::interface_key::HOTKEY_BUILDING_CONSTRUCTION_FORTIFICATION;
//Ignore tracks, DF already returns to track menu
return CR_OK;
}

@ -144,7 +144,7 @@ static command_result stockcheck(color_ostream &out, vector <string> & parameter
#define F(x) bad_flags.bits.x = true; #define F(x) bad_flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect); F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader); F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(artifact1); F(in_building); F(construction); F(artifact);
F(spider_web); F(owned); F(in_job); F(spider_web); F(owned); F(in_job);
#undef F #undef F

@ -8,10 +8,11 @@ local utils = require 'utils'
* isEnabled() * isEnabled()
* setEnabled(enable) * setEnabled(enable)
* listConstraints([job]) -> {...} * listConstraints([job[,with_history]]) -> {{...},...}
* findConstraint(token) -> {...} or nil * findConstraint(token) -> {...} or nil
* setConstraint(token[, by_count, goal, gap]) -> {...} * setConstraint(token[, by_count, goal, gap]) -> {...}
* deleteConstraint(token) -> true/false * deleteConstraint(token) -> true/false
* getCountHistory(token) -> {{...},...} or nil
--]] --]]

@ -456,21 +456,23 @@ void viewscreen_unitlaborsst::refreshNames()
void viewscreen_unitlaborsst::calcSize() void viewscreen_unitlaborsst::calcSize()
{ {
num_rows = gps->dimy - 10; auto dim = Screen::getWindowSize();
num_rows = dim.y - 10;
if (num_rows > units.size()) if (num_rows > units.size())
num_rows = units.size(); num_rows = units.size();
int num_columns = gps->dimx - DISP_COLUMN_MAX - 1; int num_columns = dim.x - DISP_COLUMN_MAX - 1;
// min/max width of columns // min/max width of columns
int col_minwidth[DISP_COLUMN_MAX]; int col_minwidth[DISP_COLUMN_MAX];
int col_maxwidth[DISP_COLUMN_MAX]; int col_maxwidth[DISP_COLUMN_MAX];
col_minwidth[DISP_COLUMN_HAPPINESS] = 4; col_minwidth[DISP_COLUMN_HAPPINESS] = 4;
col_maxwidth[DISP_COLUMN_HAPPINESS] = 4; col_maxwidth[DISP_COLUMN_HAPPINESS] = 4;
col_minwidth[DISP_COLUMN_NAME] = 0; col_minwidth[DISP_COLUMN_NAME] = 16;
col_maxwidth[DISP_COLUMN_NAME] = 0; col_maxwidth[DISP_COLUMN_NAME] = 16; // adjusted in the loop below
col_minwidth[DISP_COLUMN_PROFESSION] = 0; col_minwidth[DISP_COLUMN_PROFESSION] = 10;
col_maxwidth[DISP_COLUMN_PROFESSION] = 0; col_maxwidth[DISP_COLUMN_PROFESSION] = 10; // adjusted in the loop below
col_minwidth[DISP_COLUMN_LABORS] = num_columns*3/5; // 60% col_minwidth[DISP_COLUMN_LABORS] = num_columns*3/5; // 60%
col_maxwidth[DISP_COLUMN_LABORS] = NUM_COLUMNS; col_maxwidth[DISP_COLUMN_LABORS] = NUM_COLUMNS;
@ -840,7 +842,7 @@ void viewscreen_unitlaborsst::feed(set<df::interface_key> *events)
{ {
df::unit *unit = cur->unit; df::unit *unit = cur->unit;
const SkillColumn &col = columns[input_column]; const SkillColumn &col = columns[input_column];
bool newstatus = !unit->status.labors[col.labor]; bool newstatus = (col.labor == unit_labor::NONE) ? true : !unit->status.labors[col.labor];
for (int i = 0; i < NUM_COLUMNS; i++) for (int i = 0; i < NUM_COLUMNS; i++)
{ {
if (columns[i].group != col.group) if (columns[i].group != col.group)
@ -940,10 +942,11 @@ void viewscreen_unitlaborsst::render()
dfhack_viewscreen::render(); dfhack_viewscreen::render();
auto dim = Screen::getWindowSize();
Screen::clear(); Screen::clear();
Screen::drawBorder(" Dwarf Manipulator - Manage Labors "); Screen::drawBorder(" Dwarf Manipulator - Manage Labors ");
Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_HAPPINESS], 2, "Hap."); Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_HAPPINESS], 2, "Hap.");
Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_NAME], 2, "Name"); Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_NAME], 2, "Name");
Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_PROFESSION], 2, "Profession"); Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_PROFESSION], 2, "Profession");
@ -1057,7 +1060,7 @@ void viewscreen_unitlaborsst::render()
} }
} }
else else
bg = 4; bg = 3;
Screen::paintTile(Screen::Pen(c, fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 4 + row); Screen::paintTile(Screen::Pen(c, fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 4 + row);
} }
} }
@ -1116,48 +1119,48 @@ void viewscreen_unitlaborsst::render()
canToggle = (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE); canToggle = (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE);
} }
int x = 2; int x = 2, y = dim.y - 3;
OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::SELECT)); OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SELECT));
OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle labor, "); OutputString(canToggle ? 15 : 8, x, y, ": Toggle labor, ");
OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::SELECT_ALL)); OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SELECT_ALL));
OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle Group, "); OutputString(canToggle ? 15 : 8, x, y, ": Toggle Group, ");
OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::UNITJOB_VIEW)); OutputString(10, x, y, Screen::getKeyDisplay(interface_key::UNITJOB_VIEW));
OutputString(15, x, gps->dimy - 3, ": ViewCre, "); OutputString(15, x, y, ": ViewCre, ");
OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::UNITJOB_ZOOM_CRE)); OutputString(10, x, y, Screen::getKeyDisplay(interface_key::UNITJOB_ZOOM_CRE));
OutputString(15, x, gps->dimy - 3, ": Zoom-Cre"); OutputString(15, x, y, ": Zoom-Cre");
x = 2; x = 2; y = dim.y - 2;
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::LEAVESCREEN)); OutputString(10, x, y, Screen::getKeyDisplay(interface_key::LEAVESCREEN));
OutputString(15, x, gps->dimy - 2, ": Done, "); OutputString(15, x, y, ": Done, ");
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_DOWN)); OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SECONDSCROLL_DOWN));
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_UP)); OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SECONDSCROLL_UP));
OutputString(15, x, gps->dimy - 2, ": Sort by Skill, "); OutputString(15, x, y, ": Sort by Skill, ");
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_PAGEDOWN)); OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SECONDSCROLL_PAGEDOWN));
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_PAGEUP)); OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SECONDSCROLL_PAGEUP));
OutputString(15, x, gps->dimy - 2, ": Sort by ("); OutputString(15, x, y, ": Sort by (");
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::CHANGETAB)); OutputString(10, x, y, Screen::getKeyDisplay(interface_key::CHANGETAB));
OutputString(15, x, gps->dimy - 2, ") "); OutputString(15, x, y, ") ");
switch (altsort) switch (altsort)
{ {
case ALTSORT_NAME: case ALTSORT_NAME:
OutputString(15, x, gps->dimy - 2, "Name"); OutputString(15, x, y, "Name");
break; break;
case ALTSORT_PROFESSION: case ALTSORT_PROFESSION:
OutputString(15, x, gps->dimy - 2, "Profession"); OutputString(15, x, y, "Profession");
break; break;
case ALTSORT_HAPPINESS: case ALTSORT_HAPPINESS:
OutputString(15, x, gps->dimy - 2, "Happiness"); OutputString(15, x, y, "Happiness");
break; break;
case ALTSORT_ARRIVAL: case ALTSORT_ARRIVAL:
OutputString(15, x, gps->dimy - 2, "Arrival"); OutputString(15, x, y, "Arrival");
break; break;
default: default:
OutputString(15, x, gps->dimy - 2, "Unknown"); OutputString(15, x, y, "Unknown");
break; break;
} }
} }
@ -1193,9 +1196,10 @@ struct unitlist_hook : df::viewscreen_unitlistst
if (units[page].size()) if (units[page].size())
{ {
int x = 2; auto dim = Screen::getWindowSize();
OutputString(12, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::UNITVIEW_PRF_PROF)); int x = 2, y = dim.y - 2;
OutputString(15, x, gps->dimy - 2, ": Manage labors (DFHack)"); OutputString(12, x, y, Screen::getKeyDisplay(interface_key::UNITVIEW_PRF_PROF));
OutputString(15, x, y, ": Manage labors (DFHack)");
} }
} }
}; };

@ -125,9 +125,9 @@ DFHack callbacks
The plugin interfaces with dfhack 'onupdate' hook. The plugin interfaces with dfhack 'onupdate' hook.
To register ruby code to be run every graphic frame, use: To register ruby code to be run every graphic frame, use:
handle = df.onupdate_register { puts 'i love flooding the console' } handle = df.onupdate_register('log') { puts 'i love flooding the console' }
You can also rate-limit when your callback is called to a number of game ticks: You can also rate-limit when your callback is called to a number of game ticks:
handle = df.onupdate_register(10) { puts '10 more in-game ticks elapsed' } handle = df.onupdate_register('myname', 10) { puts '10 more in-game ticks elapsed' }
In this case, the callback is called immediately, and then every X in-game In this case, the callback is called immediately, and then every X in-game
ticks (advances only when the game is unpaused). ticks (advances only when the game is unpaused).
To stop being called, use: To stop being called, use:

@ -56,7 +56,7 @@ module DFHack
def item_isfree(i) def item_isfree(i)
!i.flags.trader and !i.flags.trader and
!i.flags.in_job and !i.flags.in_job and
!i.flags.in_inventory and (!i.flags.in_inventory or i.general_refs.grep(GeneralRefContainedInItemst).first) and
!i.flags.removed and !i.flags.removed and
!i.flags.in_building and !i.flags.in_building and
!i.flags.owned and !i.flags.owned and

@ -159,8 +159,8 @@ module DFHack
v.inorganic_mat if v v.inorganic_mat if v
end end
# return the world_data.geo_biome for current tile # return the RegionMapEntry (from designation.biome)
def geo_biome def region_map_entry
b = designation.biome b = designation.biome
wd = df.world.world_data wd = df.world.world_data
@ -174,7 +174,12 @@ module DFHack
ry -= 1 if b < 3 and ry > 0 ry -= 1 if b < 3 and ry > 0
ry += 1 if b > 5 and ry < wd.world_height-1 ry += 1 if b > 5 and ry < wd.world_height-1
wd.geo_biomes[ wd.region_map[rx][ry].geo_index ] wd.region_map[rx][ry]
end
# return the world_data.geo_biome for current tile
def geo_biome
df.world.world_data.geo_biomes[ region_map_entry.geo_index ]
end end
# return the world_data.geo_biome.layer for current tile # return the world_data.geo_biome.layer for current tile
@ -182,14 +187,53 @@ module DFHack
geo_biome.layers[designation.geolayer_index] geo_biome.layers[designation.geolayer_index]
end end
# current tile mat_index (vein if applicable, or base material) # MaterialInfo: token for current tile, based on tilemat (vein, soil, plant, lava_stone...)
def mat_index def mat_info
mat_index_vein or stone_layer.mat_index case tilemat
when :SOIL
base = stone_layer
if !df.world.raws.inorganics[base.mat_index].flags[:SOIL_ANY]
base = geo_biome.layers.find_all { |l| df.world.raws.inorganics[l.mat_index].flags[:SOIL_ANY] }.last
end
mat_index = (base ? base.mat_index : rand(df.world.raws.inorganics.length))
MaterialInfo.new(0, mat_index)
when :STONE
base = stone_layer
if df.world.raws.inorganics[base.mat_index].flags[:SOIL_ANY]
base = geo_biome.layers.find { |l| !df.world.raws.inorganics[l.mat_index].flags[:SOIL_ANY] }
end
mat_index = (base ? base.mat_index : rand(df.world.raws.inorganics.length))
MaterialInfo.new(0, mat_index)
when :MINERAL
mat_index = (mat_index_vein || stone_layer.mat_index)
MaterialInfo.new(0, mat_index)
when :LAVA_STONE
# XXX this is wrong
# maybe should search world.region_details.pos == biome_region_pos ?
idx = mapblock.region_offset[designation.biome]
mat_index = df.world.world_data.region_details[idx].lava_stone
MaterialInfo.new(0, mat_index)
# TODO
#when :PLANT
#when :GRASS_DARK, :GRASS_DEAD, :GRASS_DRY, :GRASS_LIGHT
#when :FEATURE
#when :FROZEN_LIQUID
#when :CONSTRUCTION
else # AIR ASHES BROOK CAMPFIRE DRIFTWOOD FIRE HFS MAGMA POOL RIVER
MaterialInfo.new(-1, -1)
end
end end
# MaterialInfo: inorganic token for current tile def mat_type
def mat_info mat_info.mat_type
MaterialInfo.new(0, mat_index) end
def mat_index
mat_info.mat_index
end end
def inspect def inspect

@ -138,7 +138,6 @@ module DFHack
@@inspecting = {} # avoid infinite recursion on mutually-referenced objects @@inspecting = {} # avoid infinite recursion on mutually-referenced objects
def inspect def inspect
cn = self.class.name.sub(/^DFHack::/, '') cn = self.class.name.sub(/^DFHack::/, '')
cn << ' @' << ('0x%X' % _memaddr) if cn != ''
out = "#<#{cn}" out = "#<#{cn}"
return out << ' ...>' if @@inspecting[_memaddr] return out << ' ...>' if @@inspecting[_memaddr]
@@inspecting[_memaddr] = true @@inspecting[_memaddr] = true
@ -309,7 +308,7 @@ module DFHack
DFHack.memory_write_int32(@_memaddr, v) DFHack.memory_write_int32(@_memaddr, v)
end end
when nil; DFHack.memory_write_int32(@_memaddr, 0) when nil; DFHack.memory_write_int32(@_memaddr, 0)
else _get._set(v) else @_tg._at(_getp)._set(v)
end end
end end
@ -655,6 +654,13 @@ module DFHack
DFHack.memory_bitarray_set(@_memaddr, idx, v) DFHack.memory_bitarray_set(@_memaddr, idx, v)
end end
end end
def inspect
out = "#<DfFlagarray"
each_with_index { |e, idx|
out << " #{_indexenum.sym(idx)}" if e
}
out << '>'
end
include Enumerable include Enumerable
end end

@ -438,6 +438,12 @@ static VALUE rb_cDFHack;
// DFHack module ruby methods, binds specific dfhack methods // DFHack module ruby methods, binds specific dfhack methods
// df-dfhack version (eg "0.34.11-r2")
static VALUE rb_dfversion(VALUE self)
{
return rb_str_new(DFHACK_VERSION, strlen(DFHACK_VERSION));
}
// enable/disable calls to DFHack.onupdate() // enable/disable calls to DFHack.onupdate()
static VALUE rb_dfonupdate_active(VALUE self) static VALUE rb_dfonupdate_active(VALUE self)
{ {
@ -955,6 +961,7 @@ static void ruby_bind_dfhack(void) {
rb_define_singleton_method(rb_cDFHack, "malloc", RUBY_METHOD_FUNC(rb_dfmalloc), 1); rb_define_singleton_method(rb_cDFHack, "malloc", RUBY_METHOD_FUNC(rb_dfmalloc), 1);
rb_define_singleton_method(rb_cDFHack, "free", RUBY_METHOD_FUNC(rb_dffree), 1); rb_define_singleton_method(rb_cDFHack, "free", RUBY_METHOD_FUNC(rb_dffree), 1);
rb_define_singleton_method(rb_cDFHack, "vmethod_do_call", RUBY_METHOD_FUNC(rb_dfvcall), 8); rb_define_singleton_method(rb_cDFHack, "vmethod_do_call", RUBY_METHOD_FUNC(rb_dfvcall), 8);
rb_define_singleton_method(rb_cDFHack, "version", RUBY_METHOD_FUNC(rb_dfversion), 0);
rb_define_singleton_method(rb_cDFHack, "memory_read", RUBY_METHOD_FUNC(rb_dfmemory_read), 2); rb_define_singleton_method(rb_cDFHack, "memory_read", RUBY_METHOD_FUNC(rb_dfmemory_read), 2);
rb_define_singleton_method(rb_cDFHack, "memory_read_int8", RUBY_METHOD_FUNC(rb_dfmemory_read_int8), 1); rb_define_singleton_method(rb_cDFHack, "memory_read_int8", RUBY_METHOD_FUNC(rb_dfmemory_read_int8), 1);

@ -23,9 +23,12 @@ module Kernel
end end
module DFHack module DFHack
VERSION = version
class OnupdateCallback class OnupdateCallback
attr_accessor :callback, :timelimit, :minyear, :minyeartick attr_accessor :callback, :timelimit, :minyear, :minyeartick, :description
def initialize(cb, tl, initdelay=0) def initialize(descr, cb, tl, initdelay=0)
@description = descr
@callback = cb @callback = cb
@ticklimit = tl @ticklimit = tl
@minyear = (tl ? df.cur_year : 0) @minyear = (tl ? df.cur_year : 0)
@ -34,22 +37,21 @@ module DFHack
# run callback if timedout # run callback if timedout
def check_run(year, yeartick, yearlen) def check_run(year, yeartick, yearlen)
if !@ticklimit if @ticklimit
@callback.call return unless year > @minyear or (year == @minyear and yeartick >= @minyeartick)
else @minyear = year
if year > @minyear or (year == @minyear and yeartick >= @minyeartick) @minyeartick = yeartick + @ticklimit
@minyear = year if @minyeartick > yearlen
@minyeartick = yeartick + @ticklimit @minyear += 1
if @minyeartick > yearlen @minyeartick -= yearlen
@minyear += 1
@minyeartick -= yearlen
end
@callback.call
end end
end end
rescue # t0 = Time.now
@callback.call
# dt = Time.now - t0 ; puts "rb cb #@description took #{'%.02f' % dt}s" if dt > 0.1
rescue Exception
df.onupdate_unregister self df.onupdate_unregister self
puts_err "onupdate cb #$!", $!.backtrace puts_err "onupdate #@description unregistered: #$!", $!.backtrace
end end
def <=>(o) def <=>(o)
@ -61,10 +63,11 @@ module DFHack
attr_accessor :onupdate_list, :onstatechange_list attr_accessor :onupdate_list, :onstatechange_list
# register a callback to be called every gframe or more # register a callback to be called every gframe or more
# ex: DFHack.onupdate_register { DFHack.world.units[0].counters.job_counter = 0 } # ex: DFHack.onupdate_register('fastdwarf') { DFHack.world.units[0].counters.job_counter = 0 }
def onupdate_register(ticklimit=nil, initialtickdelay=0, &b) def onupdate_register(descr, ticklimit=nil, initialtickdelay=0, &b)
raise ArgumentError, 'need a description as 1st arg' unless descr.kind_of?(::String)
@onupdate_list ||= [] @onupdate_list ||= []
@onupdate_list << OnupdateCallback.new(b, ticklimit, initialtickdelay) @onupdate_list << OnupdateCallback.new(descr, b, ticklimit, initialtickdelay)
DFHack.onupdate_active = true DFHack.onupdate_active = true
if onext = @onupdate_list.sort.first if onext = @onupdate_list.sort.first
DFHack.onupdate_minyear = onext.minyear DFHack.onupdate_minyear = onext.minyear
@ -73,8 +76,9 @@ module DFHack
@onupdate_list.last @onupdate_list.last
end end
# delete the callback for onupdate ; use the value returned by onupdate_register # delete the callback for onupdate ; use the value returned by onupdate_register or the description
def onupdate_unregister(b) def onupdate_unregister(b)
b = @onupdate_list.find { |bb| bb.description == b } if b.kind_of?(String)
@onupdate_list.delete b @onupdate_list.delete b
if @onupdate_list.empty? if @onupdate_list.empty?
DFHack.onupdate_active = false DFHack.onupdate_active = false

@ -329,8 +329,9 @@ protected:
// Display hotkey message // Display hotkey message
void print_search_option(int x, int y = -1) const void print_search_option(int x, int y = -1) const
{ {
auto dim = Screen::getWindowSize();
if (y == -1) if (y == -1)
y = gps->dimy - 2; y = dim.y - 2;
OutputString((entry_mode) ? 4 : 12, x, y, string(1, select_key)); OutputString((entry_mode) ? 4 : 12, x, y, string(1, select_key));
OutputString((entry_mode) ? 10 : 15, x, y, ": Search"); OutputString((entry_mode) ? 10 : 15, x, y, ": Search");
@ -413,8 +414,9 @@ public:
print_search_option(2); print_search_option(2);
else else
{ {
int x = 2; auto dim = Screen::getWindowSize();
OutputString(15, x, gps->dimy - 2, "Tab to enable Search"); int x = 2, y = dim.y - 2;
OutputString(15, x, y, "Tab to enable Search");
} }
} }
@ -519,7 +521,8 @@ private:
virtual bool should_check_input(set<df::interface_key> *input) virtual bool should_check_input(set<df::interface_key> *input)
{ {
if (input->count(interface_key::CURSOR_LEFT) || input->count(interface_key::CURSOR_RIGHT) || input->count(interface_key::CUSTOM_L)) if (input->count(interface_key::CURSOR_LEFT) || input->count(interface_key::CURSOR_RIGHT) ||
(!is_entry_mode() && input->count(interface_key::UNITVIEW_PRF_PROF)))
{ {
if (!is_entry_mode()) if (!is_entry_mode())
{ {

@ -212,15 +212,31 @@ struct stable_cursor_hook : df::viewscreen_dwarfmodest
{ {
typedef df::viewscreen_dwarfmodest interpose_base; typedef df::viewscreen_dwarfmodest interpose_base;
bool check_default()
{
switch (ui->main.mode) {
case ui_sidebar_mode::Default:
return true;
case ui_sidebar_mode::Build:
return ui_build_selector &&
(ui_build_selector->building_type < 0 ||
ui_build_selector->stage < 1);
default:
return false;
}
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input)) DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{ {
bool was_default = (ui->main.mode == df::ui_sidebar_mode::Default); bool was_default = check_default();
df::coord view = Gui::getViewportPos(); df::coord view = Gui::getViewportPos();
df::coord cursor = Gui::getCursorPos(); df::coord cursor = Gui::getCursorPos();
INTERPOSE_NEXT(feed)(input); INTERPOSE_NEXT(feed)(input);
bool is_default = (ui->main.mode == df::ui_sidebar_mode::Default); bool is_default = check_default();
df::coord cur_cursor = Gui::getCursorPos(); df::coord cur_cursor = Gui::getCursorPos();
if (is_default && !was_default) if (is_default && !was_default)
@ -241,7 +257,7 @@ struct stable_cursor_hook : df::viewscreen_dwarfmodest
tmp.insert(interface_key::CURSOR_UP_Z); tmp.insert(interface_key::CURSOR_UP_Z);
INTERPOSE_NEXT(feed)(&tmp); INTERPOSE_NEXT(feed)(&tmp);
} }
else if (cur_cursor.isValid()) else if (!is_default && cur_cursor.isValid())
{ {
last_cursor = df::coord(); last_cursor = df::coord();
} }

@ -291,8 +291,18 @@ int ProtectedJob::cur_tick_idx = 0;
typedef std::map<std::pair<int,int>, bool> TMaterialCache; typedef std::map<std::pair<int,int>, bool> TMaterialCache;
static const size_t MAX_HISTORY_SIZE = 28;
enum HistoryItem {
HIST_COUNT = 0,
HIST_AMOUNT,
HIST_INUSE_COUNT,
HIST_INUSE_AMOUNT
};
struct ItemConstraint { struct ItemConstraint {
PersistentDataItem config; PersistentDataItem config;
PersistentDataItem history;
// Fixed key parsed into fields // Fixed key parsed into fields
bool is_craft; bool is_craft;
@ -308,7 +318,7 @@ struct ItemConstraint {
int weight; int weight;
std::vector<ProtectedJob*> jobs; std::vector<ProtectedJob*> jobs;
int item_amount, item_count, item_inuse; int item_amount, item_count, item_inuse_amount, item_inuse_count;
bool request_suspend, request_resume; bool request_suspend, request_resume;
bool is_active, cant_resume_reported; bool is_active, cant_resume_reported;
@ -318,7 +328,7 @@ struct ItemConstraint {
public: public:
ItemConstraint() ItemConstraint()
: is_craft(false), min_quality(item_quality::Ordinary), is_local(false), : is_craft(false), min_quality(item_quality::Ordinary), is_local(false),
weight(0), item_amount(0), item_count(0), item_inuse(0), weight(0), item_amount(0), item_count(0), item_inuse_amount(0), item_inuse_count(0),
is_active(false), cant_resume_reported(false) is_active(false), cant_resume_reported(false)
{} {}
@ -352,6 +362,37 @@ public:
request_resume = (size <= goalCount()-goalGap()); request_resume = (size <= goalCount()-goalGap());
request_suspend = (size >= goalCount()); request_suspend = (size >= goalCount());
} }
static const size_t int28_size = PersistentDataItem::int28_size;
static const size_t hist_entry_size = PersistentDataItem::int28_size * 4;
size_t history_size() {
return history.data_size() / hist_entry_size;
}
int history_value(int idx, HistoryItem item) {
size_t hsize = history_size();
size_t base = ((history.ival(0)+1+idx) % hsize) * hist_entry_size;
return history.get_int28(base + item*int28_size);
}
int history_count(int idx) { return history_value(idx, HIST_COUNT); }
int history_amount(int idx) { return history_value(idx, HIST_AMOUNT); }
int history_inuse_count(int idx) { return history_value(idx, HIST_INUSE_COUNT); }
int history_inuse_amount(int idx) { return history_value(idx, HIST_INUSE_AMOUNT); }
void updateHistory()
{
size_t buffer_size = history_size();
if (buffer_size < MAX_HISTORY_SIZE && size_t(history.ival(0)+1) == buffer_size)
history.ensure_data(hist_entry_size*++buffer_size);
history.ival(0) = (history.ival(0)+1) % buffer_size;
size_t base = history.ival(0) * hist_entry_size;
history.set_int28(base + HIST_COUNT*int28_size, item_count);
history.set_int28(base + HIST_AMOUNT*int28_size, item_amount);
history.set_int28(base + HIST_INUSE_COUNT*int28_size, item_inuse_count);
history.set_int28(base + HIST_INUSE_AMOUNT*int28_size, item_inuse_amount);
}
}; };
/****************************** /******************************
@ -649,6 +690,9 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out)
update_job_data(out); update_job_data(out);
process_constraints(out); process_constraints(out);
for (size_t i = 0; i < constraints.size(); i++)
constraints[i]->updateHistory();
} }
} }
@ -659,6 +703,10 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out)
* ITEM COUNT CONSTRAINT * * ITEM COUNT CONSTRAINT *
******************************/ ******************************/
static std::string history_key(PersistentDataItem &config) {
return stl_sprintf("workflow/history/%d", config.entry_id());
}
static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg, bool create) static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg, bool create)
{ {
std::vector<std::string> tokens; std::vector<std::string> tokens;
@ -776,6 +824,8 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str
nct->init(str); nct->init(str);
} }
nct->history = World::GetPersistentData(history_key(nct->config), NULL);
constraints.push_back(nct); constraints.push_back(nct);
return nct; return nct;
} }
@ -787,6 +837,7 @@ static void delete_constraint(ItemConstraint *cv)
vector_erase_at(constraints, idx); vector_erase_at(constraints, idx);
World::DeletePersistentData(cv->config); World::DeletePersistentData(cv->config);
World::DeletePersistentData(cv->history);
delete cv; delete cv;
} }
@ -1064,7 +1115,8 @@ static void map_job_items(color_ostream &out)
{ {
constraints[i]->item_amount = 0; constraints[i]->item_amount = 0;
constraints[i]->item_count = 0; constraints[i]->item_count = 0;
constraints[i]->item_inuse = 0; constraints[i]->item_inuse_amount = 0;
constraints[i]->item_inuse_count = 0;
} }
meltable_count = 0; meltable_count = 0;
@ -1076,7 +1128,7 @@ static void map_job_items(color_ostream &out)
#define F(x) bad_flags.bits.x = true; #define F(x) bad_flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect); F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader); F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(artifact1); F(in_building); F(construction); F(artifact);
#undef F #undef F
bool dry_buckets = isOptionEnabled(CF_DRYBUCKETS); bool dry_buckets = isOptionEnabled(CF_DRYBUCKETS);
@ -1177,7 +1229,8 @@ static void map_job_items(color_ostream &out)
isAssignedSquad(item)) isAssignedSquad(item))
{ {
is_invalid = true; is_invalid = true;
cv->item_inuse++; cv->item_inuse_count++;
cv->item_inuse_amount += item->getStackSize();
} }
else else
{ {
@ -1333,6 +1386,25 @@ static void setEnabled(color_ostream &out, bool enable)
} }
} }
static void push_count_history(lua_State *L, ItemConstraint *icv)
{
size_t hsize = icv->history_size();
lua_createtable(L, hsize, 0);
for (size_t i = 0; i < hsize; i++)
{
lua_createtable(L, 0, 4);
Lua::SetField(L, icv->history_amount(i), -1, "cur_amount");
Lua::SetField(L, icv->history_count(i), -1, "cur_count");
Lua::SetField(L, icv->history_inuse_amount(i), -1, "cur_in_use_amount");
Lua::SetField(L, icv->history_inuse_count(i), -1, "cur_in_use_count");
lua_rawseti(L, -2, i+1);
}
}
static void push_constraint(lua_State *L, ItemConstraint *cv) static void push_constraint(lua_State *L, ItemConstraint *cv)
{ {
lua_newtable(L); lua_newtable(L);
@ -1367,7 +1439,8 @@ static void push_constraint(lua_State *L, ItemConstraint *cv)
Lua::SetField(L, cv->item_amount, ctable, "cur_amount"); Lua::SetField(L, cv->item_amount, ctable, "cur_amount");
Lua::SetField(L, cv->item_count, ctable, "cur_count"); Lua::SetField(L, cv->item_count, ctable, "cur_count");
Lua::SetField(L, cv->item_inuse, ctable, "cur_in_use"); Lua::SetField(L, cv->item_inuse_amount, ctable, "cur_in_use_amount");
Lua::SetField(L, cv->item_inuse_count, ctable, "cur_in_use_count");
// Current state value // Current state value
@ -1378,19 +1451,31 @@ static void push_constraint(lua_State *L, ItemConstraint *cv)
lua_newtable(L); lua_newtable(L);
bool resumed = false, want_resumed = false;
for (size_t i = 0, j = 0; i < cv->jobs.size(); i++) for (size_t i = 0, j = 0; i < cv->jobs.size(); i++)
{ {
if (!cv->jobs[i]->isLive()) continue; if (!cv->jobs[i]->isLive()) continue;
Lua::PushDFObject(L, cv->jobs[i]->actual_job); Lua::PushDFObject(L, cv->jobs[i]->actual_job);
lua_rawseti(L, -2, ++j); lua_rawseti(L, -2, ++j);
if (cv->jobs[i]->want_resumed) {
want_resumed = true;
resumed = resumed || cv->jobs[i]->isActuallyResumed();
}
} }
lua_setfield(L, ctable, "jobs"); lua_setfield(L, ctable, "jobs");
if (want_resumed && !resumed)
Lua::SetField(L, true, ctable, "is_delayed");
} }
static int listConstraints(lua_State *L) static int listConstraints(lua_State *L)
{ {
lua_settop(L, 2);
auto job = Lua::CheckDFObject<df::job>(L, 1); auto job = Lua::CheckDFObject<df::job>(L, 1);
bool with_history = lua_toboolean(L, 2);
lua_pushnil(L); lua_pushnil(L);
@ -1415,6 +1500,13 @@ static int listConstraints(lua_State *L)
for (size_t i = 0; i < vec.size(); i++) for (size_t i = 0; i < vec.size(); i++)
{ {
push_constraint(L, vec[i]); push_constraint(L, vec[i]);
if (with_history)
{
push_count_history(L, vec[i]);
lua_setfield(L, -2, "history");
}
lua_rawseti(L, -2, i+1); lua_rawseti(L, -2, i+1);
} }
@ -1463,6 +1555,24 @@ static int setConstraint(lua_State *L)
return 1; return 1;
} }
static int getCountHistory(lua_State *L)
{
auto token = luaL_checkstring(L, 1);
color_ostream &out = *Lua::GetOutput(L);
update_data_structures(out);
ItemConstraint *icv = get_constraint(out, token, NULL, false);
if (icv)
push_count_history(L, icv);
else
lua_pushnil(L);
return 1;
}
DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(isEnabled), DFHACK_LUA_FUNCTION(isEnabled),
DFHACK_LUA_FUNCTION(setEnabled), DFHACK_LUA_FUNCTION(setEnabled),
@ -1474,6 +1584,7 @@ DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(listConstraints), DFHACK_LUA_COMMAND(listConstraints),
DFHACK_LUA_COMMAND(findConstraint), DFHACK_LUA_COMMAND(findConstraint),
DFHACK_LUA_COMMAND(setConstraint), DFHACK_LUA_COMMAND(setConstraint),
DFHACK_LUA_COMMAND(getCountHistory),
DFHACK_LUA_END DFHACK_LUA_END
}; };
@ -1521,10 +1632,10 @@ static void print_constraint(color_ostream &out, ItemConstraint *cv, bool no_job
<< cv->goalCount() << " (gap " << cv->goalGap() << ")" << endl; << cv->goalCount() << " (gap " << cv->goalGap() << ")" << endl;
out.reset_color(); out.reset_color();
if (cv->item_count || cv->item_inuse) if (cv->item_count || cv->item_inuse_count)
out << prefix << " items: amount " << cv->item_amount << "; " out << prefix << " items: amount " << cv->item_amount << "; "
<< cv->item_count << " stacks available, " << cv->item_count << " stacks available, "
<< cv->item_inuse << " in use." << endl; << cv->item_inuse_count << " in use." << endl;
if (no_job) return; if (no_job) return;

@ -5,7 +5,7 @@ class AutoFarm
@lastcounts = Hash.new(0) @lastcounts = Hash.new(0)
end end
def setthreshold (id, v) def setthreshold(id, v)
if df.world.raws.plants.all.find { |r| r.id == id } if df.world.raws.plants.all.find { |r| r.id == id }
@thresholds[id] = v.to_i @thresholds[id] = v.to_i
else else
@ -13,7 +13,7 @@ class AutoFarm
end end
end end
def setdefault (v) def setdefault(v)
@thresholds.default = v.to_i @thresholds.default = v.to_i
end end
@ -29,7 +29,7 @@ class AutoFarm
def find_plantable_plants def find_plantable_plants
plantable = {} plantable = {}
counts = {} counts = Hash.new(0)
df.world.items.other[:SEEDS].each { |i| df.world.items.other[:SEEDS].each { |i|
if (!i.flags.dump && !i.flags.forbid && !i.flags.garbage_collect && if (!i.flags.dump && !i.flags.forbid && !i.flags.garbage_collect &&
@ -53,7 +53,7 @@ class AutoFarm
return plantable return plantable
end end
def set_farms ( plants, farms) def set_farms( plants, farms)
return if farms.length == 0 return if farms.length == 0
if plants.length == 0 if plants.length == 0
plants = [-1] plants = [-1]
@ -79,7 +79,7 @@ class AutoFarm
if (!i.flags.dump && !i.flags.forbid && !i.flags.garbage_collect && if (!i.flags.dump && !i.flags.forbid && !i.flags.garbage_collect &&
!i.flags.hostile && !i.flags.on_fire && !i.flags.rotten && !i.flags.hostile && !i.flags.on_fire && !i.flags.rotten &&
!i.flags.trader && !i.flags.in_building && !i.flags.construction && !i.flags.trader && !i.flags.in_building && !i.flags.construction &&
!i.flags.artifact1 && plantable.has_key? (i.mat_index)) !i.flags.artifact && plantable.has_key?(i.mat_index))
counts[i.mat_index] = counts[i.mat_index] + i.stack_size counts[i.mat_index] = counts[i.mat_index] + i.stack_size
end end
} }
@ -108,13 +108,13 @@ class AutoFarm
end end
} }
set_farms (plants_s, farms_s) set_farms(plants_s, farms_s)
set_farms (plants_u, farms_u) set_farms(plants_u, farms_u)
end end
def start def start
@onupdate = df.onupdate_register (100) { process } @onupdate = df.onupdate_register('autofarm', 100) { process }
@running = true @running = true
end end

@ -26,7 +26,7 @@ class AutoUnsuspend
end end
def start def start
@onupdate = df.onupdate_register (5) { process } @onupdate = df.onupdate_register('autounsuspend', 5) { process }
@running = true @running = true
end end
@ -36,7 +36,7 @@ class AutoUnsuspend
end end
def status def status
stat = @running ? "Running." : "Loaded." @running ? 'Running.' : 'Stopped.'
end end
end end
@ -53,6 +53,6 @@ else
if $AutoUnsuspend if $AutoUnsuspend
puts $AutoUnsuspend.status puts $AutoUnsuspend.status
else else
puts "AI not started" puts 'Not loaded.'
end end
end end

@ -66,20 +66,64 @@ function is_caste_mat(iobj)
end end
function describe_material(iobj) function describe_material(iobj)
local matline = 'any material' local matflags = utils.list_bitfield_flags(iobj.mat_mask)
if #matflags > 0 then
matflags = 'any '..table.concat(matflags, '/')
else
matflags = nil
end
if is_caste_mat(iobj) then if is_caste_mat(iobj) then
matline = 'no material' return 'no material'
elseif (iobj.mat_type or -1) >= 0 then elseif (iobj.mat_type or -1) >= 0 then
local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index) local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index)
local matline
if info then if info then
matline = info:toString() matline = info:toString()
else else
matline = iobj.mat_type..':'..iobj.mat_index matline = iobj.mat_type..':'..iobj.mat_index
end end
return matline, matflags
else
return matflags or 'any material'
end
end
function current_stock(iobj)
if iobj.goal_by_count then
return iobj.cur_count
else
return iobj.cur_amount
end end
return matline
end end
function if_by_count(iobj,bc,ba)
if iobj.goal_by_count then
return bc
else
return ba
end
end
function compute_trend(history,field)
local count = #history
if count == 0 then
return 0
end
local sumX,sumY,sumXY,sumXX = 0,0,0,0
for i,v in ipairs(history) do
sumX = sumX + i
sumY = sumY + v[field]
sumXY = sumXY + i*v[field]
sumXX = sumXX + i*i
end
return (count * sumXY - sumX * sumY) / (count * sumXX - sumX * sumX)
end
------------------------
-- RANGE EDITOR GROUP --
------------------------
local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false } local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false }
RangeEditor = defclass(RangeEditor, widgets.Label) RangeEditor = defclass(RangeEditor, widgets.Label)
@ -162,6 +206,10 @@ function RangeEditor:onIncRange(field, delta)
self.save_cb(cons) self.save_cb(cons)
end end
---------------------------
-- NEW CONSTRAINT DIALOG --
---------------------------
NewConstraint = defclass(NewConstraint, gui.FramedScreen) NewConstraint = defclass(NewConstraint, gui.FramedScreen)
NewConstraint.focus_path = 'workflow/new' NewConstraint.focus_path = 'workflow/new'
@ -177,7 +225,7 @@ NewConstraint.ATTRS {
} }
function NewConstraint:init(args) function NewConstraint:init(args)
self.constraint = args.constraint or {} self.constraint = args.constraint or { item_type = -1 }
rawset_default(self.constraint, { goal_value = 10, goal_gap = 5, goal_by_count = false }) rawset_default(self.constraint, { goal_value = 10, goal_gap = 5, goal_by_count = false })
local matlist = {} local matlist = {}
@ -202,8 +250,16 @@ function NewConstraint:init(args)
frame = { l = 1, t = 2, w = 26 }, frame = { l = 1, t = 2, w = 26 },
text = { text = {
'Type: ', 'Type: ',
{ pen = COLOR_LIGHTCYAN, { pen = function()
text = function() return describe_item_type(self.constraint) end }, if self:isValid() then return COLOR_LIGHTCYAN else return COLOR_LIGHTRED end
end,
text = function()
if self:isValid() then
return describe_item_type(self.constraint)
else
return 'item not set'
end
end },
NEWLINE, ' ', NEWLINE, ' ',
{ key = 'CUSTOM_T', text = ': Select, ', { key = 'CUSTOM_T', text = ': Select, ',
on_activate = self:callback('chooseType') }, on_activate = self:callback('chooseType') },
@ -277,6 +333,7 @@ function NewConstraint:init(args)
{ key = 'LEAVESCREEN', text = ': Cancel, ', { key = 'LEAVESCREEN', text = ': Cancel, ',
on_activate = self:callback('dismiss') }, on_activate = self:callback('dismiss') },
{ key = 'MENU_CONFIRM', key_sep = ': ', { key = 'MENU_CONFIRM', key_sep = ': ',
enabled = self:callback('isValid'),
text = function() text = function()
if self.is_existing then return 'Update' else return 'Create new' end if self.is_existing then return 'Update' else return 'Create new' end
end, end,
@ -295,9 +352,17 @@ function NewConstraint:postinit()
self:onChange() self:onChange()
end end
function NewConstraint:isValid()
return self.constraint.item_type >= 0
end
function NewConstraint:onChange() function NewConstraint:onChange()
local token = workflow.constraintToToken(self.constraint) local token = workflow.constraintToToken(self.constraint)
local out = workflow.findConstraint(token) local out
if self:isValid() then
out = workflow.findConstraint(token)
end
if out then if out then
self.constraint = out self.constraint = out
@ -390,6 +455,288 @@ function NewConstraint:onRangeChange()
cons.goal_gap = math.max(1, math.min(cons.goal_gap, cons.goal_value-1)) cons.goal_gap = math.max(1, math.min(cons.goal_gap, cons.goal_value-1))
end end
------------------------------
-- GLOBAL CONSTRAINT SCREEN --
------------------------------
ConstraintList = defclass(ConstraintList, gui.FramedScreen)
ConstraintList.focus_path = 'workflow/list'
ConstraintList.ATTRS {
frame_title = 'Workflow Status',
frame_inset = 0,
frame_background = COLOR_BLACK,
frame_style = gui.BOUNDARY_FRAME,
}
function ConstraintList:init(args)
local fwidth_cb = self:cb_getfield('fwidth')
self.fwidth = 20
self.sort_by_severity = false
self:addviews{
widgets.Panel{
frame = { w = 31, r = 0, h = 6, t = 0 },
frame_inset = 1,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
enabled = self:callback('isAnySelected'),
text = {
{ text = function()
local cur = self:getCurConstraint()
if cur then
return string.format(
'Currently %d (%d in use)',
current_stock(cur),
if_by_count(cur, cur.cur_in_use_count, cur.cur_in_use_amount)
)
else
return 'No constraint selected'
end
end }
}
},
RangeEditor{
frame = { l = 0, t = 2 },
enabled = self:callback('isAnySelected'),
get_cb = self:callback('getCurConstraint'),
save_cb = self:callback('saveConstraint'),
},
}
},
widgets.Widget{
active = false,
frame = { w = 1, r = 31 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
widgets.Widget{
active = false,
frame = { w = 31, r = 0, h = 1, t = 6 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
widgets.Panel{
frame = { l = 0, r = 32 },
frame_inset = 1,
on_layout = function(body)
self.fwidth = body.width - (12+1+1+7+1+1+1+7)
end,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
text_pen = COLOR_CYAN,
text = {
{ text = 'Item', width = 12 }, ' ',
{ text = 'Material etc', width = fwidth_cb }, ' ',
{ text = 'Stock / Limit' },
}
},
widgets.FilteredList{
view_id = 'list',
frame = { t = 2, b = 2 },
edit_below = true,
not_found_label = 'No matching constraints',
edit_pen = COLOR_LIGHTCYAN,
text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK },
cursor_pen = { fg = COLOR_WHITE, bg = COLOR_GREEN },
},
widgets.Label{
frame = { b = 0, h = 1 },
text = {
{ key = 'CUSTOM_SHIFT_A', text = ': Add',
on_activate = self:callback('onNewConstraint') }, ', ',
{ key = 'CUSTOM_SHIFT_X', text = ': Delete',
on_activate = self:callback('onDeleteConstraint') }, ', ',
{ key = 'CUSTOM_SHIFT_O', text = ': Severity Order',
on_activate = self:callback('onSwitchSort'),
pen = function()
if self.sort_by_severity then
return COLOR_LIGHTCYAN
else
return COLOR_WHITE
end
end }, ', ',
{ key = 'CUSTOM_SHIFT_S', text = ': Search',
on_activate = function()
self.subviews.list.edit.active = not self.subviews.list.edit.active
end,
pen = function()
if self.subviews.list.edit.active then
return COLOR_LIGHTCYAN
else
return COLOR_WHITE
end
end }
}
}
}
},
}
self.subviews.list.edit.active = false
self:initListChoices()
end
function stock_trend_color(cons)
local stock = current_stock(cons)
if stock >= cons.goal_value - cons.goal_gap then
return COLOR_LIGHTGREEN, 0
elseif stock <= cons.goal_gap then
return COLOR_LIGHTRED, 4
elseif stock >= cons.goal_value - 2*cons.goal_gap then
return COLOR_GREEN, 1
elseif stock <= 2*cons.goal_gap then
return COLOR_RED, 3
else
local trend = if_by_count(cons, cons.trend_count, cons.trend_amount)
if trend > 0.3 then
return COLOR_GREEN, 1
elseif trend < -0.3 then
return COLOR_RED, 3
else
return COLOR_GREY, 2
end
end
end
function ConstraintList:initListChoices(clist, sel_token)
clist = clist or workflow.listConstraints(nil, true)
local fwidth_cb = self:cb_getfield('fwidth')
local choices = {}
for i,cons in ipairs(clist) do
cons.trend_count = compute_trend(cons.history, 'cur_count')
cons.trend_amount = compute_trend(cons.history, 'cur_amount')
local itemstr = describe_item_type(cons)
local matstr,matflagstr = describe_material(cons)
if matflagstr then
matstr = matflagstr .. ' ' .. matstr
end
if cons.min_quality > 0 or cons.is_local then
local lst = {}
if cons.is_local then
table.insert(lst, 'local')
end
if cons.min_quality > 0 then
table.insert(lst, string.lower(df.item_quality[cons.min_quality]))
end
matstr = matstr .. ' ('..table.concat(lst,',')..')'
end
local goal_color = COLOR_GREY
if #cons.jobs == 0 then
goal_color = COLOR_RED
elseif cons.is_delayed then
goal_color = COLOR_YELLOW
end
table.insert(choices, {
text = {
{ text = itemstr, width = 12, pad_char = ' ' }, ' ',
{ text = matstr, width = fwidth_cb, pad_char = ' ' }, ' ',
{ text = curry(current_stock,cons), width = 7, rjustify = true,
pen = function() return { fg = stock_trend_color(cons) } end },
{ text = curry(if_by_count,cons,'S','I'), gap = 1,
pen = { fg = COLOR_GREY } },
{ text = function() return cons.goal_value end, gap = 1,
pen = { fg = goal_color } }
},
severity = select(2, stock_trend_color(cons)),
search_key = itemstr .. ' | ' .. matstr,
token = cons.token,
obj = cons
})
end
self:setChoices(choices, sel_token)
end
function ConstraintList:isAnySelected()
return self.subviews.list:getSelected() ~= nil
end
function ConstraintList:getCurConstraint()
local selidx,selobj = self.subviews.list:getSelected()
if selobj then return selobj.obj end
end
function ConstraintList:onSwitchSort()
self.sort_by_severity = not self.sort_by_severity
self:setChoices(self.subviews.list:getChoices())
end
function ConstraintList:setChoices(choices, sel_token)
if self.sort_by_severity then
table.sort(choices, function(a,b)
return a.severity > b.severity
or (a.severity == b.severity and
current_stock(a.obj)/a.obj.goal_value < current_stock(b.obj)/b.obj.goal_value)
end)
else
table.sort(choices, function(a,b) return a.search_key < b.search_key end)
end
local selidx = nil
if sel_token then
selidx = utils.linear_index(choices, sel_token, 'token')
end
local list = self.subviews.list
local filter = list:getFilter()
list:setChoices(choices, selidx)
if filter ~= '' then
list:setFilter(filter, selidx)
if selidx and list:getSelected() ~= selidx then
list:setFilter('', selidx)
end
end
end
function ConstraintList:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
else
ConstraintList.super.onInput(self, keys)
end
end
function ConstraintList:onNewConstraint()
NewConstraint{
on_submit = self:callback('saveConstraint')
}:show()
end
function ConstraintList:saveConstraint(cons)
local out = workflow.setConstraint(cons.token, cons.goal_by_count, cons.goal_value, cons.goal_gap)
self:initListChoices(nil, out.token)
end
function ConstraintList:onDeleteConstraint()
local cons = self:getCurConstraint()
dlg.showYesNoPrompt(
'Delete Constraint',
'Really delete the current constraint?',
COLOR_YELLOW,
function()
workflow.deleteConstraint(cons.token)
self:initListChoices()
end
)
end
-------------------------------
-- WORKSHOP JOB INFO OVERLAY --
-------------------------------
JobConstraints = defclass(JobConstraints, guidm.MenuOverlay) JobConstraints = defclass(JobConstraints, guidm.MenuOverlay)
JobConstraints.focus_path = 'workflow/job' JobConstraints.focus_path = 'workflow/job'
@ -480,24 +827,12 @@ function JobConstraints:initListChoices(clist, sel_token)
end end
itemstr = itemstr .. ' ('..table.concat(lst,',')..')' itemstr = itemstr .. ' ('..table.concat(lst,',')..')'
end end
local matstr = describe_material(cons) local matstr,matflagstr = describe_material(cons)
local matflagstr = ''
local matflags = utils.list_bitfield_flags(cons.mat_mask)
if #matflags > 0 then
matflags[1] = 'any '..matflags[1]
if matstr == 'any material' then
matstr = table.concat(matflags, ', ')
matflags = {}
end
end
if #matflags > 0 then
matflagstr = table.concat(matflags, ', ')
end
table.insert(choices, { table.insert(choices, {
text = { text = {
goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE, goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE,
' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', matflagstr ' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', (matflagstr or '')
}, },
token = cons.token, token = cons.token,
obj = cons obj = cons
@ -552,18 +887,22 @@ function JobConstraints:onNewConstraint()
table.insert(choices, { text = itemstr..' of '..matstr, obj = cons }) table.insert(choices, { text = itemstr..' of '..matstr, obj = cons })
end end
dlg.showListPrompt( dlg.ListBox{
'New limit', frame_title = 'New limit',
'Select one of the possible outputs:', text = 'Select one of the possible outputs:',
COLOR_WHITE, text_pen = COLOR_WHITE,
choices, choices = choices,
function(idx,item) on_select = function(idx,item)
self:saveConstraint(item.obj)
end,
select2_hint = 'Advanced',
on_select2 = function(idx,item)
NewConstraint{ NewConstraint{
constraint = item.obj, constraint = item.obj,
on_submit = self:callback('saveConstraint') on_submit = self:callback('saveConstraint')
}:show() }:show()
end end,
) }:show()
end end
function JobConstraints:onDeleteConstraint() function JobConstraints:onDeleteConstraint()
@ -589,20 +928,25 @@ function JobConstraints:onInput(keys)
end end
end end
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then local args = {...}
qerror("This script requires a workshop job selected in the 'q' mode")
end
local job = dfhack.gui.getSelectedJob() if args[1] == 'list' then
check_enabled(function() ConstraintList{}:show() end)
else
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then
qerror("This script requires a workshop job selected in the 'q' mode")
end
check_enabled(function() local job = dfhack.gui.getSelectedJob()
check_repeat(job, function()
local clist = workflow.listConstraints(job)
if not clist then
dlg.showMessage('Not Supported', 'This type of job is not supported by workflow.', COLOR_LIGHTRED)
return
end
JobConstraints{ job = job, clist = clist }:show()
end)
end)
check_enabled(function()
check_repeat(job, function()
local clist = workflow.listConstraints(job)
if not clist then
dlg.showMessage('Not Supported', 'This type of job is not supported by workflow.', COLOR_LIGHTRED)
return
end
JobConstraints{ job = job, clist = clist }:show()
end)
end)
end

@ -0,0 +1,120 @@
# control your levers from the dfhack console
def lever_pull_job(bld)
ref = DFHack::GeneralRefBuildingHolderst.cpp_new
ref.building_id = bld.id
job = DFHack::Job.cpp_new
job.job_type = :PullLever
job.pos = [bld.centerx, bld.centery, bld.z]
job.general_refs << ref
bld.jobs << job
df.job_link job
puts lever_descr(bld)
end
def lever_pull_cheat(bld)
bld.linked_mechanisms.each { |i|
i.general_refs.grep(DFHack::GeneralRefBuildingHolderst).each { |r|
r.building_tg.setTriggerState(bld.state)
}
}
bld.state = (bld.state == 0 ? 1 : 0)
puts lever_descr(bld)
end
def lever_descr(bld, idx=nil)
ret = []
# lever description
descr = ''
descr << "#{idx}: " if idx
descr << "lever ##{bld.id} @[#{bld.centerx}, #{bld.centery}, #{bld.z}] #{bld.state == 0 ? '\\' : '/'}"
bld.jobs.each { |j|
if j.job_type == :PullLever
flags = ''
flags << ', repeat' if j.flags.repeat
flags << ', suspended' if j.flags.suspend
descr << " (pull order#{flags})"
end
}
bld.linked_mechanisms.map { |i|
i.general_refs.grep(DFHack::GeneralRefBuildingHolderst)
}.flatten.each { |r|
# linked building description
tg = r.building_tg
state = ''
if tg.respond_to?(:gate_flags)
state << (tg.gate_flags.closed ? 'closed' : 'opened')
state << ", closing (#{tg.timer})" if tg.gate_flags.closing
state << ", opening (#{tg.timer})" if tg.gate_flags.opening
end
ret << (descr + " linked to #{tg._rtti_classname} ##{tg.id} @[#{tg.centerx}, #{tg.centery}, #{tg.z}] #{state}")
# indent other links
descr = descr.gsub(/./, ' ')
}
ret << descr if ret.empty?
ret
end
def lever_list
@lever_list = []
df.world.buildings.other[:TRAP].find_all { |bld|
bld.trap_type == :Lever
}.sort_by { |bld| bld.id }.each { |bld|
puts lever_descr(bld, @lever_list.length)
@lever_list << bld.id
}
end
case $script_args[0]
when 'pull'
cheat = $script_args.delete('--cheat') || $script_args.delete('--now')
id = $script_args[1].to_i
id = @lever_list[id] || id
bld = df.building_find(id)
raise 'invalid lever id' if not bld
if cheat
lever_pull_cheat(bld)
else
lever_pull_job(bld)
end
when 'list'
lever_list
when /^\d+$/
id = $script_args[0].to_i
id = @lever_list[id] || id
bld = df.building_find(id)
raise 'invalid lever id' if not bld
puts lever_descr(bld)
else
puts <<EOS
Lever control from the dfhack console
Usage:
lever list
shows the list of levers in the fortress, with their id and links
lever pull 42
order the dwarves to pull lever 42
lever pull 42 --cheat
magically pull lever 42 immediately
EOS
end

@ -4,7 +4,7 @@ $magma_sources ||= []
case $script_args[0] case $script_args[0]
when 'here' when 'here'
$magma_onupdate ||= df.onupdate_register(12) { $magma_onupdate ||= df.onupdate_register('magmasource', 12) {
# called every 12 game ticks (100x a dwarf day) # called every 12 game ticks (100x a dwarf day)
if $magma_sources.empty? if $magma_sources.empty?
df.onupdate_unregister($magma_onupdate) df.onupdate_unregister($magma_onupdate)

@ -21,7 +21,7 @@ slayit = lambda { |u|
else else
# it's getting hot around here # it's getting hot around here
# !!WARNING!! do not call on a magma-safe creature # !!WARNING!! do not call on a magma-safe creature
ouh = df.onupdate_register(1) { ouh = df.onupdate_register("slayrace ensure #{u.id}", 1) {
if u.flags1.dead if u.flags1.dead
df.onupdate_unregister(ouh) df.onupdate_unregister(ouh)
else else

@ -0,0 +1,200 @@
# mark stuff inside of cages for dumping.
def plural(nr, name)
# '1 cage' / '4 cages'
"#{nr} #{name}#{'s' if nr > 1}"
end
def cage_dump_items(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
next unless ref.kind_of?(DFHack::GeneralRefContainsItemst)
next if ref.item_tg.flags.dump
count += 1
ref.item_tg.flags.dump = true
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'item')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_armor(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
next unless ref.kind_of?(DFHack::GeneralRefContainsUnitst)
ref.unit_tg.inventory.each { |it|
next if it.mode != :Worn
next if it.item.flags.dump
count += 1
it.item.flags.dump = true
}
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'armor piece')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_weapons(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
next unless ref.kind_of?(DFHack::GeneralRefContainsUnitst)
ref.unit_tg.inventory.each { |it|
next if it.mode != :Weapon
next if it.item.flags.dump
count += 1
it.item.flags.dump = true
}
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'weapon')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_all(list)
count = 0
count_cage = 0
list.each { |cage|
pre_count = count
cage.general_refs.each { |ref|
case ref
when DFHack::GeneralRefContainsItemst
next if ref.item_tg.flags.dump
count += 1
ref.item_tg.flags.dump = true
when DFHack::GeneralRefContainsUnitst
ref.unit_tg.inventory.each { |it|
next if it.item.flags.dump
count += 1
it.item.flags.dump = true
}
end
}
count_cage += 1 if pre_count != count
}
puts "Dumped #{plural(count, 'item')} in #{plural(count_cage, 'cage')}"
end
def cage_dump_list(list)
count_total = Hash.new(0)
empty_cages = 0
list.each { |cage|
count = Hash.new(0)
cage.general_refs.each { |ref|
case ref
when DFHack::GeneralRefContainsItemst
count[ref.item_tg._rtti_classname] += 1
when DFHack::GeneralRefContainsUnitst
ref.unit_tg.inventory.each { |it|
count[it.item._rtti_classname] += 1
}
# TODO vermin ?
else
puts "unhandled ref #{ref.inspect}" if $DEBUG
end
}
type = case cage
when DFHack::ItemCagest; 'Cage'
when DFHack::ItemAnimaltrapst; 'Animal trap'
else cage._rtti_classname
end
if count.empty?
empty_cages += 1
else
puts "#{type} ##{cage.id}: ", count.sort_by { |k, v| v }.map { |k, v| " #{v} #{k}" }
end
count.each { |k, v| count_total[k] += v }
}
if list.length > 2
puts '', "Total: ", count_total.sort_by { |k, v| v }.map { |k, v| " #{v} #{k}" }
puts "with #{plural(empty_cages, 'empty cage')}"
end
end
# handle magic script arguments
here_only = $script_args.delete 'here'
if here_only
it = df.item_find
list = [it]
if not it.kind_of?(DFHack::ItemCagest) and not it.kind_of?(DFHack::ItemAnimaltrapst)
list = df.world.items.other[:ANY_CAGE_OR_TRAP].find_all { |i| df.at_cursor?(i) }
end
puts 'Please select a cage' if list.empty?
elsif ids = $script_args.find_all { |arg| arg =~ /^\d+$/ } and ids.first
list = []
ids.each { |id|
$script_args.delete id
if not it = df.item_find(id.to_i)
puts "Invalid item id #{id}"
elsif not it.kind_of?(DFHack::ItemCagest) and not it.kind_of?(DFHack::ItemAnimaltrapst)
puts "Item ##{id} is not a cage"
list << it
else
list << it
end
}
puts 'Please use a valid cage id' if list.empty?
else
list = df.world.items.other[:ANY_CAGE_OR_TRAP]
end
# act
case $script_args[0]
when 'items'
cage_dump_items(list) if not list.empty?
when 'armor'
cage_dump_armor(list) if not list.empty?
when 'weapons'
cage_dump_weapons(list) if not list.empty?
when 'all'
cage_dump_all(list) if not list.empty?
when 'list'
cage_dump_list(list) if not list.empty?
else
puts <<EOS
Marks items inside all cages for dumping.
Add 'here' to dump stuff only for selected cage.
Add a cage id to dump stuff for this cage only.
See 'autodump' to actually dump stuff.
Usage:
stripcaged items
dump items directly in cages (eg seeds after training)
stripcaged [armor|weapons] here
dump armor or weapons of caged creatures in selected cage
stripcaged all 28 29
dump every item in cage id 28 and 29, along with every item worn by creatures in there too
stripcaged list
show content of the cages
EOS
end

@ -8,12 +8,12 @@ when 'add'
if u = df.unit_find if u = df.unit_find
$superdwarf_ids |= [u.id] $superdwarf_ids |= [u.id]
if df.gamemode == :ADVENTURE if df.gamemode == :ADVENTURE and not df.respond_to?(:cur_year_tick_advmode)
onupdate_delay = nil onupdate_delay = nil
else else
onupdate_delay = 1 onupdate_delay = 1
end end
$superdwarf_onupdate ||= df.onupdate_register(onupdate_delay) { $superdwarf_onupdate ||= df.onupdate_register('superdwarf', onupdate_delay) {
if $superdwarf_ids.empty? if $superdwarf_ids.empty?
df.onupdate_unregister($superdwarf_onupdate) df.onupdate_unregister($superdwarf_onupdate)
$superdwarf_onupdate = nil $superdwarf_onupdate = nil