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
and calls it as <tt class="docutils literal">on_submit(index,choice)</tt>.</td>
</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>
<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>
<p>Call the <tt class="docutils literal">on_submit</tt> callback, as if the Enter key was handled.</p>
</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>
</div>
<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.
* ``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 = '...'``
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_submit: Enter key callback; if specified, the list reacts to the key
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.
:icon_width: If not *nil*, the specified number of character columns
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.
* ``list:submit2()``
Call the ``on_submit2`` callback, as if the Shift-Enter key was handled.
FilteredList class
------------------
@ -2836,6 +2852,7 @@ In addition to passing through all attributes supported by List, it
supports:
: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.
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.
- added a small stand-alone utility for applying and removing binary patches.
- 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
- 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:
- 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.
- lua: lua interpreter.
- lua: lua interpreter front-end converted to a script from a native command.
- dfusion: misc scripts with a text based menu.
- 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:
- gui/guide-path: displays the cached path for minecart Guide orders.
- 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/gm-editor: an universal editor for lots of dfhack things.
- 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:
- properly considers minecarts assigned to routes busy.
- 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.
New Search plugin by falconne:
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:
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.
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!
=============================
@ -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.
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.
.. image:: images/tweak-plate.png
: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%
: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,
i.e. stop the rightmost list of the Positions page of the military
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.
.. 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
----------
@ -1946,6 +1966,41 @@ embark
======
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
=======================
@ -1958,6 +2013,9 @@ are mostly implemented by lua scripts.
In order to avoid user confusion, as a matter of policy all these tools
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
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
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
Master, and U-Z for Legendary thru Legendary+5). Cells with red backgrounds
denote skills not controlled by labors.
Master, and U-Z for Legendary thru Legendary+5).
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
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.
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
===========
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
@ -2050,7 +2148,7 @@ to select the target area and apply changes.
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
@ -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 example config binds building/unit rename to Ctrl-Shift-N, and
unit profession change to Ctrl-Shift-T.
gui/room-list
=============
To use, bind to a key and activate in the 'q' mode, either immediately or after opening
the assign owner page.
To use, bind to a key (the example config uses Alt-R) and activate in the 'q' mode,
either immediately or after opening the assign owner page.
.. image:: images/room-list.png
@ -2104,7 +2205,8 @@ list, and allows unassigning them.
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
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
==============
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
@ -2129,7 +2232,8 @@ computes it when the order is executed for the first time.
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
@ -2176,7 +2280,8 @@ you have to unset the material first.
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
@ -2204,20 +2309,19 @@ can be used for troubleshooting jobs that don't match the right constraints.
.. 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
suit your need, and set the item count range.
.. image:: images/workflow-new2.png
If you don't need advanced settings, you can just press 'y' to confirm creation.
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
@ -2278,7 +2382,8 @@ Configuration UI
----------------
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
@ -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.
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

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

@ -81,6 +81,55 @@ namespace DFHack
int &ival(int i) { 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(int id, const std::string &key, std::string *sv, int *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}
local FAKE_INPUT_KEYS = {
_MOUSE_L = true,
_MOUSE_R = true,
_STRING = true,
}
function simulateInput(screen,...)
local keys = {}
local function push_key(arg)
local kv = arg
if type(arg) == 'string' then
kv = df.interface_key[arg]
if kv == nil then
if kv == nil and not FAKE_INPUT_KEYS[arg] then
error('Invalid keycode: '..arg)
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)
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
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)
return rect, body
end
@ -617,7 +627,7 @@ end
function FramedScreen:computeFrame(parent_rect)
local sw, sh = parent_rect.width, parent_rect.height
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
function FramedScreen:onRenderFrame(dc, rect)

@ -152,7 +152,9 @@ ListBox.ATTRS{
with_filter = false,
cursor_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)
@ -168,6 +170,16 @@ function ListBox:init(info)
list_widget = widgets.FilteredList
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{
list_widget{
view_id = 'list',
@ -182,11 +194,19 @@ function ListBox:init(info)
local cb = obj.on_select or obj[2]
if cb then cb(obj, sel) end
end,
on_submit2 = on_submit2,
frame = { l = 0, r = 0 },
}
}
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()
local mw, mh = InputBox.super.getWantedFrameSize(self)
local list = self.subviews.list

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

@ -110,10 +110,10 @@ bool Screen::paintTile(const Pen &pen, int x, int y)
{
if (!gps || !pen.valid()) return false;
int dimx = gps->dimx, dimy = gps->dimy;
if (x < 0 || x >= dimx || y < 0 || y >= dimy) return false;
auto dim = getWindowSize();
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;
}
@ -121,11 +121,11 @@ Pen Screen::readTile(int x, int y)
{
if (!gps) return Pen(0,0,0,-1);
int dimx = gps->dimx, dimy = gps->dimy;
if (x < 0 || x >= dimx || y < 0 || y >= dimy)
auto dim = getWindowSize();
if (x < 0 || x >= dim.x || y < 0 || y >= dim.y)
return Pen(0,0,0,-1);
int index = x*dimy + y;
int index = x*dim.y + y;
auto screen = gps->screen + index*4;
if (screen[3] & 0x80)
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)
{
if (!gps || y < 0 || y >= gps->dimy) return false;
auto dim = getWindowSize();
if (!gps || y < 0 || y >= dim.y) return false;
Pen tmp(pen);
bool ok = false;
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;
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)
{
auto dim = getWindowSize();
if (!gps || !pen.valid()) return false;
if (x1 < 0) x1 = 0;
if (y1 < 0) y1 = 0;
if (x2 >= gps->dimx) x2 = gps->dimx-1;
if (y2 >= gps->dimy) y2 = gps->dimy-1;
if (x2 >= dim.x) x2 = dim.x-1;
if (y2 >= dim.y) y2 = dim.y-1;
if (x1 > x2 || y1 > y2) return false;
for (int x = x1; x <= x2; x++)
{
int index = x*gps->dimy;
int index = x*dim.y;
for (int y = y1; y <= y2; y++)
doSetTile(pen, index+y);
@ -198,32 +200,33 @@ bool Screen::drawBorder(const std::string &title)
{
if (!gps) return false;
int dimx = gps->dimx, dimy = gps->dimy;
auto dim = getWindowSize();
Pen border('\xDB', 8);
Pen text(0, 0, 7);
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 * dimy + dimy - 1);
doSetTile(border, x * dim.y + 0);
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, (dimx - 1) * dimy + y);
doSetTile(border, 0 * dim.y + 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()
{
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()

@ -118,6 +118,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(forceequip forceequip.cpp)
DFHACK_PLUGIN(manipulator manipulator.cpp)
DFHACK_PLUGIN(search search.cpp)
DFHACK_PLUGIN(automaterial automaterial.cpp)
# this one exports functions to lua
DFHACK_PLUGIN(burrows burrows.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_chest
// || itm->flags.bits.in_inventory
|| itm->flags.bits.artifact1
|| itm->flags.bits.artifact
)
continue;
@ -271,7 +271,7 @@ command_result df_autodump_destroy_item(color_ostream &out, vector <string> & pa
if (item->flags.bits.construction ||
item->flags.bits.in_building ||
item->flags.bits.artifact1)
item->flags.bits.artifact)
{
out.printerr("Choosing not to destroy buildings, constructions and artifacts.\n");
return CR_FAILURE;

@ -1556,7 +1556,7 @@ static int stockcheck(color_ostream &out, vector <string> & parameters)
#define F(x) bad_flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
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);
#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;
F(dump); F(forbid); F(garbage_collect);
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);
#undef F

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

@ -456,21 +456,23 @@ void viewscreen_unitlaborsst::refreshNames()
void viewscreen_unitlaborsst::calcSize()
{
num_rows = gps->dimy - 10;
auto dim = Screen::getWindowSize();
num_rows = dim.y - 10;
if (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
int col_minwidth[DISP_COLUMN_MAX];
int col_maxwidth[DISP_COLUMN_MAX];
col_minwidth[DISP_COLUMN_HAPPINESS] = 4;
col_maxwidth[DISP_COLUMN_HAPPINESS] = 4;
col_minwidth[DISP_COLUMN_NAME] = 0;
col_maxwidth[DISP_COLUMN_NAME] = 0;
col_minwidth[DISP_COLUMN_PROFESSION] = 0;
col_maxwidth[DISP_COLUMN_PROFESSION] = 0;
col_minwidth[DISP_COLUMN_NAME] = 16;
col_maxwidth[DISP_COLUMN_NAME] = 16; // adjusted in the loop below
col_minwidth[DISP_COLUMN_PROFESSION] = 10;
col_maxwidth[DISP_COLUMN_PROFESSION] = 10; // adjusted in the loop below
col_minwidth[DISP_COLUMN_LABORS] = num_columns*3/5; // 60%
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;
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++)
{
if (columns[i].group != col.group)
@ -940,10 +942,11 @@ void viewscreen_unitlaborsst::render()
dfhack_viewscreen::render();
auto dim = Screen::getWindowSize();
Screen::clear();
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_NAME], 2, "Name");
Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_PROFESSION], 2, "Profession");
@ -1057,7 +1060,7 @@ void viewscreen_unitlaborsst::render()
}
}
else
bg = 4;
bg = 3;
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);
}
int x = 2;
OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::SELECT));
OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle labor, ");
int x = 2, y = dim.y - 3;
OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SELECT));
OutputString(canToggle ? 15 : 8, x, y, ": Toggle labor, ");
OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::SELECT_ALL));
OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle Group, ");
OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SELECT_ALL));
OutputString(canToggle ? 15 : 8, x, y, ": Toggle Group, ");
OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::UNITJOB_VIEW));
OutputString(15, x, gps->dimy - 3, ": ViewCre, ");
OutputString(10, x, y, Screen::getKeyDisplay(interface_key::UNITJOB_VIEW));
OutputString(15, x, y, ": ViewCre, ");
OutputString(10, x, gps->dimy - 3, Screen::getKeyDisplay(interface_key::UNITJOB_ZOOM_CRE));
OutputString(15, x, gps->dimy - 3, ": Zoom-Cre");
OutputString(10, x, y, Screen::getKeyDisplay(interface_key::UNITJOB_ZOOM_CRE));
OutputString(15, x, y, ": Zoom-Cre");
x = 2;
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::LEAVESCREEN));
OutputString(15, x, gps->dimy - 2, ": Done, ");
x = 2; y = dim.y - 2;
OutputString(10, x, y, Screen::getKeyDisplay(interface_key::LEAVESCREEN));
OutputString(15, x, y, ": Done, ");
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_DOWN));
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_UP));
OutputString(15, x, gps->dimy - 2, ": Sort by Skill, ");
OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SECONDSCROLL_DOWN));
OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SECONDSCROLL_UP));
OutputString(15, x, y, ": Sort by Skill, ");
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_PAGEDOWN));
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::SECONDSCROLL_PAGEUP));
OutputString(15, x, gps->dimy - 2, ": Sort by (");
OutputString(10, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::CHANGETAB));
OutputString(15, x, gps->dimy - 2, ") ");
OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SECONDSCROLL_PAGEDOWN));
OutputString(10, x, y, Screen::getKeyDisplay(interface_key::SECONDSCROLL_PAGEUP));
OutputString(15, x, y, ": Sort by (");
OutputString(10, x, y, Screen::getKeyDisplay(interface_key::CHANGETAB));
OutputString(15, x, y, ") ");
switch (altsort)
{
case ALTSORT_NAME:
OutputString(15, x, gps->dimy - 2, "Name");
OutputString(15, x, y, "Name");
break;
case ALTSORT_PROFESSION:
OutputString(15, x, gps->dimy - 2, "Profession");
OutputString(15, x, y, "Profession");
break;
case ALTSORT_HAPPINESS:
OutputString(15, x, gps->dimy - 2, "Happiness");
OutputString(15, x, y, "Happiness");
break;
case ALTSORT_ARRIVAL:
OutputString(15, x, gps->dimy - 2, "Arrival");
OutputString(15, x, y, "Arrival");
break;
default:
OutputString(15, x, gps->dimy - 2, "Unknown");
OutputString(15, x, y, "Unknown");
break;
}
}
@ -1193,9 +1196,10 @@ struct unitlist_hook : df::viewscreen_unitlistst
if (units[page].size())
{
int x = 2;
OutputString(12, x, gps->dimy - 2, Screen::getKeyDisplay(interface_key::UNITVIEW_PRF_PROF));
OutputString(15, x, gps->dimy - 2, ": Manage labors (DFHack)");
auto dim = Screen::getWindowSize();
int x = 2, y = dim.y - 2;
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.
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:
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
ticks (advances only when the game is unpaused).
To stop being called, use:

@ -56,7 +56,7 @@ module DFHack
def item_isfree(i)
!i.flags.trader 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.in_building and
!i.flags.owned and

@ -159,8 +159,8 @@ module DFHack
v.inorganic_mat if v
end
# return the world_data.geo_biome for current tile
def geo_biome
# return the RegionMapEntry (from designation.biome)
def region_map_entry
b = designation.biome
wd = df.world.world_data
@ -174,7 +174,12 @@ module DFHack
ry -= 1 if b < 3 and ry > 0
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
# return the world_data.geo_biome.layer for current tile
@ -182,14 +187,53 @@ module DFHack
geo_biome.layers[designation.geolayer_index]
end
# current tile mat_index (vein if applicable, or base material)
def mat_index
mat_index_vein or stone_layer.mat_index
# MaterialInfo: token for current tile, based on tilemat (vein, soil, plant, lava_stone...)
def mat_info
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
# MaterialInfo: inorganic token for current tile
def mat_info
MaterialInfo.new(0, mat_index)
def mat_type
mat_info.mat_type
end
def mat_index
mat_info.mat_index
end
def inspect

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

@ -438,6 +438,12 @@ static VALUE rb_cDFHack;
// 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()
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, "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, "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_int8", RUBY_METHOD_FUNC(rb_dfmemory_read_int8), 1);

@ -23,9 +23,12 @@ module Kernel
end
module DFHack
VERSION = version
class OnupdateCallback
attr_accessor :callback, :timelimit, :minyear, :minyeartick
def initialize(cb, tl, initdelay=0)
attr_accessor :callback, :timelimit, :minyear, :minyeartick, :description
def initialize(descr, cb, tl, initdelay=0)
@description = descr
@callback = cb
@ticklimit = tl
@minyear = (tl ? df.cur_year : 0)
@ -34,22 +37,21 @@ module DFHack
# run callback if timedout
def check_run(year, yeartick, yearlen)
if !@ticklimit
@callback.call
else
if year > @minyear or (year == @minyear and yeartick >= @minyeartick)
@minyear = year
@minyeartick = yeartick + @ticklimit
if @minyeartick > yearlen
@minyear += 1
@minyeartick -= yearlen
end
@callback.call
if @ticklimit
return unless year > @minyear or (year == @minyear and yeartick >= @minyeartick)
@minyear = year
@minyeartick = yeartick + @ticklimit
if @minyeartick > yearlen
@minyear += 1
@minyeartick -= yearlen
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
puts_err "onupdate cb #$!", $!.backtrace
puts_err "onupdate #@description unregistered: #$!", $!.backtrace
end
def <=>(o)
@ -61,10 +63,11 @@ module DFHack
attr_accessor :onupdate_list, :onstatechange_list
# register a callback to be called every gframe or more
# ex: DFHack.onupdate_register { DFHack.world.units[0].counters.job_counter = 0 }
def onupdate_register(ticklimit=nil, initialtickdelay=0, &b)
# ex: DFHack.onupdate_register('fastdwarf') { DFHack.world.units[0].counters.job_counter = 0 }
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 << OnupdateCallback.new(b, ticklimit, initialtickdelay)
@onupdate_list << OnupdateCallback.new(descr, b, ticklimit, initialtickdelay)
DFHack.onupdate_active = true
if onext = @onupdate_list.sort.first
DFHack.onupdate_minyear = onext.minyear
@ -73,8 +76,9 @@ module DFHack
@onupdate_list.last
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)
b = @onupdate_list.find { |bb| bb.description == b } if b.kind_of?(String)
@onupdate_list.delete b
if @onupdate_list.empty?
DFHack.onupdate_active = false

@ -329,8 +329,9 @@ protected:
// Display hotkey message
void print_search_option(int x, int y = -1) const
{
auto dim = Screen::getWindowSize();
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) ? 10 : 15, x, y, ": Search");
@ -413,8 +414,9 @@ public:
print_search_option(2);
else
{
int x = 2;
OutputString(15, x, gps->dimy - 2, "Tab to enable Search");
auto dim = Screen::getWindowSize();
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)
{
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())
{

@ -212,15 +212,31 @@ struct stable_cursor_hook : df::viewscreen_dwarfmodest
{
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))
{
bool was_default = (ui->main.mode == df::ui_sidebar_mode::Default);
bool was_default = check_default();
df::coord view = Gui::getViewportPos();
df::coord cursor = Gui::getCursorPos();
INTERPOSE_NEXT(feed)(input);
bool is_default = (ui->main.mode == df::ui_sidebar_mode::Default);
bool is_default = check_default();
df::coord cur_cursor = Gui::getCursorPos();
if (is_default && !was_default)
@ -241,7 +257,7 @@ struct stable_cursor_hook : df::viewscreen_dwarfmodest
tmp.insert(interface_key::CURSOR_UP_Z);
INTERPOSE_NEXT(feed)(&tmp);
}
else if (cur_cursor.isValid())
else if (!is_default && cur_cursor.isValid())
{
last_cursor = df::coord();
}

@ -291,8 +291,18 @@ int ProtectedJob::cur_tick_idx = 0;
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 {
PersistentDataItem config;
PersistentDataItem history;
// Fixed key parsed into fields
bool is_craft;
@ -308,7 +318,7 @@ struct ItemConstraint {
int weight;
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 is_active, cant_resume_reported;
@ -318,7 +328,7 @@ struct ItemConstraint {
public:
ItemConstraint()
: 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)
{}
@ -352,6 +362,37 @@ public:
request_resume = (size <= goalCount()-goalGap());
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);
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 *
******************************/
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)
{
std::vector<std::string> tokens;
@ -776,6 +824,8 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str
nct->init(str);
}
nct->history = World::GetPersistentData(history_key(nct->config), NULL);
constraints.push_back(nct);
return nct;
}
@ -787,6 +837,7 @@ static void delete_constraint(ItemConstraint *cv)
vector_erase_at(constraints, idx);
World::DeletePersistentData(cv->config);
World::DeletePersistentData(cv->history);
delete cv;
}
@ -1064,7 +1115,8 @@ static void map_job_items(color_ostream &out)
{
constraints[i]->item_amount = 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;
@ -1076,7 +1128,7 @@ static void map_job_items(color_ostream &out)
#define F(x) bad_flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
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
bool dry_buckets = isOptionEnabled(CF_DRYBUCKETS);
@ -1177,7 +1229,8 @@ static void map_job_items(color_ostream &out)
isAssignedSquad(item))
{
is_invalid = true;
cv->item_inuse++;
cv->item_inuse_count++;
cv->item_inuse_amount += item->getStackSize();
}
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)
{
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_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
@ -1378,19 +1451,31 @@ static void push_constraint(lua_State *L, ItemConstraint *cv)
lua_newtable(L);
bool resumed = false, want_resumed = false;
for (size_t i = 0, j = 0; i < cv->jobs.size(); i++)
{
if (!cv->jobs[i]->isLive()) continue;
Lua::PushDFObject(L, cv->jobs[i]->actual_job);
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");
if (want_resumed && !resumed)
Lua::SetField(L, true, ctable, "is_delayed");
}
static int listConstraints(lua_State *L)
{
lua_settop(L, 2);
auto job = Lua::CheckDFObject<df::job>(L, 1);
bool with_history = lua_toboolean(L, 2);
lua_pushnil(L);
@ -1415,6 +1500,13 @@ static int listConstraints(lua_State *L)
for (size_t i = 0; i < vec.size(); 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);
}
@ -1463,6 +1555,24 @@ static int setConstraint(lua_State *L)
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_LUA_FUNCTION(isEnabled),
DFHACK_LUA_FUNCTION(setEnabled),
@ -1474,6 +1584,7 @@ DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(listConstraints),
DFHACK_LUA_COMMAND(findConstraint),
DFHACK_LUA_COMMAND(setConstraint),
DFHACK_LUA_COMMAND(getCountHistory),
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;
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 << "; "
<< cv->item_count << " stacks available, "
<< cv->item_inuse << " in use." << endl;
<< cv->item_inuse_count << " in use." << endl;
if (no_job) return;

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

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

@ -66,20 +66,64 @@ function is_caste_mat(iobj)
end
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
matline = 'no material'
return 'no material'
elseif (iobj.mat_type or -1) >= 0 then
local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index)
local matline
if info then
matline = info:toString()
else
matline = iobj.mat_type..':'..iobj.mat_index
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
return matline
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 }
RangeEditor = defclass(RangeEditor, widgets.Label)
@ -162,6 +206,10 @@ function RangeEditor:onIncRange(field, delta)
self.save_cb(cons)
end
---------------------------
-- NEW CONSTRAINT DIALOG --
---------------------------
NewConstraint = defclass(NewConstraint, gui.FramedScreen)
NewConstraint.focus_path = 'workflow/new'
@ -177,7 +225,7 @@ NewConstraint.ATTRS {
}
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 })
local matlist = {}
@ -202,8 +250,16 @@ function NewConstraint:init(args)
frame = { l = 1, t = 2, w = 26 },
text = {
'Type: ',
{ pen = COLOR_LIGHTCYAN,
text = function() return describe_item_type(self.constraint) end },
{ pen = function()
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, ' ',
{ key = 'CUSTOM_T', text = ': Select, ',
on_activate = self:callback('chooseType') },
@ -277,6 +333,7 @@ function NewConstraint:init(args)
{ key = 'LEAVESCREEN', text = ': Cancel, ',
on_activate = self:callback('dismiss') },
{ key = 'MENU_CONFIRM', key_sep = ': ',
enabled = self:callback('isValid'),
text = function()
if self.is_existing then return 'Update' else return 'Create new' end
end,
@ -295,9 +352,17 @@ function NewConstraint:postinit()
self:onChange()
end
function NewConstraint:isValid()
return self.constraint.item_type >= 0
end
function NewConstraint:onChange()
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
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))
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.focus_path = 'workflow/job'
@ -480,24 +827,12 @@ function JobConstraints:initListChoices(clist, sel_token)
end
itemstr = itemstr .. ' ('..table.concat(lst,',')..')'
end
local matstr = 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
local matstr,matflagstr = describe_material(cons)
table.insert(choices, {
text = {
goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE,
' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', matflagstr
' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', (matflagstr or '')
},
token = cons.token,
obj = cons
@ -552,18 +887,22 @@ function JobConstraints:onNewConstraint()
table.insert(choices, { text = itemstr..' of '..matstr, obj = cons })
end
dlg.showListPrompt(
'New limit',
'Select one of the possible outputs:',
COLOR_WHITE,
choices,
function(idx,item)
dlg.ListBox{
frame_title = 'New limit',
text = 'Select one of the possible outputs:',
text_pen = COLOR_WHITE,
choices = choices,
on_select = function(idx,item)
self:saveConstraint(item.obj)
end,
select2_hint = 'Advanced',
on_select2 = function(idx,item)
NewConstraint{
constraint = item.obj,
on_submit = self:callback('saveConstraint')
}:show()
end
)
end,
}:show()
end
function JobConstraints:onDeleteConstraint()
@ -589,20 +928,25 @@ function JobConstraints:onInput(keys)
end
end
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
local args = {...}
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()
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)
local job = dfhack.gui.getSelectedJob()
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]
when 'here'
$magma_onupdate ||= df.onupdate_register(12) {
$magma_onupdate ||= df.onupdate_register('magmasource', 12) {
# called every 12 game ticks (100x a dwarf day)
if $magma_sources.empty?
df.onupdate_unregister($magma_onupdate)

@ -21,7 +21,7 @@ slayit = lambda { |u|
else
# it's getting hot around here
# !!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
df.onupdate_unregister(ouh)
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
$superdwarf_ids |= [u.id]
if df.gamemode == :ADVENTURE
if df.gamemode == :ADVENTURE and not df.respond_to?(:cur_year_tick_advmode)
onupdate_delay = nil
else
onupdate_delay = 1
end
$superdwarf_onupdate ||= df.onupdate_register(onupdate_delay) {
$superdwarf_onupdate ||= df.onupdate_register('superdwarf', onupdate_delay) {
if $superdwarf_ids.empty?
df.onupdate_unregister($superdwarf_onupdate)
$superdwarf_onupdate = nil