develop
Warmist 2012-11-11 12:46:55 +02:00
commit 0e4df55364
28 changed files with 3132 additions and 222 deletions

File diff suppressed because it is too large Load Diff

@ -1403,31 +1403,14 @@ Basic painting functions:
* ``dfhack.screen.paintTile(pen,x,y[,char,tile])``
Paints a tile using given parameters. Pen is a table with following possible fields:
``ch``
Provides the ordinary tile character, as either a 1-character string or a number.
Can be overridden with the ``char`` function parameter.
``fg``
Foreground color for the ordinary tile. Defaults to COLOR_GREY (7).
``bg``
Background color for the ordinary tile. Defaults to COLOR_BLACK (0).
``bold``
Bright/bold text flag. If *nil*, computed based on (fg & 8); fg is masked to 3 bits.
Otherwise should be *true/false*.
``tile``
Graphical tile id. Ignored unless [GRAPHICS:YES] was in init.txt.
``tile_color = true``
Specifies that the tile should be shaded with *fg/bg*.
``tile_fg, tile_bg``
If specified, overrides *tile_color* and supplies shading colors directly.
Paints a tile using given parameters. See below for a description of pen.
Returns *false* if coordinates out of bounds, or other error.
* ``dfhack.screen.readTile(x,y)``
Retrieves the contents of the specified tile from the screen buffers.
Returns a pen, or *nil* if invalid or TrueType.
Returns a pen object, or *nil* if invalid or TrueType.
* ``dfhack.screen.paintString(pen,x,y,text)``
@ -1458,6 +1441,66 @@ Basic painting functions:
Requests repaint of the screen by setting a flag. Unlike other
functions in this section, this may be used at any time.
* ``dfhack.screen.getKeyDisplay(key)``
Returns the string that should be used to represent the given
logical keybinding on the screen in texts like "press Key to ...".
The "pen" argument used by functions above may be represented by
a table with the following possible fields:
``ch``
Provides the ordinary tile character, as either a 1-character string or a number.
Can be overridden with the ``char`` function parameter.
``fg``
Foreground color for the ordinary tile. Defaults to COLOR_GREY (7).
``bg``
Background color for the ordinary tile. Defaults to COLOR_BLACK (0).
``bold``
Bright/bold text flag. If *nil*, computed based on (fg & 8); fg is masked to 3 bits.
Otherwise should be *true/false*.
``tile``
Graphical tile id. Ignored unless [GRAPHICS:YES] was in init.txt.
``tile_color = true``
Specifies that the tile should be shaded with *fg/bg*.
``tile_fg, tile_bg``
If specified, overrides *tile_color* and supplies shading colors directly.
Alternatively, it may be a pre-parsed native object with the following API:
* ``dfhack.pen.make(base[,pen_or_fg,bg,bold])``
Creates a new pre-parsed pen by combining its arguments according to the
following rules:
1. The ``base`` argument may be a pen object, a pen table as specified above,
or a single color value. In the single value case, it is split into
``fg`` and ``bold`` properties, and others are initialized to 0.
This argument will be converted to a pre-parsed object and returned
if there are no other arguments.
2. If the ``pen_or_fg`` argument is specified as a table or object, it
completely replaces the base, and is returned instead of it.
3. Otherwise, the non-nil subset of the optional arguments is used
to update the ``fg``, ``bg`` and ``bold`` properties of the base.
If the ``bold`` flag is *nil*, but *pen_or_fg* is a number, ``bold``
is deduced from it like in the simple base case.
This function always returns a new pre-parsed pen, or *nil*.
* ``dfhack.pen.parse(base[,pen_or_fg,bg,bold])``
Exactly like the above function, but returns ``base`` or ``pen_or_fg``
directly if they are already a pre-parsed native object.
* ``pen.property``, ``pen.property = value``, ``pairs(pen)``
Pre-parsed pens support reading and setting their properties,
but don't behave exactly like a simple table would; for instance,
assigning to ``pen.tile_color`` also resets ``pen.tile_fg`` and
``pen.tile_bg`` to *nil*.
In order to actually be able to paint to the screen, it is necessary
to create and register a viewscreen (basically a modal dialog) with
the game.
@ -1758,6 +1801,11 @@ environment by the mandatory init file dfhack.lua:
safecall, qerror, mkmodule, reload
* Miscellaneous constants
:NEWLINE, COMMA, PERIOD: evaluate to the relevant character strings.
:DEFAULT_NIL: is an unspecified unique token used by the class module below.
* ``printall(obj)``
If the argument is a lua table or DF object reference, prints all fields.
@ -1868,12 +1916,29 @@ utils
as a guide to which values should be skipped as uninteresting.
The ``force`` argument makes it always return a non-*nil* value.
* ``utils.parse_bitfield_int(value, type_ref)``
Given an int ``value``, and a bitfield type in the ``df`` tree,
it returns a lua table mapping the enabled bit keys to *true*,
unless value is 0, in which case it returns *nil*.
* ``utils.list_bitfield_flags(bitfield[, list])``
Adds all enabled bitfield keys to ``list`` or a newly-allocated
empty sequence, and returns it. The ``bitfield`` argument may
be *nil*.
* ``utils.sort_vector(vector,field,cmpfun)``
Sorts a native vector or lua sequence using the comparator function.
If ``field`` is not *nil*, applies the comparator to the field instead
of the whole object.
* ``utils.linear_index(vector,key[,field])``
Searches for ``key`` in the vector, and returns *index, found_value*,
or *nil* if none found.
* ``utils.binsearch(vector,key,field,cmpfun,min,max)``
Does a binary search in a native vector or lua sequence for
@ -1909,6 +1974,23 @@ utils
Exactly like ``erase_sorted_key``, but if field is specified, takes the key from ``item[field]``.
* ``utils.call_with_string(obj,methodname,...)``
Allocates a temporary string object, calls ``obj:method(tmp,...)``, and
returns the value written into the temporary after deleting it.
* ``utils.getBuildingName(building)``
Returns the string description of the given building.
* ``utils.getBuildingCenter(building)``
Returns an x/y/z table pointing at the building center.
* ``utils.split_string(string, delimiter)``
Splits the string by the given delimiter, and returns a sequence of results.
* ``utils.prompt_yes_no(prompt, default)``
Presents a yes/no prompt to the user. If ``default`` is not *nil*,
@ -1968,19 +2050,32 @@ Implements a trivial single-inheritance class system.
If the default value should be *nil*, use ``ATTRS { foo = DEFAULT_NIL }``.
Declaring an attribute is mostly the same as defining your ``init`` method like this::
function Class.init(args)
self.attr1 = args.attr1 or default1
self.attr2 = args.attr2 or default2
...
end
The main difference is that attributes are processed as a separate
initialization step, before any ``init`` methods are called. They
also make the directy relation between instance fields and constructor
arguments more explicit.
* ``new_obj = Class{ foo = arg, bar = arg, ... }``
Calling the class as a function creates and initializes a new instance.
Initialization happens in this order:
1. An empty instance table is created, and its metatable set.
2. The ``preinit`` method is called via ``invoke_before`` (see below)
with the table used as argument to the class. This method is intended
2. The ``preinit`` methods are called via ``invoke_before`` (see below)
with the table used as argument to the class. These methods are intended
for validating and tweaking that argument table.
3. Declared ATTRS are initialized from the argument table or their default values.
4. The ``init`` method is called via ``invoke_after`` with the argument table.
4. The ``init`` methods are called via ``invoke_after`` with the argument table.
This is the main constructor method.
5. The ``postinit`` method is called via ``invoke_after`` with the argument table.
5. The ``postinit`` methods are called via ``invoke_after`` with the argument table.
Place code that should be called after the object is fully constructed here.
Predefined instance methods:
@ -1995,6 +2090,14 @@ Predefined instance methods:
properly passing in self, and optionally a number of initial arguments too.
The arguments given to the closure are appended to these.
* ``instance:cb_getfield(field_name)``
Returns a closure that returns the specified field of the object when called.
* ``instance:cb_setfield(field_name)``
Returns a closure that sets the specified field to its argument when called.
* ``instance:invoke_before(method_name, args...)``
Navigates the inheritance chain of the instance starting from the most specific
@ -2019,6 +2122,715 @@ Predefined instance methods:
To avoid confusion, these methods cannot be redefined.
==================
In-game UI Library
==================
A number of lua modules with names starting with ``gui`` are dedicated
to wrapping the natives of the ``dfhack.screen`` module in a way that
is easy to use. This allows relatively easily and naturally creating
dialogs that integrate in the main game UI window.
These modules make extensive use of the ``class`` module, and define
things ranging from the basic ``Painter``, ``View`` and ``Screen``
classes, to fully functional predefined dialogs.
gui
===
This module defines the most important classes and functions for
implementing interfaces. This documents those of them that are
considered stable.
Misc
----
* ``USE_GRAPHICS``
Contains the value of ``dfhack.screen.inGraphicsMode()``, which cannot be
changed without restarting the game and thus is constant during the session.
* ``CLEAR_PEN``
The black pen used to clear the screen.
* ``simulateInput(screen, keys...)``
This function wraps an undocumented native function that passes a set of
keycodes to a screen, and is the official way to do that.
Every argument after the initial screen may be *nil*, a numeric keycode,
a string keycode, a sequence of numeric or string keycodes, or a mapping
of keycodes to *true* or *false*. For instance, it is possible to use the
table passed as argument to ``onInput``.
* ``mkdims_xy(x1,y1,x2,y2)``
Returns a table containing the arguments as fields, and also ``width`` and
``height`` that contains the rectangle dimensions.
* ``mkdims_wh(x1,y1,width,height)``
Returns the same kind of table as ``mkdims_xy``, only this time it computes
``x2`` and ``y2``.
* ``is_in_rect(rect,x,y)``
Checks if the given point is within a rectangle, represented by a table produced
by one of the ``mkdims`` functions.
* ``blink_visible(delay)``
Returns *true* or *false*, with the value switching to the opposite every ``delay``
msec. This is intended for rendering blinking interface objects.
* ``getKeyDisplay(keycode)``
Wraps ``dfhack.screen.getKeyDisplay`` in order to allow using strings for the keycode argument.
ViewRect class
--------------
This class represents an on-screen rectangle with an associated independent
clip area rectangle. It is the base of the ``Painter`` class, and is used by
``Views`` to track their client area.
* ``ViewRect{ rect = ..., clip_rect = ..., view_rect = ..., clip_view = ... }``
The constructor has the following arguments:
:rect: The ``mkdims`` rectangle in screen coordinates of the logical viewport.
Defaults to the whole screen.
:clip_rect: The clip rectangle in screen coordinates. Defaults to ``rect``.
:view_rect: A ViewRect object to copy from; overrides both ``rect`` and ``clip_rect``.
:clip_view: A ViewRect object to intersect the specified clip area with.
* ``rect:isDefunct()``
Returns *true* if the clip area is empty, i.e. no painting is possible.
* ``rect:inClipGlobalXY(x,y)``
Checks if these global coordinates are within the clip rectangle.
* ``rect:inClipLocalXY(x,y)``
Checks if these coordinates (specified relative to ``x1,y1``) are within the clip rectangle.
* ``rect:localXY(x,y)``
Converts a pair of global coordinates to local; returns *x_local,y_local*.
* ``rect:globalXY(x,y)``
Converts a pair of local coordinates to global; returns *x_global,y_global*.
* ``rect:viewport(x,y,w,h)`` or ``rect:viewport(subrect)``
Returns a ViewRect representing a sub-rectangle of the current one.
The arguments are specified in local coordinates; the ``subrect``
argument must be a ``mkdims`` table. The returned object consists of
the exact specified rectangle, and a clip area produced by intersecting
it with the clip area of the original object.
Painter class
-------------
The painting natives in ``dfhack.screen`` apply to the whole screen, are
completely stateless and don't implement clipping.
The Painter class inherits from ViewRect to provide clipping and local
coordinates, and tracks current cursor position and current pen.
* ``Painter{ ..., pen = ..., key_pen = ... }``
In addition to ViewRect arguments, Painter accepts a suggestion of
the initial value for the main pen, and the keybinding pen. They
default to COLOR_GREY and COLOR_LIGHTGREEN otherwise.
There are also some convenience functions that wrap this constructor:
- ``Painter.new(rect,pen)``
- ``Painter.new_view(view_rect,pen)``
- ``Painter.new_xy(x1,y1,x2,y2,pen)``
- ``Painter.new_wh(x1,y1,width,height,pen)``
* ``painter:isValidPos()``
Checks if the current cursor position is within the clip area.
* ``painter:viewport(x,y,w,h)``
Like the superclass method, but returns a Painter object.
* ``painter:cursor()``
Returns the current cursor *x,y* in local coordinates.
* ``painter:seek(x,y)``
Sets the current cursor position, and returns *self*.
Either of the arguments may be *nil* to keep the current value.
* ``painter:advance(dx,dy)``
Adds the given offsets to the cursor position, and returns *self*.
Either of the arguments may be *nil* to keep the current value.
* ``painter:newline([dx])``
Advances the cursor to the start of the next line plus the given x offset, and returns *self*.
* ``painter:pen(...)``
Sets the current pen to ``dfhack.pen.parse(old_pen,...)``, and returns *self*.
* ``painter:key_pen(...)``
Sets the current keybinding pen to ``dfhack.pen.parse(old_pen,...)``, and returns *self*.
* ``painter:clear()``
Fills the whole clip rectangle with ``CLEAR_PEN``, and returns *self*.
* ``painter:fill(x1,y1,x2,y2[,...])`` or ``painter:fill(rect[,...])``
Fills the specified local coordinate rectangle with ``dfhack.pen.parse(cur_pen,...)``,
and returns *self*.
* ``painter:char([char[, ...]])``
Paints one character using ``char`` and ``dfhack.pen.parse(cur_pen,...)``; returns *self*.
The ``char`` argument, if not nil, is used to override the ``ch`` property of the pen.
* ``painter:tile([char, tile[, ...]])``
Like above, but also allows overriding the ``tile`` property on ad-hoc basis.
* ``painter:string(text[, ...])``
Paints the string with ``dfhack.pen.parse(cur_pen,...)``; returns *self*.
* ``painter:key(keycode[, ...])``
Paints the description of the keycode using ``dfhack.pen.parse(cur_key_pen,...)``; returns *self*.
As noted above, all painting methods return *self*, in order to allow chaining them like this::
painter:pen(foo):seek(x,y):char(1):advance(1):string('bar')...
View class
----------
This class is the common abstract base of both the stand-alone screens
and common widgets to be used inside them. It defines the basic layout,
rendering and event handling framework.
The class defines the following attributes:
:visible: Specifies that the view should be painted.
:active: Specifies that the view should receive events, if also visible.
:view_id: Specifies an identifier to easily identify the view among subviews.
This is reserved for implementation of top-level views, and should
not be used by widgets for their internal subviews.
It also always has the following fields:
:subviews: Contains a table of all subviews. The sequence part of the
table is used for iteration. In addition, subviews are also
indexed under their *view_id*, if any; see ``addviews()`` below.
These fields are computed by the layout process:
:frame_parent_rect: The ViewRect represeting the client area of the parent view.
:frame_rect: The ``mkdims`` rect of the outer frame in parent-local coordinates.
:frame_body: The ViewRect representing the body part of the View's own frame.
The class has the following methods:
* ``view:addviews(list)``
Adds the views in the list to the ``subviews`` sequence. If any of the views
in the list have ``view_id`` attributes that don't conflict with existing keys
in ``subviews``, also stores them under the string keys. Finally, copies any
non-conflicting string keys from the ``subviews`` tables of the listed views.
Thus, doing something like this::
self:addviews{
Panel{
view_id = 'panel',
subviews = {
Label{ view_id = 'label' }
}
}
}
Would make the label accessible as both ``self.subviews.label`` and
``self.subviews.panel.subviews.label``.
* ``view:getWindowSize()``
Returns the dimensions of the ``frame_body`` rectangle.
* ``view:getMousePos()``
Returns the mouse *x,y* in coordinates local to the ``frame_body``
rectangle if it is within its clip area, or nothing otherwise.
* ``view:updateLayout([parent_rect])``
Recomputes layout of the view and its subviews. If no argument is
given, re-uses the previous parent rect. The process goes as follows:
1. Calls ``preUpdateLayout(parent_rect)`` via ``invoke_before``.
2. Uses ``computeFrame(parent_rect)`` to compute the desired frame.
3. Calls ``postComputeFrame(frame_body)`` via ``invoke_after``.
4. Calls ``updateSubviewLayout(frame_body)`` to update children.
5. Calls ``postUpdateLayout(frame_body)`` via ``invoke_after``.
* ``view:computeFrame(parent_rect)`` *(for overriding)*
Called by ``updateLayout`` in order to compute the frame rectangle(s).
Should return the ``mkdims`` rectangle for the outer frame, and optionally
also for the body frame. If only one rectangle is returned, it is used
for both frames, and the margin becomes zero.
* ``view:updateSubviewLayout(frame_body)``
Calls ``updateLayout`` on all children.
* ``view:render(painter)``
Given the parent's painter, renders the view via the following process:
1. Calls ``onRenderFrame(painter, frame_rect)`` to paint the outer frame.
2. Creates a new painter using the ``frame_body`` rect.
3. Calls ``onRenderBody(new_painter)`` to paint the client area.
4. Calls ``renderSubviews(new_painter)`` to paint visible children.
* ``view:renderSubviews(painter)``
Calls ``render`` on all ``visible`` subviews in the order they
appear in the ``subviews`` sequence.
* ``view:onRenderFrame(painter, rect)`` *(for overriding)*
Called by ``render`` to paint the outer frame; by default does nothing.
* ``view:onRenderBody(painter)`` *(for overriding)*
Called by ``render`` to paint the client area; by default does nothing.
* ``view:onInput(keys)`` *(for overriding)*
Override this to handle events. By default directly calls ``inputToSubviews``.
Return a true value from this method to signal that the event has been handled
and should not be passed on to more views.
* ``view:inputToSubviews(keys)``
Calls ``onInput`` on all visible active subviews, iterating the ``subviews``
sequence in *reverse order*, so that topmost subviews get events first.
Returns *true* if any of the subviews handled the event.
Screen class
------------
This is a View subclass intended for use as a stand-alone dialog or screen.
It adds the following methods:
* ``screen:isShown()``
Returns *true* if the screen is currently in the game engine's display stack.
* ``screen:isDismissed()``
Returns *true* if the screen is dismissed.
* ``screen:isActive()``
Returns *true* if the screen is shown and not dismissed.
* ``screen:invalidate()``
Requests a repaint. Note that currently using it is not necessary, because
repaints are constantly requested automatically, due to issues with native
screens happening otherwise.
* ``screen:renderParent()``
Asks the parent native screen to render itself, or clears the screen if impossible.
* ``screen:sendInputToParent(...)``
Uses ``simulateInput`` to send keypresses to the native parent screen.
* ``screen:show([parent])``
Adds the screen to the display stack with the given screen as the parent;
if parent is not specified, places this one one topmost. Before calling
``dfhack.screen.show``, calls ``self:onAboutToShow(parent)``.
* ``screen:onAboutToShow(parent)`` *(for overriding)*
Called when ``dfhack.screen.show`` is about to be called.
* ``screen:onShow()``
Called by ``dfhack.screen.show`` once the screen is successfully shown.
* ``screen:dismiss()``
Dismisses the screen. A dismissed screen does not receive any more
events or paint requests, but may remain in the display stack for
a short time until the game removes it.
* ``screen:onDismiss()`` *(for overriding)*
Called by ``dfhack.screen.dismiss()``.
* ``screen:onDestroy()`` *(for overriding)*
Called by the native code when the screen is fully destroyed and removed
from the display stack. Place code that absolutely must be called whenever
the screen is removed by any means here.
* ``screen:onResize``, ``screen:onRender``
Defined as callbacks for native code.
FramedScreen class
------------------
A Screen subclass that paints a visible frame around its body.
Most dialogs should inherit from this class.
A framed screen has the following attributes:
:frame_style: A table that defines a set of pens to draw various parts of the frame.
:frame_title: A string to display in the middle of the top of the frame.
:frame_width: Desired width of the client area. If *nil*, the screen will occupy the whole width.
:frame_height: Likewise, for height.
:frame_inset: The gap between the frame and the client area. Defaults to 0.
:frame_background: The pen to fill in the frame with. Defaults to CLEAR_PEN.
There are the following predefined frame style tables:
* ``GREY_FRAME``
A plain grey-colored frame.
* ``BOUNDARY_FRAME``
The same frame as used by the usual full-screen DF views, like dwarfmode.
* ``GREY_LINE_FRAME``
A frame consisting of grey lines, similar to the one used by titan announcements.
gui.widgets
===========
This module implements some basic widgets based on the View infrastructure.
Widget class
------------
Base of all the widgets. Inherits from View and has the following attributes:
* ``frame = {...}``
Specifies the constraints on the outer frame of the widget.
If omitted, the widget will occupy the whole parent rectangle.
The frame is specified as a table with the following possible fields:
:l: gap between the left edges of the frame and the parent.
:t: gap between the top edges of the frame and the parent.
:r: gap between the right edges of the frame and the parent.
:b: gap between the bottom edges of the frame and the parent.
:w: maximum width of the frame.
:h: maximum heigth of the frame.
:xalign: X alignment of the frame.
:yalign: Y alignment of the frame.
First the ``l,t,r,b`` fields restrict the available area for
placing the frame. If ``w`` and ``h`` are not specified or
larger then the computed area, it becomes the frame. Otherwise
the smaller frame is placed within the are based on the
``xalign/yalign`` fields. If the align hints are omitted, they
are assumed to be 0, 1, or 0.5 based on which of the ``l/r/t/b``
fields are set.
* ``frame_inset = {...}``
Specifies the gap between the outer frame, and the client area.
The attribute may be a simple integer value to specify a uniform
inset, or a table with the following fields:
:l: left margin.
:t: top margin.
:r: right margin.
:b: bottom margin.
:x: left/right margin, if ``l`` and/or ``r`` are omitted.
:y: top/bottom margin, if ``t`` and/or ``b`` are omitted.
* ``frame_background = pen``
The pen to fill the outer frame with. Defaults to no fill.
Panel class
-----------
Inherits from Widget, and intended for grouping a number of subviews.
Has attributes:
* ``subviews = {}``
Used to initialize the subview list in the constructor.
* ``on_render = function(painter)``
Called from ``onRenderBody``.
Pages class
-----------
Subclass of Panel; keeps exactly one child visible.
* ``Pages{ ..., selected = ... }``
Specifies which child to select initially; defaults to the first one.
* ``pages:getSelected()``
Returns the selected *index, child*.
* ``pages:setSelected(index)``
Selects the specified child, hiding the previous selected one.
It is permitted to use the subview object, or its ``view_id`` as index.
EditField class
---------------
Subclass of Widget; implements a simple edit field.
Attributes:
:text: The current contents of the field.
:text_pen: The pen to draw the text with.
:on_char: Input validation callback; used as ``on_char(new_char,text)``.
If it returns false, the character is ignored.
:on_change: Change notification callback; used as ``on_change(new_text,old_text)``.
:on_submit: Enter key callback; if set the field will handle the key and call ``on_submit(text)``.
Label class
-----------
This Widget subclass implements flowing semi-static text.
It has the following attributes:
:text_pen: Specifies the pen for active text.
:text_dpen: Specifies the pen for disabled text.
:disabled: Boolean or a callback; if true, the label is disabled.
:enabled: Boolean or a callback; if false, the label is disabled.
:auto_height: Sets self.frame.h from the text height.
:auto_width: Sets self.frame.w from the text width.
The text itself is represented as a complex structure, and passed
to the object via the ``text`` argument of the constructor, or via
the ``setText`` method, as one of:
* A simple string, possibly containing newlines.
* A sequence of tokens.
Every token in the sequence in turn may be either a string, possibly
containing newlines, or a table with the following possible fields:
* ``token.text = ...``
Specifies the main text content of a token, and may be a string, or
a callback returning a string.
* ``token.gap = ...``
Specifies the number of character positions to advance on the line
before rendering the token.
* ``token.tile = pen``
Specifies a pen to paint as one tile before the main part of the token.
* ``token.key = '...'``
Specifies the keycode associated with the token. The string description
of the key binding is added to the text content of the token.
* ``token.key_sep = '...'``
Specifies the separator to place between the keybinding label produced
by ``token.key``, and the main text of the token. If the separator is
'()', the token is formatted as ``text..' ('..binding..')'``. Otherwise
it is simply ``binding..sep..text``.
* ``token.enabled``, ``token.disabled``
Same as the attributes of the label itself, but applies only to the token.
* ``token.pen``, ``token.dpen``
Specify the pen and disabled pen to be used for the token's text.
The field may be either the pen itself, or a callback that returns it.
* ``token.on_activate``
If this field is not nil, and ``token.key`` is set, the token will actually
respond to that key binding unless disabled, and call this callback. Eventually
this may be extended with mouse click support.
* ``token.id``
Specifies a unique identifier for the token.
* ``token.line``, ``token.x1``, ``token.x2``
Reserved for internal use.
The Label widget implements the following methods:
* ``label:setText(new_text)``
Replaces the text currently contained in the widget.
* ``label:itemById(id)``
Finds a token by its ``id`` field.
* ``label:getTextHeight()``
Computes the height of the text.
* ``label:getTextWidth()``
Computes the width of the text.
List class
----------
The List widget implements a simple list with paging.
It has the following attributes:
:text_pen: Specifies the pen for deselected list entries.
:cursor_pen: Specifies the pen for the selected entry.
:inactive_pen: If specified, used for the cursor when the widget is not active.
:icon_pen: Default pen for icons.
: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)``.
: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.
:scroll_keys: Specifies which keys the list should react to as a table.
Every list item may be specified either as a string, or as a lua table
with the following fields:
:text: Specifies the label text in the same format as the Label text.
:caption, [1]: Deprecated legacy aliases for **text**.
:text_*: Reserved for internal use.
:key: Specifies a keybinding that acts as a shortcut for the specified item.
:icon: Specifies an icon string, or a pen to paint a single character. May be a callback.
:icon_pen: When the icon is a string, used to paint it.
The list supports the following methods:
* ``List{ ..., choices = ..., selected = ... }``
Same as calling ``setChoices`` after construction.
* ``list:setChoices(choices[, selected])``
Replaces the list of choices, possibly also setting the currently selected index.
* ``list:setSelected(selected)``
Sets the currently selected index. Returns the index after validation.
* ``list:getChoices()``
Returns the list of choices.
* ``list:getSelected()``
Returns the selected *index, choice*, or nothing if the list is empty.
* ``list:getContentWidth()``
Returns the minimal width to draw all choices without clipping.
* ``list:getContentHeight()``
Returns the minimal width to draw all choices without scrolling.
* ``list:submit()``
Call the ``on_submit`` callback, as if the Enter key was handled.
FilteredList class
------------------
This widget combines List, EditField and Label into a combo-box like
construction that allows filtering the list by subwords of its items.
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.
:not_found_label: Specifies the text of the label shown when no items match the filter.
The list choices may include the following attributes:
:search_key: If specified, used instead of **text** to match against the filter.
The widget implements:
* ``list:setChoices(choices[, selected])``
Resets the filter, and passes through to the inner list.
* ``list:getChoices()``
Returns the list of *all* choices.
* ``list:getFilter()``
Returns the current filter string, and the *filtered* list of choices.
* ``list:setFilter(filter[,pos])``
Sets the new filter string, filters the list, and selects the item at
index ``pos`` in the *unfiltered* list if possible.
* ``list:canSubmit()``
Checks if there are currently any choices in the filtered list.
* ``list:getSelected()``, ``list:getContentWidth()``, ``list:getContentHeight()``, ``list:submit()``
Same as with an ordinary list.
=======
Plugins

@ -10,6 +10,8 @@ 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 scripts:
- region-pops: displays animal populations of the region and allows tweaking them.
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.
@ -19,10 +21,15 @@ DFHack future
- properly considers minecarts assigned to routes busy.
- code for deducing job outputs rewritten in lua for flexibility.
- logic fix: collecting webs produces silk, and ungathered webs are not thread.
- items assigned to squads are considered busy, even if not in inventory.
- shearing and milking jobs are supported, but only with generic MILK or YARN outputs.
New Fix Armory plugin:
Together with a couple of binary patches and the gui/assign-rack script,
this plugin makes weapon racks, armor stands, chests and cabinets in
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.
DFHack v0.34.11-r2

@ -500,33 +500,34 @@ access DF memory and allow for easier development of new tools.</p>
</li>
<li><a class="reference internal" href="#in-game-interface-tools" id="id126">In-game interface tools</a><ul>
<li><a class="reference internal" href="#dwarf-manipulator" id="id127">Dwarf Manipulator</a></li>
<li><a class="reference internal" href="#gui-liquids" id="id128">gui/liquids</a></li>
<li><a class="reference internal" href="#gui-mechanisms" id="id129">gui/mechanisms</a></li>
<li><a class="reference internal" href="#gui-rename" id="id130">gui/rename</a></li>
<li><a class="reference internal" href="#gui-room-list" id="id131">gui/room-list</a></li>
<li><a class="reference internal" href="#gui-choose-weapons" id="id132">gui/choose-weapons</a></li>
<li><a class="reference internal" href="#gui-guide-path" id="id133">gui/guide-path</a></li>
<li><a class="reference internal" href="#gui-workshop-job" id="id134">gui/workshop-job</a></li>
<li><a class="reference internal" href="#gui-workflow" id="id135">gui/workflow</a></li>
<li><a class="reference internal" href="#gui-assign-rack" id="id136">gui/assign-rack</a></li>
<li><a class="reference internal" href="#search" id="id128">Search</a></li>
<li><a class="reference internal" href="#gui-liquids" id="id129">gui/liquids</a></li>
<li><a class="reference internal" href="#gui-mechanisms" id="id130">gui/mechanisms</a></li>
<li><a class="reference internal" href="#gui-rename" id="id131">gui/rename</a></li>
<li><a class="reference internal" href="#gui-room-list" id="id132">gui/room-list</a></li>
<li><a class="reference internal" href="#gui-choose-weapons" id="id133">gui/choose-weapons</a></li>
<li><a class="reference internal" href="#gui-guide-path" id="id134">gui/guide-path</a></li>
<li><a class="reference internal" href="#gui-workshop-job" id="id135">gui/workshop-job</a></li>
<li><a class="reference internal" href="#gui-workflow" id="id136">gui/workflow</a></li>
<li><a class="reference internal" href="#gui-assign-rack" id="id137">gui/assign-rack</a></li>
</ul>
</li>
<li><a class="reference internal" href="#behavior-mods" id="id137">Behavior Mods</a><ul>
<li><a class="reference internal" href="#siege-engine" id="id138">Siege Engine</a><ul>
<li><a class="reference internal" href="#rationale" id="id139">Rationale</a></li>
<li><a class="reference internal" href="#configuration-ui" id="id140">Configuration UI</a></li>
<li><a class="reference internal" href="#behavior-mods" id="id138">Behavior Mods</a><ul>
<li><a class="reference internal" href="#siege-engine" id="id139">Siege Engine</a><ul>
<li><a class="reference internal" href="#rationale" id="id140">Rationale</a></li>
<li><a class="reference internal" href="#configuration-ui" id="id141">Configuration UI</a></li>
</ul>
</li>
<li><a class="reference internal" href="#power-meter" id="id141">Power Meter</a></li>
<li><a class="reference internal" href="#steam-engine" id="id142">Steam Engine</a><ul>
<li><a class="reference internal" href="#id1" id="id143">Rationale</a></li>
<li><a class="reference internal" href="#construction" id="id144">Construction</a></li>
<li><a class="reference internal" href="#operation" id="id145">Operation</a></li>
<li><a class="reference internal" href="#explosions" id="id146">Explosions</a></li>
<li><a class="reference internal" href="#save-files" id="id147">Save files</a></li>
<li><a class="reference internal" href="#power-meter" id="id142">Power Meter</a></li>
<li><a class="reference internal" href="#steam-engine" id="id143">Steam Engine</a><ul>
<li><a class="reference internal" href="#id1" id="id144">Rationale</a></li>
<li><a class="reference internal" href="#construction" id="id145">Construction</a></li>
<li><a class="reference internal" href="#operation" id="id146">Operation</a></li>
<li><a class="reference internal" href="#explosions" id="id147">Explosions</a></li>
<li><a class="reference internal" href="#save-files" id="id148">Save files</a></li>
</ul>
</li>
<li><a class="reference internal" href="#add-spatter" id="id148">Add Spatter</a></li>
<li><a class="reference internal" href="#add-spatter" id="id149">Add Spatter</a></li>
</ul>
</li>
</ul>
@ -2691,14 +2692,33 @@ cursor onto that cell instead of toggling it.</li>
<p>Pressing ESC normally returns to the unit screen, but Shift-ESC would exit
directly to the main dwarf mode screen.</p>
</div>
<div class="section" id="search">
<h2><a class="toc-backref" href="#id128">Search</a></h2>
<p>The search plugin adds search to the Stocks, Trading and Unit List screens.</p>
<p>Searching works the same way as the search option in &quot;Move to Depot&quot; does.
You will see the Search option displayed on screen with a hotkey (usually 's').
Pressing it lets you start typing a query and the relevant list will start
filtering automatically.</p>
<p>Pressing ENTER, ESC or the arrow keys will return you to browsing the now
filtered list, which still functions as normal. You can clear the filter
by either going back into search mode and backspacing to delete it, or
pressing the &quot;shifted&quot; version of the search hotkey while browsing the
list (e.g. if the hotkey is 's', then hitting 'shift-s' will clear any
filter).</p>
<p>Leaving any screen automatically clears the filter.</p>
<p>In the Trade screen, the actual trade will always only act on items that
are actually visible in the list; the same effect applies to the Trade
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.</p>
</div>
<div class="section" id="gui-liquids">
<h2><a class="toc-backref" href="#id128">gui/liquids</a></h2>
<h2><a class="toc-backref" href="#id129">gui/liquids</a></h2>
<p>To use, bind to a key and activate in the 'k' mode.</p>
<p>While active, use the suggested keys to switch the usual liquids parameters, and Enter
to select the target area and apply changes.</p>
</div>
<div class="section" id="gui-mechanisms">
<h2><a class="toc-backref" href="#id129">gui/mechanisms</a></h2>
<h2><a class="toc-backref" href="#id130">gui/mechanisms</a></h2>
<p>To use, bind to a key and activate in the 'q' mode.</p>
<p>Lists mechanisms connected to the building, and their links. Navigating the list centers
the view on the relevant linked buildings.</p>
@ -2707,7 +2727,7 @@ focus on the current one. Shift-Enter has an effect equivalent to pressing Enter
re-entering the mechanisms ui.</p>
</div>
<div class="section" id="gui-rename">
<h2><a class="toc-backref" href="#id130">gui/rename</a></h2>
<h2><a class="toc-backref" href="#id131">gui/rename</a></h2>
<p>Backed by the rename plugin, this script allows entering the desired name
via a simple dialog in the game ui.</p>
<ul>
@ -2723,14 +2743,14 @@ It is also possible to rename zones from the 'i' menu.</p>
<p>The <tt class="docutils literal">building</tt> or <tt class="docutils literal">unit</tt> options are automatically assumed when in relevant ui state.</p>
</div>
<div class="section" id="gui-room-list">
<h2><a class="toc-backref" href="#id131">gui/room-list</a></h2>
<h2><a class="toc-backref" href="#id132">gui/room-list</a></h2>
<p>To use, bind to a key and activate in the 'q' mode, either immediately or after opening
the assign owner page.</p>
<p>The script lists other rooms owned by the same owner, or by the unit selected in the assign
list, and allows unassigning them.</p>
</div>
<div class="section" id="gui-choose-weapons">
<h2><a class="toc-backref" href="#id132">gui/choose-weapons</a></h2>
<h2><a class="toc-backref" href="#id133">gui/choose-weapons</a></h2>
<p>Bind to a key, and activate in the Equip-&gt;View/Customize page of the military screen.</p>
<p>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
@ -2740,13 +2760,13 @@ only that entry, and does it even if it is not 'individual choice'.</p>
and may lead to inappropriate weapons being selected.</p>
</div>
<div class="section" id="gui-guide-path">
<h2><a class="toc-backref" href="#id133">gui/guide-path</a></h2>
<h2><a class="toc-backref" href="#id134">gui/guide-path</a></h2>
<p>Bind to a key, and activate in the Hauling menu with the cursor over a Guide order.</p>
<p>The script displays the cached path that will be used by the order; the game
computes it when the order is executed for the first time.</p>
</div>
<div class="section" id="gui-workshop-job">
<h2><a class="toc-backref" href="#id134">gui/workshop-job</a></h2>
<h2><a class="toc-backref" href="#id135">gui/workshop-job</a></h2>
<p>Bind to a key, and activate with a job selected in a workshop in the 'q' mode.</p>
<p>The script shows a list of the input reagents of the selected job, and allows changing
them like the <tt class="docutils literal">job <span class="pre">item-type</span></tt> and <tt class="docutils literal">job <span class="pre">item-material</span></tt> commands.</p>
@ -2774,7 +2794,7 @@ and then try to change the input item type, now it won't let you select <em>plan
you have to unset the material first.</p>
</div>
<div class="section" id="gui-workflow">
<h2><a class="toc-backref" href="#id135">gui/workflow</a></h2>
<h2><a class="toc-backref" href="#id136">gui/workflow</a></h2>
<p>Bind to a key, and activate with a job selected in a workshop in the 'q' mode.</p>
<p>This script provides a simple interface to constraints managed by the workflow
plugin. When active, it displays a list of all constraints applicable to the
@ -2796,7 +2816,7 @@ as described in <tt class="docutils literal">workflow</tt> documentation above.
can be used for troubleshooting jobs that don't match the right constraints.</p>
</div>
<div class="section" id="gui-assign-rack">
<h2><a class="toc-backref" href="#id136">gui/assign-rack</a></h2>
<h2><a class="toc-backref" href="#id137">gui/assign-rack</a></h2>
<p>Bind to a key, and activate when viewing a weapon rack in the 'q' mode.</p>
<p>This script is part of a group of related fixes to make the armory storage
work again. The existing issues are:</p>
@ -2816,7 +2836,7 @@ the intended user.</p>
</div>
</div>
<div class="section" id="behavior-mods">
<h1><a class="toc-backref" href="#id137">Behavior Mods</a></h1>
<h1><a class="toc-backref" href="#id138">Behavior Mods</a></h1>
<p>These plugins, when activated via configuration UI or by detecting certain
structures in RAWs, modify the game engine behavior concerning the target
objects to add features not otherwise present.</p>
@ -2827,20 +2847,20 @@ technical challenge, and do not represent any long-term plans to produce more
similar modifications of the game.</p>
</div>
<div class="section" id="siege-engine">
<h2><a class="toc-backref" href="#id138">Siege Engine</a></h2>
<h2><a class="toc-backref" href="#id139">Siege Engine</a></h2>
<p>The siege-engine plugin enables siege engines to be linked to stockpiles, and
aimed at an arbitrary rectangular area across Z levels, instead of the original
four directions. Also, catapults can be ordered to load arbitrary objects, not
just stones.</p>
<div class="section" id="rationale">
<h3><a class="toc-backref" href="#id139">Rationale</a></h3>
<h3><a class="toc-backref" href="#id140">Rationale</a></h3>
<p>Siege engines are a very interesting feature, but sadly almost useless in the current state
because they haven't been updated since 2D and can only aim in four directions. This is an
attempt to bring them more up to date until Toady has time to work on it. Actual improvements,
e.g. like making siegers bring their own, are something only Toady can do.</p>
</div>
<div class="section" id="configuration-ui">
<h3><a class="toc-backref" href="#id140">Configuration UI</a></h3>
<h3><a class="toc-backref" href="#id141">Configuration UI</a></h3>
<p>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.</p>
<p>The main mode displays the current target, selected ammo item type, linked stockpiles and
@ -2861,7 +2881,7 @@ menu.</p>
</div>
</div>
<div class="section" id="power-meter">
<h2><a class="toc-backref" href="#id141">Power Meter</a></h2>
<h2><a class="toc-backref" href="#id142">Power Meter</a></h2>
<p>The power-meter plugin implements a modified pressure plate that detects power being
supplied to gear boxes built in the four adjacent N/S/W/E tiles.</p>
<p>The configuration front-end is implemented by the gui/power-meter script. Bind it to a
@ -2870,11 +2890,11 @@ key and activate after selecting Pressure Plate in the build menu.</p>
configuration page, but configures parameters relevant to the modded power meter building.</p>
</div>
<div class="section" id="steam-engine">
<h2><a class="toc-backref" href="#id142">Steam Engine</a></h2>
<h2><a class="toc-backref" href="#id143">Steam Engine</a></h2>
<p>The steam-engine plugin detects custom workshops with STEAM_ENGINE in
their token, and turns them into real steam engines.</p>
<div class="section" id="id1">
<h3><a class="toc-backref" href="#id143">Rationale</a></h3>
<h3><a class="toc-backref" href="#id144">Rationale</a></h3>
<p>The vanilla game contains only water wheels and windmills as sources of
power, but windmills give relatively little power, and water wheels require
flowing water, which must either be a real river and thus immovable and
@ -2885,7 +2905,7 @@ it can be done just by combining existing features of the game engine
in a new way with some glue code and a bit of custom logic.</p>
</div>
<div class="section" id="construction">
<h3><a class="toc-backref" href="#id144">Construction</a></h3>
<h3><a class="toc-backref" href="#id145">Construction</a></h3>
<p>The workshop needs water as its input, which it takes via a
passable floor tile below it, like usual magma workshops do.
The magma version also needs magma.</p>
@ -2909,7 +2929,7 @@ short axles that can be built later than both of the engines.</p>
</div>
</div>
<div class="section" id="operation">
<h3><a class="toc-backref" href="#id145">Operation</a></h3>
<h3><a class="toc-backref" href="#id146">Operation</a></h3>
<p>In order to operate the engine, queue the Stoke Boiler job (optionally
on repeat). A furnace operator will come, possibly bringing a bar of fuel,
and perform it. As a result, a &quot;boiling water&quot; item will appear
@ -2940,7 +2960,7 @@ decrease it by further 4%, and also decrease the whole steam
use rate by 10%.</p>
</div>
<div class="section" id="explosions">
<h3><a class="toc-backref" href="#id146">Explosions</a></h3>
<h3><a class="toc-backref" href="#id147">Explosions</a></h3>
<p>The engine must be constructed using barrel, pipe and piston
from fire-safe, or in the magma version magma-safe metals.</p>
<p>During operation weak parts get gradually worn out, and
@ -2949,7 +2969,7 @@ toppled during operation by a building destroyer, or a
tantruming dwarf.</p>
</div>
<div class="section" id="save-files">
<h3><a class="toc-backref" href="#id147">Save files</a></h3>
<h3><a class="toc-backref" href="#id148">Save files</a></h3>
<p>It should be safe to load and view engine-using fortresses
from a DF version without DFHack installed, except that in such
case the engines won't work. However actually making modifications
@ -2960,7 +2980,7 @@ being generated.</p>
</div>
</div>
<div class="section" id="add-spatter">
<h2><a class="toc-backref" href="#id148">Add Spatter</a></h2>
<h2><a class="toc-backref" href="#id149">Add Spatter</a></h2>
<p>This plugin makes reactions with names starting with <tt class="docutils literal">SPATTER_ADD_</tt>
produce contaminants on the items instead of improvements. The produced
contaminants are immune to being washed away by water or destroyed by

@ -1898,6 +1898,31 @@ Pressing ESC normally returns to the unit screen, but Shift-ESC would exit
directly to the main dwarf mode screen.
Search
======
The search plugin adds search to the Stocks, Trading and Unit List screens.
Searching works the same way as the search option in "Move to Depot" does.
You will see the Search option displayed on screen with a hotkey (usually 's').
Pressing it lets you start typing a query and the relevant list will start
filtering automatically.
Pressing ENTER, ESC or the arrow keys will return you to browsing the now
filtered list, which still functions as normal. You can clear the filter
by either going back into search mode and backspacing to delete it, or
pressing the "shifted" version of the search hotkey while browsing the
list (e.g. if the hotkey is 's', then hitting 'shift-s' will clear any
filter).
Leaving any screen automatically clears the filter.
In the Trade screen, the actual trade will always only act on items that
are actually visible in the list; the same effect applies to the Trade
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.
gui/liquids
===========

@ -18,7 +18,7 @@ keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
# gui/rename script
keybinding add Ctrl-Shift-N gui/rename
keybinding add Alt-Shift-P "gui/rename unit-profession"
keybinding add Ctrl-Shift-T "gui/rename unit-profession"
##############################
# Generic adv mode bindings #

@ -725,6 +725,316 @@ static void OpenMatinfo(lua_State *state)
lua_pop(state, 1);
}
/**************
* Pen object *
**************/
static int DFHACK_PEN_TOKEN = 0;
void Lua::Push(lua_State *L, const Screen::Pen &info)
{
if (!info.valid())
{
lua_pushnil(L);
return;
}
void *pdata = lua_newuserdata(L, sizeof(Pen));
lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_PEN_TOKEN);
lua_setmetatable(L, -2);
new (pdata) Pen(info);
}
static Pen *check_pen_native(lua_State *L, int index)
{
lua_rawgetp(L, LUA_REGISTRYINDEX, &DFHACK_PEN_TOKEN);
if (!lua_getmetatable(L, index) || !lua_rawequal(L, -1, -2))
luaL_argerror(L, index, "not a pen object");
lua_pop(L, 2);
return (Pen*)lua_touserdata(L, index);
}
void Lua::CheckPen(lua_State *L, Screen::Pen *pen, int index, bool allow_nil, bool allow_color)
{
index = lua_absindex(L, index);
luaL_checkany(L, index);
if (lua_isnil(L, index))
{
if (!allow_nil)
luaL_argerror(L, index, "nil pen not allowed");
*pen = Pen(0,0,0,-1);
}
else if (lua_isuserdata(L, index))
{
*pen = *check_pen_native(L, index);
}
else if (allow_color && lua_isnumber(L, index))
{
*pen = Pen(0, lua_tointeger(L, index)&15, 0);
}
else
{
luaL_checktype(L, index, LUA_TTABLE);
decode_pen(L, *pen, index);
}
}
static int adjust_pen(lua_State *L, bool no_copy)
{
lua_settop(L, 4);
Pen pen;
int iidx = 1;
Lua::CheckPen(L, &pen, 1, true, true);
if (!lua_isnil(L, 2) || !lua_isnil(L, 3) || !lua_isnil(L, 4))
{
if (lua_isnumber(L, 2) || lua_isnil(L, 2))
{
if (!pen.valid())
pen = Pen();
iidx = -1;
pen.fg = luaL_optint(L, 2, pen.fg) & 15;
pen.bg = luaL_optint(L, 3, pen.bg);
if (!lua_isnil(L, 4))
pen.bold = lua_toboolean(L, 4);
else if (!lua_isnil(L, 2))
{
pen.bold = !!(pen.fg & 8);
pen.fg &= 7;
}
}
else
{
iidx = 2;
Lua::CheckPen(L, &pen, 2, false, false);
}
}
if (no_copy && iidx > 0 && lua_isuserdata(L, iidx))
lua_pushvalue(L, iidx);
else
Lua::Push(L, pen);
return 1;
}
static int dfhack_pen_parse(lua_State *L)
{
return adjust_pen(L, true);
}
static int dfhack_pen_make(lua_State *L)
{
return adjust_pen(L, false);
}
static void make_pen_table(lua_State *L, Pen &pen)
{
if (!pen.valid())
luaL_error(L, "invalid pen state");
else
{
lua_newtable(L);
lua_pushinteger(L, (unsigned char)pen.ch); lua_setfield(L, -2, "ch");
lua_pushinteger(L, pen.fg); lua_setfield(L, -2, "fg");
lua_pushinteger(L, pen.bg); lua_setfield(L, -2, "bg");
lua_pushboolean(L, pen.bold); lua_setfield(L, -2, "bold");
if (pen.tile)
{
lua_pushinteger(L, pen.tile); lua_setfield(L, -2, "tile");
}
switch (pen.tile_mode) {
case Pen::CharColor:
lua_pushboolean(L, true); lua_setfield(L, -2, "tile_color");
break;
case Pen::TileColor:
lua_pushinteger(L, pen.tile_fg); lua_setfield(L, -2, "tile_fg");
lua_pushinteger(L, pen.tile_bg); lua_setfield(L, -2, "tile_bg");
break;
default:
lua_pushboolean(L, false); lua_setfield(L, -2, "tile_color");
break;
}
}
}
static void get_pen_mirror(lua_State *L, int idx)
{
lua_getuservalue(L, idx);
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
Pen pen;
Lua::CheckPen(L, &pen, idx, false, false);
make_pen_table(L, pen);
lua_dup(L);
lua_setuservalue(L, idx);
}
}
static int dfhack_pen_index(lua_State *L)
{
lua_settop(L, 2);
luaL_checktype(L, 1, LUA_TUSERDATA);
// check metatable
if (!lua_getmetatable(L, 1))
luaL_argerror(L, 1, "must be a pen");
lua_pushvalue(L, 2);
lua_rawget(L, -2);
if (!lua_isnil(L, -1))
return 1;
// otherwise read from the mirror table, creating it if necessary
lua_settop(L, 2);
get_pen_mirror(L, 1);
lua_pushvalue(L, 2);
lua_rawget(L, -2);
return 1;
}
static int pen_pnext(lua_State *L)
{
lua_settop(L, 2); /* create a 2nd argument if there isn't one */
if (lua_next(L, lua_upvalueindex(1)))
return 2;
lua_pushnil(L);
return 1;
}
static int dfhack_pen_pairs(lua_State *L)
{
luaL_checktype(L, 1, LUA_TUSERDATA);
get_pen_mirror(L, 1);
lua_pushcclosure(L, pen_pnext, 1);
lua_pushnil(L);
lua_pushnil(L);
return 3;
}
const char *const pen_fields[] = {
"ch", "fg", "bold", "bg", "tile", "tile_color", "tile_fg", "tile_bg", NULL
};
static int dfhack_pen_newindex(lua_State *L)
{
lua_settop(L, 3);
luaL_checktype(L, 1, LUA_TUSERDATA);
int id = luaL_checkoption(L, 2, NULL, pen_fields);
int arg = 0;
Pen &pen = *check_pen_native(L, 1);
bool wipe_tile = false, wipe_tc = false;
switch (id) {
case 0:
if (lua_type(L, 3) != LUA_TNUMBER)
arg = (unsigned char)*luaL_checkstring(L, 3);
else
arg = luaL_checkint(L, 3);
pen.ch = arg;
lua_pushinteger(L, (unsigned char)pen.ch);
break;
case 1:
pen.fg = luaL_checkint(L, 3) & 15;
lua_pushinteger(L, pen.fg);
break;
case 2:
pen.bold = lua_toboolean(L, 3);
lua_pushboolean(L, pen.bold);
break;
case 3:
pen.bg = luaL_checkint(L, 3) & 15;
lua_pushinteger(L, pen.bg);
break;
case 4:
arg = lua_isnil(L, 3) ? 0 : luaL_checkint(L, 3);
if (arg < 0)
luaL_argerror(L, 3, "invalid tile index");
pen.tile = arg;
if (pen.tile)
lua_pushinteger(L, pen.tile);
else
lua_pushnil(L);
break;
case 5:
wipe_tile = (pen.tile_mode == Pen::TileColor);
pen.tile_mode = lua_toboolean(L, 3) ? Pen::CharColor : Pen::AsIs;
lua_pushboolean(L, pen.tile_mode == Pen::CharColor);
break;
case 6:
if (pen.tile_mode != Pen::TileColor) { wipe_tc = true; pen.tile_bg = 0; }
pen.tile_fg = luaL_checkint(L, 3) & 15;
pen.tile_mode = Pen::TileColor;
lua_pushinteger(L, pen.tile_fg);
break;
case 7:
if (pen.tile_mode != Pen::TileColor) { wipe_tc = true; pen.tile_fg = 7; }
pen.tile_bg = luaL_checkint(L, 3) & 15;
pen.tile_mode = Pen::TileColor;
lua_pushinteger(L, pen.tile_bg);
break;
}
lua_getuservalue(L, 1);
if (!lua_isnil(L, -1))
{
lua_remove(L, 3);
lua_insert(L, 2);
lua_rawset(L, 2);
if (wipe_tc) {
lua_pushnil(L); lua_setfield(L, 2, "tile_color");
lua_pushinteger(L, pen.tile_fg); lua_setfield(L, 2, "tile_fg");
lua_pushinteger(L, pen.tile_bg); lua_setfield(L, 2, "tile_bg");
}
if (wipe_tile) {
lua_pushnil(L); lua_setfield(L, 2, "tile_fg");
lua_pushnil(L); lua_setfield(L, 2, "tile_bg");
}
}
return 0;
}
static const luaL_Reg dfhack_pen_funcs[] = {
{ "parse", dfhack_pen_parse },
{ "make", dfhack_pen_make },
{ "__index", dfhack_pen_index },
{ "__pairs", dfhack_pen_pairs },
{ "__newindex", dfhack_pen_newindex },
{ NULL, NULL }
};
static void OpenPen(lua_State *state)
{
luaL_getsubtable(state, lua_gettop(state), "pen");
lua_dup(state);
lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_PEN_TOKEN);
luaL_setfuncs(state, dfhack_pen_funcs, 0);
lua_pop(state, 1);
}
/************************
* Wrappers for C++ API *
************************/
@ -1251,7 +1561,7 @@ static int screen_getWindowSize(lua_State *L)
static int screen_paintTile(lua_State *L)
{
Pen pen;
decode_pen(L, pen, 1);
Lua::CheckPen(L, &pen, 1);
int x = luaL_checkint(L, 2);
int y = luaL_checkint(L, 3);
if (lua_gettop(L) >= 4 && !lua_isnil(L, 4))
@ -1272,44 +1582,14 @@ static int screen_readTile(lua_State *L)
int x = luaL_checkint(L, 1);
int y = luaL_checkint(L, 2);
Pen pen = Screen::readTile(x, y);
if (!pen.valid())
{
lua_pushnil(L);
}
else
{
lua_newtable(L);
lua_pushinteger(L, pen.ch); lua_setfield(L, -2, "ch");
lua_pushinteger(L, pen.fg); lua_setfield(L, -2, "fg");
lua_pushinteger(L, pen.bg); lua_setfield(L, -2, "bg");
lua_pushboolean(L, pen.bold); lua_setfield(L, -2, "bold");
if (pen.tile)
{
lua_pushinteger(L, pen.tile); lua_setfield(L, -2, "tile");
switch (pen.tile_mode) {
case Pen::CharColor:
lua_pushboolean(L, true); lua_setfield(L, -2, "tile_color");
break;
case Pen::TileColor:
lua_pushinteger(L, pen.tile_fg); lua_setfield(L, -2, "tile_fg");
lua_pushinteger(L, pen.tile_bg); lua_setfield(L, -2, "tile_bg");
break;
default:
break;
}
}
}
Lua::Push(L, pen);
return 1;
}
static int screen_paintString(lua_State *L)
{
Pen pen;
decode_pen(L, pen, 1);
Lua::CheckPen(L, &pen, 1);
int x = luaL_checkint(L, 2);
int y = luaL_checkint(L, 3);
const char *text = luaL_checkstring(L, 4);
@ -1320,7 +1600,7 @@ static int screen_paintString(lua_State *L)
static int screen_fillRect(lua_State *L)
{
Pen pen;
decode_pen(L, pen, 1);
Lua::CheckPen(L, &pen, 1);
int x1 = luaL_checkint(L, 2);
int y1 = luaL_checkint(L, 3);
int x2 = luaL_checkint(L, 4);
@ -1720,6 +2000,7 @@ void OpenDFHackApi(lua_State *state)
{
OpenPersistent(state);
OpenMatinfo(state);
OpenPen(state);
LuaWrapper::SetFunctionWrappers(state, dfhack_module);
OpenModule(state, "gui", dfhack_gui_module);

@ -41,6 +41,9 @@ namespace DFHack {
namespace Units {
struct NoblePosition;
}
namespace Screen {
struct Pen;
};
}
namespace DFHack {namespace Lua {
@ -285,6 +288,7 @@ namespace DFHack {namespace Lua {
DFHACK_EXPORT void Push(lua_State *state, df::coord2d obj);
void Push(lua_State *state, const Units::NoblePosition &pos);
DFHACK_EXPORT void Push(lua_State *state, MaterialInfo &info);
DFHACK_EXPORT void Push(lua_State *state, const Screen::Pen &info);
template<class T> inline void Push(lua_State *state, T *ptr) {
PushDFObject(state, ptr);
}
@ -315,6 +319,8 @@ namespace DFHack {namespace Lua {
DFHACK_EXPORT int PushPosXYZ(lua_State *state, df::coord pos);
DFHACK_EXPORT int PushPosXY(lua_State *state, df::coord2d pos);
DFHACK_EXPORT void CheckPen(lua_State *L, Screen::Pen *pen, int index, bool allow_nil = false, bool allow_color = true);
DFHACK_EXPORT bool IsCoreContext(lua_State *state);
namespace Event {

@ -203,6 +203,12 @@ namespace DFHack
return ENUM_ATTR(tiletype_shape, passable_flow, tileShape(tiletype));
}
inline
bool FlowPassableDown(df::tiletype tiletype)
{
return ENUM_ATTR(tiletype_shape, passable_flow_down, tileShape(tiletype));
}
inline
bool isWalkable(df::tiletype tiletype)
{

@ -76,6 +76,8 @@ namespace DFHack
bool valid() const { return tile >= 0; }
bool empty() const { return ch == 0 && tile == 0; }
// NOTE: LuaApi.cpp assumes this struct is plain data and has empty destructor
Pen(char ch = 0, int8_t fg = 7, int8_t bg = 0, int tile = 0, bool color_tile = false)
: ch(ch), fg(fg&7), bg(bg), bold(!!(fg&8)),
tile(tile), tile_mode(color_tile ? CharColor : AsIs), tile_fg(0), tile_bg(0)

@ -6,7 +6,9 @@ local dscreen = dfhack.screen
USE_GRAPHICS = dscreen.inGraphicsMode()
CLEAR_PEN = {ch=32,fg=0,bg=0}
local to_pen = dfhack.pen.parse
CLEAR_PEN = to_pen{ch=32,fg=0,bg=0}
function simulateInput(screen,...)
local keys = {}
@ -116,16 +118,6 @@ function blink_visible(delay)
return math.floor(dfhack.getTickCount()/delay) % 2 == 0
end
function to_pen(default, pen, bg, bold)
if pen == nil then
return default or {}
elseif type(pen) ~= 'table' then
return {fg=pen,bg=bg,bold=bold}
else
return pen
end
end
function getKeyDisplay(code)
if type(code) == 'string' then
code = df.interface_key[code]
@ -215,7 +207,8 @@ Painter = defclass(Painter, ViewRect)
function Painter:init(args)
self.x = self.x1
self.y = self.y1
self.cur_pen = to_pen(nil, args.pen or COLOR_GREY)
self.cur_pen = to_pen(args.pen or COLOR_GREY)
self.cur_key_pen = to_pen(args.key_pen or COLOR_LIGHTGREEN)
end
function Painter.new(rect, pen)
@ -241,6 +234,7 @@ end
function Painter:viewport(x,y,w,h)
local vp = ViewRect.viewport(x,y,w,h)
vp.cur_pen = self.cur_pen
vp.cur_key_pen = self.cur_key_pen
return mkinstance(Painter, vp):seek(0,0)
end
@ -280,10 +274,12 @@ function Painter:pen(pen,...)
end
function Painter:color(fg,bold,bg)
self.cur_pen = copyall(self.cur_pen)
self.cur_pen.fg = fg
self.cur_pen.bold = bold
if bg then self.cur_pen.bg = bg end
self.cur_pen = to_pen(self.cur_pen, fg, bg, bold)
return self
end
function Painter:key_pen(pen,...)
self.cur_key_pen = to_pen(self.cur_key_pen, pen, ...)
return self
end
@ -339,10 +335,10 @@ function Painter:string(text,pen,...)
return self:advance(#text, nil)
end
function Painter:key(code,pen,bg,...)
function Painter:key(code,pen,...)
return self:string(
getKeyDisplay(code),
pen or COLOR_LIGHTGREEN, bg or self.cur_pen.bg, ...
to_pen(self.cur_key_pen, pen, ...)
)
end
@ -557,28 +553,28 @@ end
-- Plain grey-colored frame.
GREY_FRAME = {
frame_pen = { ch = ' ', fg = COLOR_BLACK, bg = COLOR_GREY },
title_pen = { fg = COLOR_BLACK, bg = COLOR_WHITE },
signature_pen = { fg = COLOR_BLACK, bg = COLOR_GREY },
frame_pen = to_pen{ ch = ' ', fg = COLOR_BLACK, bg = COLOR_GREY },
title_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_WHITE },
signature_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
}
-- The usual boundary used by the DF screens. Often has fancy pattern in tilesets.
BOUNDARY_FRAME = {
frame_pen = { ch = 0xDB, fg = COLOR_DARKGREY, bg = COLOR_BLACK },
title_pen = { fg = COLOR_BLACK, bg = COLOR_GREY },
signature_pen = { fg = COLOR_BLACK, bg = COLOR_DARKGREY },
frame_pen = to_pen{ ch = 0xDB, fg = COLOR_DARKGREY, bg = COLOR_BLACK },
title_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
signature_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_DARKGREY },
}
GREY_LINE_FRAME = {
frame_pen = { ch = 206, fg = COLOR_GREY, bg = COLOR_BLACK },
h_frame_pen = { ch = 205, fg = COLOR_GREY, bg = COLOR_BLACK },
v_frame_pen = { ch = 186, fg = COLOR_GREY, bg = COLOR_BLACK },
lt_frame_pen = { ch = 201, fg = COLOR_GREY, bg = COLOR_BLACK },
lb_frame_pen = { ch = 200, fg = COLOR_GREY, bg = COLOR_BLACK },
rt_frame_pen = { ch = 187, fg = COLOR_GREY, bg = COLOR_BLACK },
rb_frame_pen = { ch = 188, fg = COLOR_GREY, bg = COLOR_BLACK },
title_pen = { fg = COLOR_BLACK, bg = COLOR_GREY },
signature_pen = { fg = COLOR_DARKGREY, bg = COLOR_BLACK },
frame_pen = to_pen{ ch = 206, fg = COLOR_GREY, bg = COLOR_BLACK },
h_frame_pen = to_pen{ ch = 205, fg = COLOR_GREY, bg = COLOR_BLACK },
v_frame_pen = to_pen{ ch = 186, fg = COLOR_GREY, bg = COLOR_BLACK },
lt_frame_pen = to_pen{ ch = 201, fg = COLOR_GREY, bg = COLOR_BLACK },
lb_frame_pen = to_pen{ ch = 200, fg = COLOR_GREY, bg = COLOR_BLACK },
rt_frame_pen = to_pen{ ch = 187, fg = COLOR_GREY, bg = COLOR_BLACK },
rb_frame_pen = to_pen{ ch = 188, fg = COLOR_GREY, bg = COLOR_BLACK },
title_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
signature_pen = to_pen{ fg = COLOR_DARKGREY, bg = COLOR_BLACK },
}
function paint_frame(x1,y1,x2,y2,style,title)

@ -160,8 +160,8 @@ function ListBox:preinit(info)
end
function ListBox:init(info)
local spen = gui.to_pen(COLOR_CYAN, self.select_pen, nil, false)
local cpen = gui.to_pen(COLOR_LIGHTCYAN, self.cursor_pen or self.select_pen, nil, true)
local spen = dfhack.pen.parse(COLOR_CYAN, self.select_pen, nil, false)
local cpen = dfhack.pen.parse(COLOR_LIGHTCYAN, self.cursor_pen or self.select_pen, nil, true)
local list_widget = widgets.List
if self.with_filter then

@ -504,7 +504,7 @@ function List:onRenderBody(dc)
local icon = getval(obj.icon)
if icon then
dc:seek(0, y)
if type(icon) == 'table' then
if type(icon) ~= 'string' then
dc:char(nil,icon)
else
if current then
@ -573,7 +573,7 @@ FilteredList = defclass(FilteredList, Widget)
function FilteredList:init(info)
self.edit = EditField{
text_pen = info.cursor_pen,
text_pen = info.edit_pen or info.cursor_pen,
frame = { l = info.icon_width, t = 0 },
on_change = self:callback('onFilterChange'),
on_char = self:callback('onFilterChar'),
@ -583,6 +583,7 @@ function FilteredList:init(info)
text_pen = info.text_pen,
cursor_pen = info.cursor_pen,
inactive_pen = info.inactive_pen,
icon_pen = info.icon_pen,
row_height = info.row_height,
scroll_keys = info.scroll_keys,
icon_width = info.icon_width,

@ -457,6 +457,7 @@ function getBuildingCenter(building)
return xyz2pos(building.centerx, building.centery, building.z)
end
-- Split the string by the given delimiter
function split_string(self, delimiter)
local result = { }
local from = 1

@ -75,7 +75,7 @@ df::job *DFHack::Job::cloneJobStruct(df::job *job)
{
df::general_ref *ref = pnew->references[i];
if (virtual_cast<df::general_ref_unit_workerst>(ref))
if (virtual_cast<df::general_ref_unit>(ref))
vector_erase_at(pnew->references, i);
else
pnew->references[i] = ref->clone();

@ -190,6 +190,8 @@ bool MaterialInfo::find(const std::vector<std::string> &items)
}
else if (items.size() == 2)
{
if (items[1] == "NONE" && findBuiltin(items[0]))
return true;
if (findPlant(items[0], items[1]))
return true;
if (findCreature(items[0], items[1]))
@ -210,7 +212,7 @@ bool MaterialInfo::findBuiltin(const std::string &token)
}
df::world_raws &raws = world->raws;
for (int i = 1; i < NUM_BUILTIN; i++)
for (int i = 0; i < NUM_BUILTIN; i++)
{
auto obj = raws.mat_table.builtin[i];
if (obj && obj->id == token)
@ -312,7 +314,7 @@ std::string MaterialInfo::getToken()
else if (index == 1)
return "COAL:CHARCOAL";
}
return material->id + ":NONE";
return material->id;
case Inorganic:
return "INORGANIC:" + inorganic->id;
case Creature:
@ -423,6 +425,8 @@ bool MaterialInfo::matches(const df::dfhack_material_category &cat)
TEST(glass, IS_GLASS);
if (cat.bits.clay && linear_index(material->reaction_product.id, std::string("FIRED_MAT")) >= 0)
return true;
if (cat.bits.milk && linear_index(material->reaction_product.id, std::string("CHEESE_MAT")) >= 0)
return true;
return false;
}

@ -1 +1 @@
Subproject commit fcacacce7cf09cf70f011fea87b5be416da73457
Subproject commit 4b2124957e282683480eaf05922e63c353364ec1

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

@ -132,6 +132,9 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out)
* grace period during which the items can be instantly picked up again.
*/
// Completely block the use of stockpiles
#define NO_STOCKPILES
// Check if the item is assigned to any use controlled by the military tab
static bool is_assigned_item(df::item *item)
{
@ -143,24 +146,11 @@ static bool is_assigned_item(df::item *item)
if (idx < 0)
return false;
// Exclude weapons used by miners, wood cutters etc
switch (type) {
case item_type::WEAPON:
// the game code also checks this for ammo, funnily enough
// maybe it's not just for weapons?..
if (binsearch_index(ui->equipment.work_weapons, item->id) >= 0)
return false;
break;
default:
break;
}
return true;
}
// Check if this ammo item is assigned to this squad with one of the specified uses
static bool is_squad_ammo(df::item *item, df::squad *squad, bool train, bool combat)
static bool is_squad_ammo(df::item *item, df::squad *squad, bool combat, bool train)
{
for (size_t i = 0; i < squad->ammunition.size(); i++)
{
@ -186,8 +176,6 @@ static bool can_store_ammo_rec(df::item *item, df::building *holder, int squad_i
if (squads)
{
bool target = holder->getType() == building_type::ArcheryTarget;
for (size_t i = 0; i < squads->size(); i++)
{
auto use = (*squads)[i];
@ -198,8 +186,7 @@ static bool can_store_ammo_rec(df::item *item, df::building *holder, int squad_i
// Squad Equipment -> combat
bool combat = use->mode.bits.squad_eq;
// Archery target with Train -> training
bool train = target && use->mode.bits.train;
bool train = false;
if (combat || train)
{
@ -210,6 +197,41 @@ static bool can_store_ammo_rec(df::item *item, df::building *holder, int squad_i
}
}
}
// Ugh, archery targets don't actually have a squad use vector
else if (holder->getType() == building_type::ArcheryTarget)
{
auto &squads = df::global::world->squads.all;
for (size_t si = 0; si < squads.size(); si++)
{
auto squad = squads[si];
// For containers assigned to a squad, only consider that squad
if (squad_id >= 0 && squad->id != squad_id)
continue;
for (size_t j = 0; j < squad->rooms.size(); j++)
{
auto use = squad->rooms[j];
if (use->building_id != holder->id)
continue;
// Squad Equipment -> combat
bool combat = use->mode.bits.squad_eq;
// Archery target with Train -> training
bool train = use->mode.bits.train;
if (combat || train)
{
if (is_squad_ammo(item, squad, combat, train))
return true;
}
break;
}
}
}
for (size_t i = 0; i < holder->parents.size(); i++)
if (can_store_ammo_rec(item, holder->parents[i], squad_id))
@ -296,6 +318,16 @@ template<class Item> struct armory_hook : Item {
*/
DEFINE_VMETHOD_INTERPOSE(bool, isCollected, ())
{
#ifdef NO_STOCKPILES
/*
* Completely block any items assigned to a squad from being stored
* in stockpiles. The reason is that I still observe haulers running
* around with bins to pick them up for some reason. There could be
* some unaccounted race conditions involved.
*/
if (is_assigned_item(this))
return false;
#else
// Block stockpiling of items in the armory.
if (is_in_armory(this))
return false;
@ -322,6 +354,7 @@ template<class Item> struct armory_hook : Item {
return false;
}
}
#endif
// Call the original vmethod
return INTERPOSE_NEXT(isCollected)();

@ -301,15 +301,26 @@ function listWeakenedConstraints(outputs)
local mask = cons.mat_mask
if (cons.mat_type or -1) >= 0 then
cons.mat_mask = nil
local info = dfhack.matinfo.decode(cons)
if info then
for i,flag in ipairs(df.dfhack_material_category) do
if flag and flag ~= 'wood2' and info:matches{[flag]=true} then
mask = mask or {}
mask[flag] = true
end
end
end
end
register(cons)
if mask then
table.insert(generic, {
item_type = cons.item_type,
item_subtype = cons.item_subtype,
is_craft = cons.is_craft,
mat_mask = mask
})
for k,v in pairs(mask) do
table.insert(generic, {
item_type = cons.item_type,
item_subtype = cons.item_subtype,
is_craft = cons.is_craft,
mat_mask = { [k] = v }
})
end
end
table.insert(anymat, {
item_type = cons.item_type,

@ -298,6 +298,10 @@ bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2)
{
if (sort_skill != job_skill::NONE)
{
if (!d1->unit->status.current_soul)
return !descending;
if (!d2->unit->status.current_soul)
return descending;
df::unit_skill *s1 = binsearch_in_vector<df::unit_skill,df::job_skill>(d1->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill);
df::unit_skill *s2 = binsearch_in_vector<df::unit_skill,df::job_skill>(d2->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill);
int l1 = s1 ? s1->rating : 0;
@ -1030,7 +1034,9 @@ void viewscreen_unitlaborsst::render()
fg = 9;
if (columns[col_offset].skill != job_skill::NONE)
{
df::unit_skill *skill = binsearch_in_vector<df::unit_skill,df::job_skill>(unit->status.current_soul->skills, &df::unit_skill::id, columns[col_offset].skill);
df::unit_skill *skill = NULL;
if (unit->status.current_soul)
skill = binsearch_in_vector<df::unit_skill,df::job_skill>(unit->status.current_soul->skills, &df::unit_skill::id, columns[col_offset].skill);
if ((skill != NULL) && (skill->rating || skill->experience))
{
int level = skill->rating;
@ -1061,18 +1067,22 @@ void viewscreen_unitlaborsst::render()
if (cur != NULL)
{
df::unit *unit = cur->unit;
int x = 1;
Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->transname);
int x = 1, y = 3 + num_rows + 2;
Screen::Pen white_pen(' ', 15, 0);
Screen::paintString(white_pen, x, y, (cur->unit && cur->unit->sex) ? "\x0b" : "\x0c");
x += 2;
Screen::paintString(white_pen, x, y, cur->transname);
x += cur->transname.length();
if (cur->transname.length())
{
Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ", ");
Screen::paintString(white_pen, x, y, ", ");
x += 2;
}
Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->profession);
Screen::paintString(white_pen, x, y, cur->profession);
x += cur->profession.length();
Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ": ");
Screen::paintString(white_pen, x, y, ": ");
x += 2;
string str;
@ -1086,7 +1096,9 @@ void viewscreen_unitlaborsst::render()
}
else
{
df::unit_skill *skill = binsearch_in_vector<df::unit_skill,df::job_skill>(unit->status.current_soul->skills, &df::unit_skill::id, columns[sel_column].skill);
df::unit_skill *skill = NULL;
if (unit->status.current_soul)
skill = binsearch_in_vector<df::unit_skill,df::job_skill>(unit->status.current_soul->skills, &df::unit_skill::id, columns[sel_column].skill);
if (skill)
{
int level = skill->rating;

@ -181,7 +181,8 @@ module DFHack
@nume ||= const_get(:NUME)
end
def self.int(i)
def self.int(i, allow_bad_sym=false)
raise ArgumentError, "invalid enum member #{i} of #{self}" if i.kind_of?(::Symbol) and not allow_bad_sym and not nume.has_key?(i)
nume[i] || i
end
def self.sym(i)
@ -797,7 +798,6 @@ module DFHack
def isset(key)
raise unless @_memaddr
key = @_enum.int(key) if _enum
raise "unknown key #{key.inspect}" if key.kind_of?(::Symbol)
DFHack.memory_stlset_isset(@_memaddr, key)
end
alias is_set? isset
@ -805,14 +805,12 @@ module DFHack
def set(key)
raise unless @_memaddr
key = @_enum.int(key) if _enum
raise "unknown key #{key.inspect}" if key.kind_of?(::Symbol)
DFHack.memory_stlset_set(@_memaddr, key)
end
def delete(key)
raise unless @_memaddr
key = @_enum.int(key) if _enum
raise "unknown key #{key.inspect}" if key.kind_of?(::Symbol)
DFHack.memory_stlset_deletekey(@_memaddr, key)
end

@ -0,0 +1,704 @@
#include <modules/Screen.h>
#include <modules/Translation.h>
#include <modules/Units.h>
#include <MiscUtils.h>
#include <VTableInterpose.h>
//#include "df/viewscreen_petst.h"
#include "df/viewscreen_storesst.h"
#include "df/viewscreen_tradegoodsst.h"
#include "df/viewscreen_unitlistst.h"
#include "df/interface_key.h"
#include "df/interfacest.h"
using std::set;
using std::vector;
using std::string;
using namespace DFHack;
using namespace df::enums;
using df::global::gps;
using df::global::gview;
/*
Search Plugin
A plugin that adds a "Search" hotkey to some screens (Units, Trade and Stocks)
that allows filtering of the list items by a typed query.
Works by manipulating the vector(s) that the list based viewscreens use to store
their items. When a search is started the plugin saves the original vectors and
with each keystroke creates a new filtered vector off the saves for the screen
to use.
*/
void OutputString(int8_t color, int &x, int y, const std::string &text)
{
Screen::paintString(Screen::Pen(' ', color, 0), x, y, text);
x += text.length();
}
static bool is_live_screen(const df::viewscreen *screen)
{
for (df::viewscreen *cur = &gview->view; cur; cur = cur->child)
if (cur == screen)
return true;
return false;
}
//
// START: Base Search functionality
//
// Parent class that does most of the work
template <class S, class T, class V = void*>
class search_parent
{
public:
// Called each time you enter or leave a searchable screen. Resets everything.
void reset_all()
{
reset_search();
valid = false;
sort_list1 = NULL;
sort_list2 = NULL;
viewscreen = NULL;
select_key = 's';
track_secondary_values = false;
}
bool reset_on_change()
{
if (valid && is_live_screen(viewscreen))
return false;
reset_all();
return true;
}
// A new keystroke is received in a searchable screen
virtual bool process_input(set<df::interface_key> *input)
{
// If the page has two search options (Trade screen), only allow one to operate
// at a time
if (lock != NULL && lock != this)
return false;
// Allows custom preprocessing for each screen
if (!should_check_input(input))
return false;
bool key_processed = true;
if (entry_mode)
{
// Query typing mode
df::interface_key last_token = *input->rbegin();
if (last_token >= interface_key::STRING_A032 && last_token <= interface_key::STRING_A126)
{
// Standard character
search_string += last_token - ascii_to_enum_offset;
do_search();
}
else if (last_token == interface_key::STRING_A000)
{
// Backspace
if (search_string.length() > 0)
{
search_string.erase(search_string.length()-1);
do_search();
}
}
else if (input->count(interface_key::SELECT) || input->count(interface_key::LEAVESCREEN))
{
// ENTER or ESC: leave typing mode
end_entry_mode();
}
else if (input->count(interface_key::CURSOR_UP) || input->count(interface_key::CURSOR_DOWN)
|| input->count(interface_key::CURSOR_LEFT) || input->count(interface_key::CURSOR_RIGHT))
{
// Arrow key pressed. Leave entry mode and allow screen to process key
end_entry_mode();
key_processed = false;
}
}
// Not in query typing mode
else if (input->count(select_token))
{
// Hotkey pressed, enter typing mode
start_entry_mode();
}
else if (input->count((df::interface_key) (select_token + shift_offset)))
{
// Shift + Hotkey pressed, clear query
clear_search();
}
else
{
// Not a key for us, pass it on to the screen
key_processed = false;
}
return key_processed || entry_mode; // Only pass unrecognized keys down if not in typing mode
}
// Called if the search should be redone after the screen processes the keystroke.
// Used by the stocks screen where changing categories should redo the search on
// the new category.
virtual void do_post_update_check()
{
if (redo_search)
{
do_search();
redo_search = false;
}
}
static search_parent<S,T,V> *lock;
protected:
const S *viewscreen;
vector <T> saved_list1, reference_list;
vector <V> saved_list2;
vector <int> saved_indexes;
bool valid;
bool redo_search;
bool track_secondary_values;
string search_string;
search_parent() : ascii_to_enum_offset(interface_key::STRING_A048 - '0'), shift_offset('A' - 'a')
{
reset_all();
}
virtual void init(int *cursor_pos, vector <T> *sort_list1, vector <V> *sort_list2 = NULL, char select_key = 's')
{
this->cursor_pos = cursor_pos;
this->sort_list1 = sort_list1;
this->sort_list2 = sort_list2;
this->select_key = select_key;
select_token = (df::interface_key) (ascii_to_enum_offset + select_key);
track_secondary_values = false;
valid = true;
}
bool is_entry_mode()
{
return entry_mode;
}
void start_entry_mode()
{
entry_mode = true;
lock = this;
}
void end_entry_mode()
{
entry_mode = false;
lock = NULL;
}
void reset_search()
{
end_entry_mode();
search_string = "";
saved_list1.clear();
saved_list2.clear();
reference_list.clear();
saved_indexes.clear();
}
// If the second vector is editable (i.e. Trade screen vector used for marking). then it may
// have been edited while the list was filtered. We have to update the original unfiltered
// list with these values. Uses a stored reference vector to determine if the list has been
// reordered after filtering, in which case indexes must be remapped.
void update_secondary_values()
{
if (sort_list2 != NULL && track_secondary_values)
{
bool list_has_been_sorted = (sort_list1->size() == reference_list.size()
&& *sort_list1 != reference_list);
for (size_t i = 0; i < saved_indexes.size(); i++)
{
int adjusted_item_index = i;
if (list_has_been_sorted)
{
for (size_t j = 0; j < sort_list1->size(); j++)
{
if ((*sort_list1)[j] == reference_list[i])
{
adjusted_item_index = j;
break;
}
}
}
saved_list2[saved_indexes[i]] = (*sort_list2)[adjusted_item_index];
}
saved_indexes.clear();
}
}
// Store a copy of filtered list, used later to work out if filtered list has been sorted after filtering
void store_reference_values()
{
if (track_secondary_values)
reference_list = *sort_list1;
}
// Shortcut to clear the search immediately
void clear_search()
{
if (saved_list1.size() > 0)
{
*sort_list1 = saved_list1;
if (sort_list2 != NULL)
{
update_secondary_values();
*sort_list2 = saved_list2;
}
saved_list1.clear();
saved_list2.clear();
}
store_reference_values();
search_string = "";
}
// The actual sort
void do_search()
{
if (search_string.length() == 0)
{
clear_search();
return;
}
if (saved_list1.size() == 0)
{
// On first run, save the original list
saved_list1 = *sort_list1;
if (sort_list2 != NULL)
saved_list2 = *sort_list2;
}
else
update_secondary_values(); // Update original list with any modified values
// Clear viewscreen vectors
sort_list1->clear();
if (sort_list2 != NULL)
{
sort_list2->clear();
saved_indexes.clear();
}
string search_string_l = toLower(search_string);
for (size_t i = 0; i < saved_list1.size(); i++ )
{
T element = saved_list1[i];
string desc = toLower(get_element_description(element));
if (desc.find(search_string_l) != string::npos)
{
sort_list1->push_back(element);
if (sort_list2 != NULL)
{
sort_list2->push_back(saved_list2[i]);
if (track_secondary_values)
saved_indexes.push_back(i); // Used to map filtered indexes back to original, if needed
}
}
}
store_reference_values(); //Keep a copy, in case user sorts new list
*cursor_pos = 0;
}
virtual bool should_check_input(set<df::interface_key> *input)
{
return true;
}
// Display hotkey message
void print_search_option(int x, int y = -1) const
{
if (y == -1)
y = gps->dimy - 2;
OutputString((entry_mode) ? 4 : 12, x, y, string(1, select_key));
OutputString((entry_mode) ? 10 : 15, x, y, ": Search");
if (search_string.length() > 0 || entry_mode)
OutputString(15, x, y, ": " + search_string);
if (entry_mode)
OutputString(10, x, y, "_");
}
virtual string get_element_description(T element) const = 0;
virtual void render () const = 0;
private:
vector <T> *sort_list1;
vector <V> *sort_list2;
int *cursor_pos;
char select_key;
bool entry_mode;
df::interface_key select_token;
const int ascii_to_enum_offset;
const int shift_offset;
};
template <class S, class T, class V> search_parent<S,T,V> *search_parent<S,T,V> ::lock = NULL;
// Parent struct for the hooks
template <class T, class V, typename D = void>
struct search_hook : T
{
typedef T interpose_base;
static V module;
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
if (!module.init(this))
{
INTERPOSE_NEXT(feed)(input);
return;
}
if (!module.process_input(input))
{
INTERPOSE_NEXT(feed)(input);
module.do_post_update_check();
}
}
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
bool ok = module.init(this);
INTERPOSE_NEXT(render)();
if (ok)
module.render();
}
};
template <class T, class V, typename D> V search_hook<T, V, D> ::module;
//
// END: Base Search functionality
//
//
// START: Stocks screen search
//
class stocks_search : public search_parent<df::viewscreen_storesst, df::item*>
{
public:
virtual void render() const
{
if (!viewscreen->in_group_mode)
print_search_option(2);
else
{
int x = 2;
OutputString(15, x, gps->dimy - 2, "Tab to enable Search");
}
}
virtual void do_post_update_check()
{
if (viewscreen->in_group_mode)
{
// Disable search if item lists are grouped
clear_search();
reset_search();
}
else
search_parent::do_post_update_check();
}
bool init(df::viewscreen_storesst *screen)
{
if (screen != viewscreen && !reset_on_change())
return false;
if (!valid)
{
viewscreen = screen;
search_parent::init(&screen->item_cursor, &screen->items);
}
return true;
}
private:
virtual string get_element_description(df::item *element) const
{
return Items::getDescription(element, 0, true);
}
virtual bool should_check_input(set<df::interface_key> *input)
{
if (viewscreen->in_group_mode)
return false;
if ((input->count(interface_key::CURSOR_UP) || input->count(interface_key::CURSOR_DOWN)) && !viewscreen->in_right_list)
{
// Redo search if category changes
saved_list1.clear();
end_entry_mode();
if (search_string.length() > 0)
redo_search = true;
return false;
}
return true;
}
};
typedef search_hook<df::viewscreen_storesst, stocks_search> stocks_search_hook;
template<> IMPLEMENT_VMETHOD_INTERPOSE(stocks_search_hook, feed);
template<> IMPLEMENT_VMETHOD_INTERPOSE(stocks_search_hook, render);
//
// END: Stocks screen search
//
//
// START: Unit screen search
//
class unitlist_search : public search_parent<df::viewscreen_unitlistst, df::unit*, df::job*>
{
public:
virtual void render() const
{
print_search_option(28);
}
bool init(df::viewscreen_unitlistst *screen)
{
if (screen != viewscreen && !reset_on_change())
return false;
if (!valid)
{
viewscreen = screen;
search_parent::init(&screen->cursor_pos[viewscreen->page], &screen->units[viewscreen->page], &screen->jobs[viewscreen->page]);
}
return true;
}
private:
virtual string get_element_description(df::unit *element) const
{
string desc = Translation::TranslateName(Units::getVisibleName(element), false);
desc += ", " + Units::getProfessionName(element); // Check animal type too
return desc;
}
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 (!is_entry_mode())
{
// Changing screens, reset search
clear_search();
reset_all();
}
else
input->clear(); // Ignore cursor keys when typing
return false;
}
return true;
}
};
typedef search_hook<df::viewscreen_unitlistst, unitlist_search> unitlist_search_hook;
template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(unitlist_search_hook, feed, 100);
template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(unitlist_search_hook, render, 100);
//
// END: Unit screen search
//
//
// TODO: Animals screen search
//
//
// END: Animals screen search
//
//
// START: Trade screen search
//
class trade_search_base : public search_parent<df::viewscreen_tradegoodsst, df::item*, char>
{
private:
virtual string get_element_description(df::item *element) const
{
return Items::getDescription(element, 0, true);
}
virtual bool should_check_input(set<df::interface_key> *input)
{
if (is_entry_mode())
return true;
if (input->count(interface_key::TRADE_TRADE) ||
input->count(interface_key::TRADE_OFFER) ||
input->count(interface_key::TRADE_SEIZE))
{
// Block the keys if were searching
if (!search_string.empty())
input->clear();
// Trying to trade, reset search
clear_search();
reset_all();
return false;
}
return true;
}
};
class trade_search_merc : public trade_search_base
{
public:
virtual void render() const
{
print_search_option(2, 26);
}
bool init(df::viewscreen_tradegoodsst *screen)
{
if (screen != viewscreen && !reset_on_change())
return false;
if (!valid)
{
viewscreen = screen;
search_parent::init(&screen->trader_cursor, &screen->trader_items, &screen->trader_selected, 'q');
track_secondary_values = true;
}
return true;
}
};
typedef search_hook<df::viewscreen_tradegoodsst, trade_search_merc, int> trade_search_merc_hook;
template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_merc_hook, feed);
template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_merc_hook, render);
class trade_search_fort : public trade_search_base
{
public:
virtual void render() const
{
print_search_option(42, 26);
}
bool init(df::viewscreen_tradegoodsst *screen)
{
if (screen != viewscreen && !reset_on_change())
return false;
if (!valid)
{
viewscreen = screen;
search_parent::init(&screen->broker_cursor, &screen->broker_items, &screen->broker_selected, 'w');
track_secondary_values = true;
}
return true;
}
};
typedef search_hook<df::viewscreen_tradegoodsst, trade_search_fort, char> trade_search_fort_hook;
template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_fort_hook, feed);
template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_fort_hook, render);
//
// END: Trade screen search
//
DFHACK_PLUGIN("search");
DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCommand> &commands)
{
if (!gps || !gview ||
!INTERPOSE_HOOK(unitlist_search_hook, feed).apply() ||
!INTERPOSE_HOOK(unitlist_search_hook, render).apply() ||
!INTERPOSE_HOOK(trade_search_merc_hook, feed).apply() ||
!INTERPOSE_HOOK(trade_search_merc_hook, render).apply() ||
!INTERPOSE_HOOK(trade_search_fort_hook, feed).apply() ||
!INTERPOSE_HOOK(trade_search_fort_hook, render).apply() ||
!INTERPOSE_HOOK(stocks_search_hook, feed).apply() ||
!INTERPOSE_HOOK(stocks_search_hook, render).apply())
out.printerr("Could not insert Search hooks!\n");
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
INTERPOSE_HOOK(unitlist_search_hook, feed).remove();
INTERPOSE_HOOK(unitlist_search_hook, render).remove();
INTERPOSE_HOOK(trade_search_merc_hook, feed).remove();
INTERPOSE_HOOK(trade_search_merc_hook, render).remove();
INTERPOSE_HOOK(trade_search_fort_hook, feed).remove();
INTERPOSE_HOOK(trade_search_fort_hook, render).remove();
INTERPOSE_HOOK(stocks_search_hook, feed).remove();
INTERPOSE_HOOK(stocks_search_hook, render).remove();
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_change_event event )
{
switch (event) {
case SC_VIEWSCREEN_CHANGED:
unitlist_search_hook::module.reset_on_change();
trade_search_merc_hook::module.reset_on_change();
trade_search_fort_hook::module.reset_on_change();
stocks_search_hook::module.reset_on_change();
break;
default:
break;
}
return CR_OK;
}

@ -320,7 +320,7 @@ struct workshop_hook : df::building_workshopst {
for (int y = y1; y <= y2; y++)
{
auto ptile = Maps::getTileType(x,y,z);
if (!ptile || !LowPassable(*ptile))
if (!ptile || !FlowPassableDown(*ptile))
continue;
auto pltile = Maps::getTileType(x,y,z-1);
@ -891,7 +891,7 @@ IMPLEMENT_VMETHOD_INTERPOSE(dwarfmode_hook, feed);
* Scan raws for matching workshop buildings.
*/
static bool find_engines()
static bool find_engines(color_ostream &out)
{
engines.clear();
@ -943,6 +943,8 @@ static bool find_engines()
if (!ws.gear_tiles.empty())
engines.push_back(ws);
else
out.printerr("%s has no gear tiles - ignoring.\n", wslist[i]->code.c_str());
}
return !engines.empty();
@ -973,7 +975,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan
{
switch (event) {
case SC_WORLD_LOADED:
if (find_engines())
if (find_engines(out))
{
out.print("Detected steam engine workshops - enabling plugin.\n");
enable_hooks(true);

@ -1 +1 @@
Subproject commit 75df766263b23182820a1e07b330e64f87d5c9b7
Subproject commit cb97cf308c6e09638c0de94894473c9bd0f561fd

@ -377,7 +377,9 @@ static bool isSupportedJob(df::job *job)
Job::getHolder(job) &&
(!job->job_items.empty() ||
job->job_type == job_type::CollectClay ||
job->job_type == job_type::CollectSand);
job->job_type == job_type::CollectSand ||
job->job_type == job_type::MilkCreature ||
job->job_type == job_type::ShearCreature);
}
static bool isOptionEnabled(unsigned flag)
@ -1004,6 +1006,12 @@ static bool isRouteVehicle(df::item *item)
return vehicle && vehicle->route_id >= 0;
}
static bool isAssignedSquad(df::item *item)
{
auto &vec = ui->equipment.items_assigned[item->getType()];
return binsearch_index(vec, &df::item::id, item->id) >= 0;
}
static void map_job_items(color_ostream &out)
{
for (size_t i = 0; i < constraints.size(); i++)
@ -1117,8 +1125,10 @@ static void map_job_items(color_ostream &out)
item->isAssignedToStockpile() ||
isRouteVehicle(item) ||
itemInRealJob(item) ||
itemBusy(item))
itemBusy(item) ||
isAssignedSquad(item))
{
is_invalid = true;
cv->item_inuse++;
}
else

@ -306,7 +306,7 @@ function JobConstraints:onNewConstraint()
end
dlg.showListPrompt(
'Job Outputs',
'New limit',
'Select one of the possible outputs:',
COLOR_WHITE,
choices,

@ -0,0 +1,141 @@
-- Shows populations of animals in the region, and allows tweaking them.
local utils = require 'utils'
local function sort_keys(tab)
local kt = {}
for k,v in pairs(tab) do table.insert(kt,k) end
table.sort(kt)
return ipairs(kt)
end
local is_plant_map = {
Animal = false, Vermin = false, VerminInnumerable = false,
ColonyInsect = false, Tree = true, Grass = true, Bush = true
}
function enum_populations()
local stat_table = {
plants = {},
creatures = {},
any = {}
}
for i,v in ipairs(df.global.world.populations) do
local typeid = df.world_population_type[v.type]
local is_plant = is_plant_map[typeid]
local id, obj, otable, idtoken
if is_plant then
id = v.plant
obj = df.plant_raw.find(id)
otable = stat_table.plants
idtoken = obj.id
else
id = v.race
obj = df.creature_raw.find(id)
otable = stat_table.creatures
idtoken = obj.creature_id
end
local entry = otable[idtoken]
if not entry then
entry = {
obj = obj, token = idtoken, id = id, records = {},
count = 0, known_count = 0,
known = false, infinite = false
}
otable[idtoken] = entry
stat_table.any[idtoken] = entry
end
table.insert(entry.records, v)
entry.known = entry.known or v.known
if v.quantity < 10000001 then
entry.count = entry.count + v.quantity
if v.known then
entry.known_count = entry.known_count + v.quantity
end
else
entry.infinite = true
end
end
return stat_table
end
function list_poptable(entries, all, pattern)
for _,k in sort_keys(entries) do
local entry = entries[k]
if (all or entry.known) and (not pattern or string.match(k,pattern)) then
local count = entry.known_count
if all then
count = entry.count
end
if entry.infinite then
count = 'innumerable'
end
print(string.format('%-40s %s', entry.token, count))
end
end
end
function list_populations(stat_table, all, pattern)
print('Plants:')
list_poptable(stat_table.plants, true, pattern)
print('\nCreatures and vermin:')
list_poptable(stat_table.creatures, all, pattern)
end
function boost_population(entry, factor, boost_count)
for _,v in ipairs(entry.records) do
if v.quantity < 10000001 then
boost_count = boost_count + 1
v.quantity = math.floor(v.quantity * factor)
end
end
return boost_count
end
local args = {...}
local pops = enum_populations()
if args[1] == 'list' or args[1] == 'list-all' then
list_populations(pops, args[1] == 'list-all', args[2])
elseif args[1] == 'boost' or args[1] == 'boost-all' then
local factor = tonumber(args[3])
if not factor or factor < 0 then
qerror('Invalid boost factor.')
end
local count = 0
if args[1] == 'boost' then
local entry = pops.any[args[2]] or qerror('Unknown population token.')
count = boost_population(entry, factor, count)
else
for k,entry in pairs(pops.any) do
if string.match(k, args[2]) then
count = boost_population(entry, factor, count)
end
end
end
print('Updated '..count..' populations.')
else
print([[
Usage:
region-pops list [pattern]
Lists encountered populations of the region, possibly restricted by pattern.
region-pops list-all [pattern]
Lists all populations of the region.
region-pops boost <TOKEN> <factor>
Multiply all populations of TOKEN by factor.
If the factor is greater than one, increases the
population, otherwise decreases it.
region-pops boost-all <pattern> <factor>
Same as above, but match using a pattern acceptable to list.
]])
end