Conflicts:
	NEWS
develop
Warmist 2012-12-09 14:25:31 +02:00
commit 6c4f163a5e
47 changed files with 1364 additions and 367 deletions

@ -1749,7 +1749,10 @@ options; if multiple interpretations exist, the table will contain multiple keys
<dd><p class="first last">Maps to an integer in range 0-255. Duplicates a separate &quot;STRING_A???&quot; code for convenience.</p> <dd><p class="first last">Maps to an integer in range 0-255. Duplicates a separate &quot;STRING_A???&quot; code for convenience.</p>
</dd> </dd>
<dt><tt class="docutils literal">_MOUSE_L, _MOUSE_R</tt></dt> <dt><tt class="docutils literal">_MOUSE_L, _MOUSE_R</tt></dt>
<dd><p class="first last">If the left or right mouse button is pressed.</p> <dd><p class="first last">If the left or right mouse button is being pressed.</p>
</dd>
<dt><tt class="docutils literal">_MOUSE_L_DOWN, _MOUSE_R_DOWN</tt></dt>
<dd><p class="first last">If the left or right mouse button was just pressed.</p>
</dd> </dd>
</dl> </dl>
<p>If this method is omitted, the screen is dismissed on receival of the <tt class="docutils literal">LEAVESCREEN</tt> key.</p> <p>If this method is omitted, the screen is dismissed on receival of the <tt class="docutils literal">LEAVESCREEN</tt> key.</p>
@ -2787,6 +2790,14 @@ before rendering the token.</p>
<li><p class="first"><tt class="docutils literal">token.tile = pen</tt></p> <li><p class="first"><tt class="docutils literal">token.tile = pen</tt></p>
<p>Specifies a pen to paint as one tile before the main part of the token.</p> <p>Specifies a pen to paint as one tile before the main part of the token.</p>
</li> </li>
<li><p class="first"><tt class="docutils literal">token.width = ...</tt></p>
<p>If specified either as a value or a callback, the text field is padded
or truncated to the specified number.</p>
</li>
<li><p class="first"><tt class="docutils literal">token.pad_char = <span class="pre">'?'</span></tt></p>
<p>If specified together with <tt class="docutils literal">width</tt>, the padding area is filled with
this character instead of just being skipped over.</p>
</li>
<li><p class="first"><tt class="docutils literal">token.key = <span class="pre">'...'</span></tt></p> <li><p class="first"><tt class="docutils literal">token.key = <span class="pre">'...'</span></tt></p>
<p>Specifies the keycode associated with the token. The string description <p>Specifies the keycode associated with the token. The string description
of the key binding is added to the text content of the token.</p> of the key binding is added to the text content of the token.</p>
@ -2848,11 +2859,16 @@ this may be extended with mouse click support.</p>
</tr> </tr>
<tr class="field"><th class="field-name">icon_pen:</th><td class="field-body">Default pen for icons.</td> <tr class="field"><th class="field-name">icon_pen:</th><td class="field-body">Default pen for icons.</td>
</tr> </tr>
<tr class="field"><th class="field-name">on_select:</th><td class="field-body">Selection change callback; called as <tt class="docutils literal">on_select(index,choice)</tt>.</td> <tr class="field"><th class="field-name">on_select:</th><td class="field-body">Selection change callback; called as <tt class="docutils literal">on_select(index,choice)</tt>.
This is also called with <em>nil</em> arguments if <tt class="docutils literal">setChoices</tt> is called
with an empty list.</td>
</tr> </tr>
<tr class="field"><th class="field-name">on_submit:</th><td class="field-body">Enter key callback; if specified, the list reacts to the key <tr class="field"><th class="field-name">on_submit:</th><td class="field-body">Enter key callback; if specified, the list reacts to the key
and calls it as <tt class="docutils literal">on_submit(index,choice)</tt>.</td> and calls it as <tt class="docutils literal">on_submit(index,choice)</tt>.</td>
</tr> </tr>
<tr class="field"><th class="field-name">on_submit2:</th><td class="field-body">Shift-Enter key callback; if specified, the list reacts to the key
and calls it as <tt class="docutils literal">on_submit2(index,choice)</tt>.</td>
</tr>
<tr class="field"><th class="field-name">row_height:</th><td class="field-body">Height of every row in text lines.</td> <tr class="field"><th class="field-name">row_height:</th><td class="field-body">Height of every row in text lines.</td>
</tr> </tr>
<tr class="field"><th class="field-name">icon_width:</th><td class="field-body">If not <em>nil</em>, the specified number of character columns <tr class="field"><th class="field-name">icon_width:</th><td class="field-body">If not <em>nil</em>, the specified number of character columns
@ -2908,6 +2924,9 @@ with the following fields:</p>
<li><p class="first"><tt class="docutils literal">list:submit()</tt></p> <li><p class="first"><tt class="docutils literal">list:submit()</tt></p>
<p>Call the <tt class="docutils literal">on_submit</tt> callback, as if the Enter key was handled.</p> <p>Call the <tt class="docutils literal">on_submit</tt> callback, as if the Enter key was handled.</p>
</li> </li>
<li><p class="first"><tt class="docutils literal">list:submit2()</tt></p>
<p>Call the <tt class="docutils literal">on_submit2</tt> callback, as if the Shift-Enter key was handled.</p>
</li>
</ul> </ul>
</div> </div>
<div class="section" id="filteredlist-class"> <div class="section" id="filteredlist-class">
@ -2922,6 +2941,8 @@ supports:</p>
<tbody valign="top"> <tbody valign="top">
<tr class="field"><th class="field-name">edit_pen:</th><td class="field-body">If specified, used instead of <tt class="docutils literal">cursor_pen</tt> for the edit field.</td> <tr class="field"><th class="field-name">edit_pen:</th><td class="field-body">If specified, used instead of <tt class="docutils literal">cursor_pen</tt> for the edit field.</td>
</tr> </tr>
<tr class="field"><th class="field-name">edit_below:</th><td class="field-body">If true, the edit field is placed below the list instead of above.</td>
</tr>
<tr class="field"><th class="field-name" colspan="2">not_found_label:</th></tr> <tr class="field"><th class="field-name" colspan="2">not_found_label:</th></tr>
<tr class="field"><td>&nbsp;</td><td class="field-body">Specifies the text of the label shown when no items match the filter.</td> <tr class="field"><td>&nbsp;</td><td class="field-body">Specifies the text of the label shown when no items match the filter.</td>
</tr> </tr>

@ -1610,7 +1610,10 @@ Supported callbacks and fields are:
Maps to an integer in range 0-255. Duplicates a separate "STRING_A???" code for convenience. Maps to an integer in range 0-255. Duplicates a separate "STRING_A???" code for convenience.
``_MOUSE_L, _MOUSE_R`` ``_MOUSE_L, _MOUSE_R``
If the left or right mouse button is pressed. If the left or right mouse button is being pressed.
``_MOUSE_L_DOWN, _MOUSE_R_DOWN``
If the left or right mouse button was just pressed.
If this method is omitted, the screen is dismissed on receival of the ``LEAVESCREEN`` key. If this method is omitted, the screen is dismissed on receival of the ``LEAVESCREEN`` key.
@ -2710,6 +2713,16 @@ containing newlines, or a table with the following possible fields:
Specifies a pen to paint as one tile before the main part of the token. Specifies a pen to paint as one tile before the main part of the token.
* ``token.width = ...``
If specified either as a value or a callback, the text field is padded
or truncated to the specified number.
* ``token.pad_char = '?'``
If specified together with ``width``, the padding area is filled with
this character instead of just being skipped over.
* ``token.key = '...'`` * ``token.key = '...'``
Specifies the keycode associated with the token. The string description Specifies the keycode associated with the token. The string description
@ -2775,8 +2788,12 @@ It has the following attributes:
:inactive_pen: If specified, used for the cursor when the widget is not active. :inactive_pen: If specified, used for the cursor when the widget is not active.
:icon_pen: Default pen for icons. :icon_pen: Default pen for icons.
:on_select: Selection change callback; called as ``on_select(index,choice)``. :on_select: Selection change callback; called as ``on_select(index,choice)``.
This is also called with *nil* arguments if ``setChoices`` is called
with an empty list.
:on_submit: Enter key callback; if specified, the list reacts to the key :on_submit: Enter key callback; if specified, the list reacts to the key
and calls it as ``on_submit(index,choice)``. and calls it as ``on_submit(index,choice)``.
:on_submit2: Shift-Enter key callback; if specified, the list reacts to the key
and calls it as ``on_submit2(index,choice)``.
:row_height: Height of every row in text lines. :row_height: Height of every row in text lines.
:icon_width: If not *nil*, the specified number of character columns :icon_width: If not *nil*, the specified number of character columns
are reserved to the left of the list item for the icons. are reserved to the left of the list item for the icons.
@ -2826,6 +2843,10 @@ The list supports the following methods:
Call the ``on_submit`` callback, as if the Enter key was handled. Call the ``on_submit`` callback, as if the Enter key was handled.
* ``list:submit2()``
Call the ``on_submit2`` callback, as if the Shift-Enter key was handled.
FilteredList class FilteredList class
------------------ ------------------
@ -2836,6 +2857,7 @@ In addition to passing through all attributes supported by List, it
supports: supports:
:edit_pen: If specified, used instead of ``cursor_pen`` for the edit field. :edit_pen: If specified, used instead of ``cursor_pen`` for the edit field.
:edit_below: If true, the edit field is placed below the list instead of above.
:not_found_label: Specifies the text of the label shown when no items match the filter. :not_found_label: Specifies the text of the label shown when no items match the filter.
The list choices may include the following attributes: The list choices may include the following attributes:
@ -2932,6 +2954,36 @@ sort
Does not export any native functions as of now. Instead, it Does not export any native functions as of now. Instead, it
calls lua code to perform the actual ordering of list items. calls lua code to perform the actual ordering of list items.
Eventful
========
This plugin exports some events to lua thus allowing to run lua functions
on DF world events.
List of events
--------------
1. onReactionComplete(reaction,unit,input_items,input_reagents,output_items,call_native) - auto activates if detects reactions starting with ``LUA_HOOK_``. Is called when reaction finishes.
2. onItemContaminateWound(item,unit,wound,number1,number2) - Is called when item tries to contaminate wound (e.g. stuck in)
Examples
--------
Spawn dragon breath on each item attempt to contaminate wound:
::
b=require "plugins.eventful"
b.onItemContaminateWound.one=function(item,unit,un_wound,x,y)
local flw=dfhack.maps.spawnFlow(unit.pos,6,0,0,50000)
end
Reaction complete example"
::
b.onReactionComplete.one=function(reaction,unit,in_items,in_reag,out_items,call_native)
local pos=copyall(unit.pos)
dfhack.timeout(100,"ticks",function() dfhack.maps.spawnFlow(pos,6,0,0,50000) end) -- spawn dragonbreath after 100 ticks
call_native.value=false --do not call real item creation code
end
======= =======
Scripts Scripts

23
NEWS

@ -12,6 +12,7 @@ DFHack future
- removebadthoughts: add --dry-run option - removebadthoughts: add --dry-run option
- superdwarf: work in adventure mode too - superdwarf: work in adventure mode too
- tweak stable-cursor: carries cursor location from/to Build menu. - tweak stable-cursor: carries cursor location from/to Build menu.
- deathcause: allow selection from the unitlist screen
New tweaks: New tweaks:
- tweak military-training: speed up melee squad training up to 10x (normally 3-5x). - tweak military-training: speed up melee squad training up to 10x (normally 3-5x).
New scripts: New scripts:
@ -22,34 +23,36 @@ DFHack future
- embark: lets you embark anywhere. - embark: lets you embark anywhere.
- lever: list and pull fort levers from the dfhack console. - lever: list and pull fort levers from the dfhack console.
- stripcaged: mark items inside cages for dumping, eg caged goblin weapons. - stripcaged: mark items inside cages for dumping, eg caged goblin weapons.
- soundsense-season: writes the correct season to gamelog.txt on world load.
New GUI scripts: New GUI scripts:
- gui/guide-path: displays the cached path for minecart Guide orders. - gui/guide-path: displays the cached path for minecart Guide orders.
- gui/workshop-job: displays inputs of a workshop job and allows tweaking them. - gui/workshop-job: displays inputs of a workshop job and allows tweaking them.
- gui/workflow: a front-end for the workflow plugin. - gui/workflow: a front-end for the workflow plugin (part inspired by falconne).
- gui/assign-rack: works together with a binary patch to fix weapon racks. - gui/assign-rack: works together with a binary patch to fix weapon racks.
- gui/gm-editor: an universal editor for lots of dfhack things. - gui/gm-editor: an universal editor for lots of dfhack things.
- gui/companion-order: a adventure mode command interface for your companions. - gui/companion-order: a adventure mode command interface for your companions.
- gui/advfort: a way to do jobs with your adventurer (e.g. build fort). - gui/advfort: a way to do jobs with your adventurer (e.g. build fort).
New binary patches: New binary patches (for use with binpatch):
- armorstand-capacity - armorstand-capacity: doubles the capacity of armor stands.
- custom-reagent-size - custom-reagent-size: lets custom reactions use small amounts of inputs.
- deconstruct-heapfall - deconstruct-heapfall: stops some items still falling on head when deconstructing.
- deconstruct-teleport - deconstruct-teleport: stops items from 16x16 block teleporting when deconstructing.
- hospital-overstocking - hospital-overstocking: stops hospital overstocking with supplies.
- training-ammo - training-ammo: lets dwarves with quiver full of combat-only ammo train.
- weaponrack-unassign - weaponrack-unassign: fixes bug that negates work done by gui/assign-rack.
Workflow plugin: Workflow plugin:
- properly considers minecarts assigned to routes busy. - properly considers minecarts assigned to routes busy.
- code for deducing job outputs rewritten in lua for flexibility. - code for deducing job outputs rewritten in lua for flexibility.
- logic fix: collecting webs produces silk, and ungathered webs are not thread. - logic fix: collecting webs produces silk, and ungathered webs are not thread.
- items assigned to squads are considered busy, even if not in inventory. - 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. - shearing and milking jobs are supported, but only with generic MILK or YARN outputs.
- workflow announces when the stock level gets very low once a season.
New Fix Armory plugin: New Fix Armory plugin:
Together with a couple of binary patches and the gui/assign-rack script, Together with a couple of binary patches and the gui/assign-rack script,
this plugin makes weapon racks, armor stands, chests and cabinets in this plugin makes weapon racks, armor stands, chests and cabinets in
properly designated barracks be used again for storage of squad equipment. properly designated barracks be used again for storage of squad equipment.
New Search plugin by falconne: New Search plugin by falconne:
Adds an incremental search function to the Stocks, Trading and Unit List screens. Adds an incremental search function to the Stocks, Trading, Stockpile and Unit List screens.
New AutoMaterial plugin by falconne: New AutoMaterial plugin by falconne:
Makes building constructions (walls, floors, fortifications, etc) a little bit easier by Makes building constructions (walls, floors, fortifications, etc) a little bit easier by
saving you from having to trawl through long lists of materials each time you place one. saving you from having to trawl through long lists of materials each time you place one.

@ -2873,7 +2873,7 @@ directly to the main dwarf mode screen.</p>
</div> </div>
<div class="section" id="search"> <div class="section" id="search">
<h2><a class="toc-backref" href="#id136">Search</a></h2> <h2><a class="toc-backref" href="#id136">Search</a></h2>
<p>The search plugin adds search to the Stocks, Trading and Unit List screens.</p> <p>The search plugin adds search to the Stocks, Trading, Stockpile and Unit List screens.</p>
<img alt="images/search.png" src="images/search.png" /> <img alt="images/search.png" src="images/search.png" />
<p>Searching works the same way as the search option in &quot;Move to Depot&quot; does. <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'). You will see the Search option displayed on screen with a hotkey (usually 's').
@ -2890,6 +2890,13 @@ filter).</p>
are actually visible in the list; the same effect applies to the Trade 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' 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> key while search is active clears the search instead of executing the trade.</p>
<p>In the stockpile screen the option only appears if the cursor is in the
rightmost list:</p>
<img alt="images/search-stockpile.png" src="images/search-stockpile.png" />
<p>Note that the 'Permit XXX'/'Forbid XXX' keys conveniently operate only
on items actually shown in the rightmost list, so it is possible to select
only fat or tallow by forbidding fats, then searching for fat/tallow, and
using Permit Fats again while the list is filtered.</p>
</div> </div>
<div class="section" id="automaterial"> <div class="section" id="automaterial">
<h2><a class="toc-backref" href="#id137">AutoMaterial</a></h2> <h2><a class="toc-backref" href="#id137">AutoMaterial</a></h2>
@ -3034,22 +3041,39 @@ current job, and their current status.</p>
current count is below the lower bound of the range, the job is resumed; if it current count is below the lower bound of the range, the job is resumed; if it
is above or equal to the top bound, it will be suspended. Within the range, the is above or equal to the top bound, it will be suspended. Within the range, the
specific constraint has no effect on the job; others may still affect it.</p> specific constraint has no effect on the job; others may still affect it.</p>
<p>Pressing 'c' switches the current constraint between counting stacks or items. <p>Pressing 'I' switches the current constraint between counting stacks or items.
Pressing 'm' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
bounds by 1, 5, or 25 depending on the direction and the 'c' setting (counting bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting
items and expanding the range each gives a 5x bonus).</p> items and expanding the range each gives a 2x bonus).</p>
<p>Pressing 'n' produces a list of possible outputs of this job as guessed by <p>Pressing 'A' produces a list of possible outputs of this job as guessed by
workflow, and lets you create a new constraint by choosing one as template. If you workflow, and lets you create a new constraint by choosing one as template. If you
don't see the choice you want in the list, it likely means you have to adjust don't see the choice you want in the list, it likely means you have to adjust
the job material first using <tt class="docutils literal">job <span class="pre">item-material</span></tt> or <tt class="docutils literal"><span class="pre">gui/workshop-job</span></tt>, the job material first using <tt class="docutils literal">job <span class="pre">item-material</span></tt> or <tt class="docutils literal"><span class="pre">gui/workshop-job</span></tt>,
as described in <tt class="docutils literal">workflow</tt> documentation above. In this manner, this feature as described in <tt class="docutils literal">workflow</tt> documentation above. In this manner, this feature
can be used for troubleshooting jobs that don't match the right constraints.</p> can be used for troubleshooting jobs that don't match the right constraints.</p>
<img alt="images/workflow-new1.png" src="images/workflow-new1.png" /> <img alt="images/workflow-new1.png" src="images/workflow-new1.png" />
<p>After selecting one of the presented outputs, the interface proceeds to the <p>If you select one of the outputs with Enter, the matching constraint is simply
added to the list. If you use Shift-Enter, the interface proceeds to the
next dialog, which allows you to edit the suggested constraint parameters to next dialog, which allows you to edit the suggested constraint parameters to
suit your need, and set the item count range.</p> suit your need, and set the item count range.</p>
<img alt="images/workflow-new2.png" src="images/workflow-new2.png" /> <img alt="images/workflow-new2.png" src="images/workflow-new2.png" />
<p>If you don't need advanced settings, you can just press 'y' to confirm creation.</p> <p>Pressing 'S' (or, with the example config, Alt-W in the 'z' stocks screen)
opens the overall status screen, which was copied from the C++ implementation
by falconne for better integration with the rest of the lua script:</p>
<img alt="images/workflow-status.png" src="images/workflow-status.png" />
<p>This screen shows all currently existing workflow constraints, and allows
monitoring and/or changing them from one screen. The constraint list can
be filtered by typing text in the field below.</p>
<p>The color of the stock level number indicates how &quot;healthy&quot; the stock level
is, based on current count and trend. Bright green is very good, green is good,
red is bad, bright red is very bad.</p>
<p>The limit number is also color-coded. Red means that there are currently no
workshops producing that item (i.e. no jobs). If it's yellow, that means the
production has been delayed, possibly due to lack of input materials.</p>
<p>The chart on the right is a plot of the last 14 days (28 half day plots) worth
of stock history for the selected item, with the rightmost point representing
the current stock value. The bright green dashed line is the target
limit (maximum) and the dark green line is that minus the gap (minimum).</p>
</div> </div>
<div class="section" id="gui-assign-rack"> <div class="section" id="gui-assign-rack">
<h2><a class="toc-backref" href="#id146">gui/assign-rack</a></h2> <h2><a class="toc-backref" href="#id146">gui/assign-rack</a></h2>

@ -2074,7 +2074,7 @@ directly to the main dwarf mode screen.
Search Search
====== ======
The search plugin adds search to the Stocks, Trading and Unit List screens. The search plugin adds search to the Stocks, Trading, Stockpile and Unit List screens.
.. image:: images/search.png .. image:: images/search.png
@ -2097,6 +2097,16 @@ 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' Value numbers displayed by the screen. Because of this, pressing the 't'
key while search is active clears the search instead of executing the trade. key while search is active clears the search instead of executing the trade.
In the stockpile screen the option only appears if the cursor is in the
rightmost list:
.. image:: images/search-stockpile.png
Note that the 'Permit XXX'/'Forbid XXX' keys conveniently operate only
on items actually shown in the rightmost list, so it is possible to select
only fat or tallow by forbidding fats, then searching for fat/tallow, and
using Permit Fats again while the list is filtered.
AutoMaterial AutoMaterial
============ ============
@ -2295,12 +2305,12 @@ current count is below the lower bound of the range, the job is resumed; if it
is above or equal to the top bound, it will be suspended. Within the range, the is above or equal to the top bound, it will be suspended. Within the range, the
specific constraint has no effect on the job; others may still affect it. specific constraint has no effect on the job; others may still affect it.
Pressing 'c' switches the current constraint between counting stacks or items. Pressing 'I' switches the current constraint between counting stacks or items.
Pressing 'm' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
bounds by 1, 5, or 25 depending on the direction and the 'c' setting (counting bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting
items and expanding the range each gives a 5x bonus). items and expanding the range each gives a 2x bonus).
Pressing 'n' produces a list of possible outputs of this job as guessed by Pressing 'A' produces a list of possible outputs of this job as guessed by
workflow, and lets you create a new constraint by choosing one as template. If you workflow, and lets you create a new constraint by choosing one as template. If you
don't see the choice you want in the list, it likely means you have to adjust don't see the choice you want in the list, it likely means you have to adjust
the job material first using ``job item-material`` or ``gui/workshop-job``, the job material first using ``job item-material`` or ``gui/workshop-job``,
@ -2309,14 +2319,35 @@ can be used for troubleshooting jobs that don't match the right constraints.
.. image:: images/workflow-new1.png .. image:: images/workflow-new1.png
After selecting one of the presented outputs, the interface proceeds to the If you select one of the outputs with Enter, the matching constraint is simply
added to the list. If you use Shift-Enter, the interface proceeds to the
next dialog, which allows you to edit the suggested constraint parameters to next dialog, which allows you to edit the suggested constraint parameters to
suit your need, and set the item count range. suit your need, and set the item count range.
.. image:: images/workflow-new2.png .. image:: images/workflow-new2.png
If you don't need advanced settings, you can just press 'y' to confirm creation. Pressing 'S' (or, with the example config, Alt-W in the 'z' stocks screen)
opens the overall status screen, which was copied from the C++ implementation
by falconne for better integration with the rest of the lua script:
.. image:: images/workflow-status.png
This screen shows all currently existing workflow constraints, and allows
monitoring and/or changing them from one screen. The constraint list can
be filtered by typing text in the field below.
The color of the stock level number indicates how "healthy" the stock level
is, based on current count and trend. Bright green is very good, green is good,
red is bad, bright red is very bad.
The limit number is also color-coded. Red means that there are currently no
workshops producing that item (i.e. no jobs). If it's yellow, that means the
production has been delayed, possibly due to lack of input materials.
The chart on the right is a plot of the last 14 days (28 half day plots) worth
of stock history for the selected item, with the rightmost point representing
the current stock value. The bright green dashed line is the target
limit (maximum) and the dark green line is that minus the gap (minimum).
gui/assign-rack gui/assign-rack

@ -91,6 +91,7 @@ keybinding add Alt-A@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workshop-job
# workflow front-end # workflow front-end
keybinding add Alt-W@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workflow keybinding add Alt-W@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workflow
keybinding add Alt-W@overallstatus "gui/workflow status"
# assign weapon racks to squads so that they can be used # assign weapon racks to squads so that they can be used
keybinding add P@dwarfmode/QueryBuilding/Some/Weaponrack gui/assign-rack keybinding add P@dwarfmode/QueryBuilding/Some/Weaponrack gui/assign-rack
@ -136,6 +137,13 @@ tweak military-color-assigned
# remove inverse dependency of squad training speed on unit list size and use more sparring # remove inverse dependency of squad training speed on unit list size and use more sparring
tweak military-training tweak military-training
###########
# Scripts #
###########
# write the correct season to gamelog on world load
soundsense-season
####################################################### #######################################################
# Apply binary patches at runtime # # Apply binary patches at runtime #
# # # #

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

@ -121,7 +121,6 @@ include/modules/Materials.h
include/modules/Notes.h include/modules/Notes.h
include/modules/Screen.h include/modules/Screen.h
include/modules/Translation.h include/modules/Translation.h
include/modules/Vegetation.h
include/modules/Vermin.h include/modules/Vermin.h
include/modules/World.h include/modules/World.h
include/modules/Graphic.h include/modules/Graphic.h
@ -142,7 +141,6 @@ modules/Materials.cpp
modules/Notes.cpp modules/Notes.cpp
modules/Screen.cpp modules/Screen.cpp
modules/Translation.cpp modules/Translation.cpp
modules/Vegetation.cpp
modules/Vermin.cpp modules/Vermin.cpp
modules/World.cpp modules/World.cpp
modules/Graphic.cpp modules/Graphic.cpp

@ -343,6 +343,50 @@ command_result Core::runCommand(color_ostream &out, const std::string &command)
return CR_NOT_IMPLEMENTED; return CR_NOT_IMPLEMENTED;
} }
static bool try_autocomplete(color_ostream &con, const std::string &first, std::string &completed)
{
std::vector<std::string> possible;
auto plug_mgr = Core::getInstance().getPluginManager();
for(size_t i = 0; i < plug_mgr->size(); i++)
{
const Plugin * plug = (plug_mgr->operator[](i));
for (size_t j = 0; j < plug->size(); j++)
{
const PluginCommand &pcmd = plug->operator[](j);
if (pcmd.isHotkeyCommand())
continue;
if (pcmd.name.substr(0, first.size()) == first)
possible.push_back(pcmd.name);
}
}
bool all = (first.find('/') != std::string::npos);
std::map<string, string> scripts;
listScripts(plug_mgr, scripts, Core::getInstance().getHackPath() + "scripts/", all);
for (auto iter = scripts.begin(); iter != scripts.end(); ++iter)
if (iter->first.substr(0, first.size()) == first)
possible.push_back(iter->first);
if (possible.size() == 1)
{
completed = possible[0];
fprintf(stderr, "Autocompleted %s to %s\n", first.c_str(), completed.c_str());
return true;
}
if (possible.size() > 1 && possible.size() < 8)
{
std::string out;
for (size_t i = 0; i < possible.size(); i++)
out += " " + possible[i];
con.print("Possible completions:%s\n", out.c_str());
}
return false;
}
command_result Core::runCommand(color_ostream &con, const std::string &first, vector<string> &parts) command_result Core::runCommand(color_ostream &con, const std::string &first, vector<string> &parts)
{ {
if (!first.empty()) if (!first.empty())
@ -665,10 +709,14 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
if(res == CR_NOT_IMPLEMENTED) if(res == CR_NOT_IMPLEMENTED)
{ {
auto filename = getHackPath() + "scripts/" + first; auto filename = getHackPath() + "scripts/" + first;
std::string completed;
if (fileExists(filename + ".lua")) if (fileExists(filename + ".lua"))
res = runLuaScript(con, first, parts); res = runLuaScript(con, first, parts);
else if (plug_mgr->eval_ruby && fileExists(filename + ".rb")) else if (plug_mgr->eval_ruby && fileExists(filename + ".rb"))
res = runRubyScript(con, plug_mgr, first, parts); res = runRubyScript(con, plug_mgr, first, parts);
else if (try_autocomplete(con, first, completed))
return runCommand(con, completed, parts);
else else
con.printerr("%s is not a recognized command.\n", first.c_str()); con.printerr("%s is not a recognized command.\n", first.c_str());
} }
@ -733,7 +781,6 @@ void fIOthread(void * iodata)
{ {
string command = ""; string command = "";
int ret = con.lineedit("[DFHack]# ",command, main_history); int ret = con.lineedit("[DFHack]# ",command, main_history);
fprintf(stderr,"Command: [%s]\n",command.c_str());
if(ret == -2) if(ret == -2)
{ {
cerr << "Console is shutting down properly." << endl; cerr << "Console is shutting down properly." << endl;
@ -747,14 +794,10 @@ void fIOthread(void * iodata)
else if(ret) else if(ret)
{ {
// a proper, non-empty command was entered // a proper, non-empty command was entered
fprintf(stderr,"Adding command to history\n");
main_history.add(command); main_history.add(command);
fprintf(stderr,"Saving history\n");
main_history.save("dfhack.history"); main_history.save("dfhack.history");
} }
fprintf(stderr,"Running command\n");
auto rv = core->runCommand(con, command); auto rv = core->runCommand(con, command);
if (rv == CR_NOT_IMPLEMENTED) if (rv == CR_NOT_IMPLEMENTED)

@ -61,7 +61,6 @@ distribution.
#include "modules/Translation.h" #include "modules/Translation.h"
#include "modules/World.h" #include "modules/World.h"
#include "modules/Items.h" #include "modules/Items.h"
#include "modules/Vegetation.h"
#include "modules/Maps.h" #include "modules/Maps.h"
#include "modules/Gui.h" #include "modules/Gui.h"

@ -33,7 +33,6 @@ namespace DFHack
Module* createGui(); Module* createGui();
Module* createWorld(); Module* createWorld();
Module* createMaterials(); Module* createMaterials();
Module* createVegetation();
Module* createNotes(); Module* createNotes();
Module* createGraphic(); Module* createGraphic();
} }

@ -32,7 +32,6 @@ distribution.
#include "Export.h" #include "Export.h"
#include "Module.h" #include "Module.h"
#include "modules/Vegetation.h"
#include <vector> #include <vector>
#include "BitArray.h" #include "BitArray.h"
#include "modules/Materials.h" #include "modules/Materials.h"

@ -1,70 +0,0 @@
/*
https://github.com/peterix/dfhack
Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com)
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any
damages arising from the use of this software.
Permission is granted to anyone to use this software for any
purpose, including commercial applications, and to alter it and
redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must
not claim that you wrote the original software. If you use this
software in a product, an acknowledgment in the product documentation
would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and
must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
*/
#pragma once
#ifndef CL_MOD_VEGETATION
#define CL_MOD_VEGETATION
/**
* \defgroup grp_vegetation Vegetation : stuff that grows and gets cut down or trampled by dwarves
* @ingroup grp_modules
*/
#include "Export.h"
#include "DataDefs.h"
#include "df/plant.h"
namespace DFHack
{
namespace Vegetation
{
const uint32_t sapling_to_tree_threshold = 120 * 28 * 12 * 3; // 3 years
// "Simplified" copy of plant
struct t_plant {
df::language_name name;
df::plant_flags flags;
int16_t material;
df::coord pos;
int32_t grow_counter;
uint16_t temperature_1;
uint16_t temperature_2;
int32_t is_burning;
int32_t hitpoints;
int16_t update_order;
//std::vector<void *> unk1;
//int32_t unk2;
//uint16_t temperature_3;
//uint16_t temperature_4;
//uint16_t temperature_5;
// Pointer to original object, in case you want to modify it
df::plant *origin;
};
DFHACK_EXPORT bool isValid();
DFHACK_EXPORT uint32_t getCount();
DFHACK_EXPORT df::plant * getPlant(const int32_t index);
DFHACK_EXPORT bool copyPlant (const int32_t index, t_plant &out);
}
}
#endif

@ -13,6 +13,8 @@ CLEAR_PEN = to_pen{ch=32,fg=0,bg=0}
local FAKE_INPUT_KEYS = { local FAKE_INPUT_KEYS = {
_MOUSE_L = true, _MOUSE_L = true,
_MOUSE_R = true, _MOUSE_R = true,
_MOUSE_L_DOWN = true,
_MOUSE_R_DOWN = true,
_STRING = true, _STRING = true,
} }
@ -112,10 +114,14 @@ function inset_frame(rect, inset, gap)
return mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap) return mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
end end
function compute_frame_body(wavail, havail, spec, inset, gap) function compute_frame_body(wavail, havail, spec, inset, gap, inner_frame)
gap = gap or 0 gap = gap or 0
local l,t,r,b = parse_inset(inset) local l,t,r,b = parse_inset(inset)
local rect = compute_frame_rect(wavail, havail, spec, gap*2+l+r, gap*2+t+b) local xgap,ygap = 0,0
if inner_frame then
xgap,ygap = gap*2+l+r, gap*2+t+b
end
local rect = compute_frame_rect(wavail, havail, spec, xgap, ygap)
local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap) local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
return rect, body return rect, body
end end
@ -623,7 +629,7 @@ end
function FramedScreen:computeFrame(parent_rect) function FramedScreen:computeFrame(parent_rect)
local sw, sh = parent_rect.width, parent_rect.height local sw, sh = parent_rect.width, parent_rect.height
local fw, fh = self:getWantedFrameSize(parent_rect) local fw, fh = self:getWantedFrameSize(parent_rect)
return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1) return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1, true)
end end
function FramedScreen:onRenderFrame(dc, rect) function FramedScreen:onRenderFrame(dc, rect)

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

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

@ -59,6 +59,7 @@ using namespace std;
#include "df/z_level_flags.h" #include "df/z_level_flags.h"
#include "df/region_map_entry.h" #include "df/region_map_entry.h"
#include "df/flow_info.h" #include "df/flow_info.h"
#include "df/plant.h"
using namespace DFHack; using namespace DFHack;
using namespace df::enums; using namespace df::enums;

@ -664,14 +664,24 @@ int dfhack_lua_viewscreen::do_input(lua_State *L)
if (enabler && enabler->tracking_on) if (enabler && enabler->tracking_on)
{ {
if (enabler->mouse_lbut) { if (enabler->mouse_lbut_down) {
lua_pushboolean(L, true); lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_L"); lua_setfield(L, -2, "_MOUSE_L");
} }
if (enabler->mouse_rbut) { if (enabler->mouse_rbut_down) {
lua_pushboolean(L, true); lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_R"); lua_setfield(L, -2, "_MOUSE_R");
} }
if (enabler->mouse_lbut) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_L_DOWN");
enabler->mouse_lbut = 0;
}
if (enabler->mouse_rbut) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_R_DOWN");
enabler->mouse_rbut = 0;
}
} }
lua_call(L, 2, 0); lua_call(L, 2, 0);

@ -115,6 +115,9 @@ void Translation::setNickname(df::language_name *name, std::string nick)
if (!name->has_name) if (!name->has_name)
{ {
if (nick.empty())
return;
*name = df::language_name(); *name = df::language_name();
name->language = 0; name->language = 0;
@ -122,6 +125,18 @@ void Translation::setNickname(df::language_name *name, std::string nick)
} }
name->nickname = nick; name->nickname = nick;
// If the nick is empty, check if this made the whole name empty
if (name->nickname.empty() && name->first_name.empty())
{
bool has_words = false;
for (int i = 0; i < 7; i++)
if (name->words[i] >= 0)
has_words = true;
if (!has_words)
name->has_name = false;
}
} }
string Translation::TranslateName(const df::language_name * name, bool inEnglish, bool onlyLastPart) string Translation::TranslateName(const df::language_name * name, bool inEnglish, bool onlyLastPart)

@ -1,85 +0,0 @@
/*
https://github.com/peterix/dfhack
Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com)
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any
damages arising from the use of this software.
Permission is granted to anyone to use this software for any
purpose, including commercial applications, and to alter it and
redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must
not claim that you wrote the original software. If you use this
software in a product, an acknowledgment in the product documentation
would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and
must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
*/
#include "Internal.h"
#include <string>
#include <vector>
#include <map>
using namespace std;
#include "VersionInfo.h"
#include "MemAccess.h"
#include "Types.h"
#include "Core.h"
using namespace DFHack;
#include "modules/Vegetation.h"
#include "df/world.h"
using namespace DFHack;
using df::global::world;
bool Vegetation::isValid()
{
return (world != NULL);
}
uint32_t Vegetation::getCount()
{
return world->plants.all.size();
}
df::plant * Vegetation::getPlant(const int32_t index)
{
if (uint32_t(index) >= getCount())
return NULL;
return world->plants.all[index];
}
bool Vegetation::copyPlant(const int32_t index, t_plant &out)
{
if (uint32_t(index) >= getCount())
return false;
out.origin = world->plants.all[index];
out.name = out.origin->name;
out.flags = out.origin->flags;
out.material = out.origin->material;
out.pos = out.origin->pos;
out.grow_counter = out.origin->grow_counter;
out.temperature_1 = out.origin->temperature.whole;
out.temperature_2 = out.origin->temperature.fraction;
out.is_burning = out.origin->is_burning;
out.hitpoints = out.origin->hitpoints;
out.update_order = out.origin->update_order;
//out.unk1 = out.origin->anon_1;
//out.unk2 = out.origin->anon_2;
//out.temperature_3 = out.origin->temperature_unk;
//out.temperature_4 = out.origin->min_safe_temp;
//out.temperature_5 = out.origin->max_safe_temp;
return true;
}

@ -148,8 +148,8 @@ static MaterialDescriptor get_material_in_list(size_t i)
} }
else if (VIRTUAL_CAST_VAR(spec, df::build_req_choice_specst, ui_build_selector->choices[i])) else if (VIRTUAL_CAST_VAR(spec, df::build_req_choice_specst, ui_build_selector->choices[i]))
{ {
result.item_type = gen->item_type; result.item_type = spec->candidate->getType();
result.item_subtype = gen->item_subtype; result.item_subtype = spec->candidate->getSubtype();
result.type = spec->candidate->getActualMaterial(); result.type = spec->candidate->getActualMaterial();
result.index = spec->candidate->getActualMaterialIndex(); result.index = spec->candidate->getActualMaterialIndex();
result.valid = true; result.valid = true;
@ -294,7 +294,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
{ {
if (in_material_choice_stage()) if (in_material_choice_stage())
{ {
if (!last_used_moved) if (!last_used_moved && ui_build_selector->is_grouped)
{ {
if (auto_choose_materials && get_curr_constr_prefs().size() > 0) if (auto_choose_materials && get_curr_constr_prefs().size() > 0)
{ {
@ -304,7 +304,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
return; return;
} }
} }
else if (ui_build_selector->is_grouped) else
{ {
last_used_moved = true; last_used_moved = true;
move_material_to_top(get_last_used_material()); move_material_to_top(get_last_used_material());

@ -12,6 +12,7 @@
#include "df/global_objects.h" #include "df/global_objects.h"
#include "df/builtin_mats.h" #include "df/builtin_mats.h"
#include "df/contaminant.h" #include "df/contaminant.h"
#include "df/plant.h"
using std::vector; using std::vector;
using std::string; using std::string;

@ -11,7 +11,6 @@ using std::string;
#include <Console.h> #include <Console.h>
#include <Export.h> #include <Export.h>
#include <PluginManager.h> #include <PluginManager.h>
#include <modules/Vegetation.h>
#include <modules/Maps.h> #include <modules/Maps.h>
#include <modules/Gui.h> #include <modules/Gui.h>
#include <TileTypes.h> #include <TileTypes.h>

@ -110,7 +110,8 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out)
* 1. Combat ammo and ammo without any allowed use can be stored * 1. Combat ammo and ammo without any allowed use can be stored
* in BOXes marked for Squad Equipment, either directly or via * in BOXes marked for Squad Equipment, either directly or via
* containing room. No-allowed-use ammo is assumed to be reserved * containing room. No-allowed-use ammo is assumed to be reserved
* for emergency combat use, or something like that. * for emergency combat use, or something like that; however if
* it is already stored in a training chest, it won't be moved.
* 1a. If assigned to a squad position, that box can be used _only_ * 1a. If assigned to a squad position, that box can be used _only_
* for ammo assigned to that specific _squad_. Otherwise, if * for ammo assigned to that specific _squad_. Otherwise, if
* multiple squads can use this room, they will store their * multiple squads can use this room, they will store their
@ -158,8 +159,8 @@ static bool is_squad_ammo(df::item *item, df::squad *squad, bool combat, bool tr
bool cs = spec->flags.bits.use_combat; bool cs = spec->flags.bits.use_combat;
bool ts = spec->flags.bits.use_training; bool ts = spec->flags.bits.use_training;
// no-use ammo assumed to be combat // no-use ammo assumed to fit any category
if (((cs || !ts) && combat) || (ts && train)) if (((cs || !ts) && combat) || ((ts || !cs) && train))
{ {
if (binsearch_index(spec->assigned, item->id) >= 0) if (binsearch_index(spec->assigned, item->id) >= 0)
return true; return true;

@ -11,8 +11,8 @@
#include "df/map_block.h" #include "df/map_block.h"
#include "df/tile_dig_designation.h" #include "df/tile_dig_designation.h"
#include "df/plant_raw.h" #include "df/plant_raw.h"
#include "df/plant.h"
#include "modules/Vegetation.h"
#include <set> #include <set>
using std::string; using std::string;

@ -37,7 +37,6 @@ using std::set;
#include "Console.h" #include "Console.h"
#include "Export.h" #include "Export.h"
#include "PluginManager.h" #include "PluginManager.h"
#include "modules/Vegetation.h"
#include "modules/Maps.h" #include "modules/Maps.h"
#include "modules/Gui.h" #include "modules/Gui.h"
#include "TileTypes.h" #include "TileTypes.h"

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

@ -13,6 +13,7 @@ using namespace google::protobuf::io;
#include "DataDefs.h" #include "DataDefs.h"
#include "df/world.h" #include "df/world.h"
#include "df/plant.h"
#include "modules/Constructions.h" #include "modules/Constructions.h"
#include "proto/Map.pb.h" #include "proto/Map.pb.h"

@ -9,17 +9,19 @@
#include "Console.h" #include "Console.h"
#include "Export.h" #include "Export.h"
#include "PluginManager.h" #include "PluginManager.h"
#include "modules/Vegetation.h"
#include "modules/Maps.h" #include "modules/Maps.h"
#include "modules/Gui.h" #include "modules/Gui.h"
#include "TileTypes.h" #include "TileTypes.h"
#include "modules/MapCache.h" #include "modules/MapCache.h"
#include "df/plant.h"
using std::vector; using std::vector;
using std::string; using std::string;
using namespace DFHack; using namespace DFHack;
using df::global::world; using df::global::world;
const uint32_t sapling_to_tree_threshold = 120 * 28 * 12 * 3; // 3 years
command_result df_grow (color_ostream &out, vector <string> & parameters); command_result df_grow (color_ostream &out, vector <string> & parameters);
command_result df_immolate (color_ostream &out, vector <string> & parameters); command_result df_immolate (color_ostream &out, vector <string> & parameters);
command_result df_extirpate (color_ostream &out, vector <string> & parameters); command_result df_extirpate (color_ostream &out, vector <string> & parameters);
@ -113,7 +115,7 @@ static command_result immolations (color_ostream &out, do_what what, bool shrubs
if(shrubs && p->flags.bits.is_shrub || trees && !p->flags.bits.is_shrub) if(shrubs && p->flags.bits.is_shrub || trees && !p->flags.bits.is_shrub)
{ {
if (what == do_immolate) if (what == do_immolate)
p->is_burning = true; p->damage_flags.bits.is_burning = true;
p->hitpoints = 0; p->hitpoints = 0;
destroyed ++; destroyed ++;
} }
@ -136,7 +138,7 @@ static command_result immolations (color_ostream &out, do_what what, bool shrubs
if(tree->pos.x == x && tree->pos.y == y && tree->pos.z == z) if(tree->pos.x == x && tree->pos.y == y && tree->pos.z == z)
{ {
if(what == do_immolate) if(what == do_immolate)
tree->is_burning = true; tree->damage_flags.bits.is_burning = true;
tree->hitpoints = 0; tree->hitpoints = 0;
didit = true; didit = true;
break; break;
@ -219,7 +221,7 @@ command_result df_grow (color_ostream &out, vector <string> & parameters)
if(tileShape(map.tiletypeAt(DFCoord(x,y,z))) == tiletype_shape::SAPLING && if(tileShape(map.tiletypeAt(DFCoord(x,y,z))) == tiletype_shape::SAPLING &&
tileSpecial(map.tiletypeAt(DFCoord(x,y,z))) != tiletype_special::DEAD) tileSpecial(map.tiletypeAt(DFCoord(x,y,z))) != tiletype_special::DEAD)
{ {
tree->grow_counter = Vegetation::sapling_to_tree_threshold; tree->grow_counter = sapling_to_tree_threshold;
} }
break; break;
} }
@ -235,7 +237,7 @@ command_result df_grow (color_ostream &out, vector <string> & parameters)
df::tiletype ttype = map.tiletypeAt(df::coord(p->pos.x,p->pos.y,p->pos.z)); df::tiletype ttype = map.tiletypeAt(df::coord(p->pos.x,p->pos.y,p->pos.z));
if(!p->flags.bits.is_shrub && tileShape(ttype) == tiletype_shape::SAPLING && tileSpecial(ttype) != tiletype_special::DEAD) if(!p->flags.bits.is_shrub && tileShape(ttype) == tiletype_shape::SAPLING && tileSpecial(ttype) != tiletype_special::DEAD)
{ {
p->grow_counter = Vegetation::sapling_to_tree_threshold; p->grow_counter = sapling_to_tree_threshold;
} }
} }
} }

@ -33,6 +33,7 @@ using namespace std;
#include "df/region_map_entry.h" #include "df/region_map_entry.h"
#include "df/inclusion_type.h" #include "df/inclusion_type.h"
#include "df/viewscreen_choose_start_sitest.h" #include "df/viewscreen_choose_start_sitest.h"
#include "df/plant.h"
using namespace DFHack; using namespace DFHack;
using namespace df::enums; using namespace df::enums;

@ -698,6 +698,8 @@ sub sizeof {
return 12; return 12;
} elsif ($subtype eq 'df-flagarray') { } elsif ($subtype eq 'df-flagarray') {
return 8; return 8;
} elsif ($subtype eq 'df-static-flagarray') {
return $field->getAttribute('count');
} elsif ($subtype eq 'df-array') { } elsif ($subtype eq 'df-array') {
return 8; # XXX 6 ? return 8; # XXX 6 ?
} else { } else {
@ -849,6 +851,9 @@ sub render_item_number {
} elsif ($subtype eq 's-float') { } elsif ($subtype eq 's-float') {
push @lines_rb, 'float'; push @lines_rb, 'float';
return; return;
} elsif ($subtype eq 'd-float') {
push @lines_rb, 'double';
return;
} else { } else {
print "no render number $subtype\n"; print "no render number $subtype\n";
return; return;
@ -910,6 +915,7 @@ sub render_item_container {
my $rbmethod = join('_', split('-', $subtype)); my $rbmethod = join('_', split('-', $subtype));
my $tg = $item->findnodes('child::ld:item')->[0]; my $tg = $item->findnodes('child::ld:item')->[0];
my $indexenum = $item->getAttribute('index-enum'); my $indexenum = $item->getAttribute('index-enum');
my $count = $item->getAttribute('count');
if ($tg) if ($tg)
{ {
if ($rbmethod eq 'df_linked_list') { if ($rbmethod eq 'df_linked_list') {
@ -926,12 +932,20 @@ sub render_item_container {
elsif ($indexenum) elsif ($indexenum)
{ {
$indexenum = rb_ucase($indexenum); $indexenum = rb_ucase($indexenum);
if ($count) {
push @lines_rb, "$rbmethod($count, $indexenum)";
} else {
push @lines_rb, "$rbmethod($indexenum)"; push @lines_rb, "$rbmethod($indexenum)";
} }
}
else else
{ {
if ($count) {
push @lines_rb, "$rbmethod($count)";
} else {
push @lines_rb, "$rbmethod"; push @lines_rb, "$rbmethod";
} }
}
} }
sub render_item_pointer { sub render_item_pointer {

@ -35,6 +35,9 @@ module DFHack
def float def float
Float.new Float.new
end end
def double
Double.new
end
def bit(shift, enum=nil) def bit(shift, enum=nil)
BitField.new(shift, 1, enum) BitField.new(shift, 1, enum)
end end
@ -75,6 +78,9 @@ module DFHack
def df_flagarray(indexenum=nil) def df_flagarray(indexenum=nil)
DfFlagarray.new(indexenum) DfFlagarray.new(indexenum)
end end
def df_static_flagarray(len, indexenum=nil)
DfStaticFlagarray.new(len, indexenum)
end
def df_array(tglen) def df_array(tglen)
DfArray.new(tglen, yield) DfArray.new(tglen, yield)
end end
@ -237,6 +243,19 @@ module DFHack
_set(0.0) _set(0.0)
end end
end end
class Double < MemStruct
def _get
DFHack.memory_read_double(@_memaddr)
end
def _set(v)
DFHack.memory_write_double(@_memaddr, v)
end
def _cpp_init
_set(0.0)
end
end
class BitField < MemStruct class BitField < MemStruct
attr_accessor :_shift, :_len, :_enum attr_accessor :_shift, :_len, :_enum
def initialize(shift, len, enum=nil) def initialize(shift, len, enum=nil)
@ -308,7 +327,7 @@ module DFHack
DFHack.memory_write_int32(@_memaddr, v) DFHack.memory_write_int32(@_memaddr, v)
end end
when nil; DFHack.memory_write_int32(@_memaddr, 0) when nil; DFHack.memory_write_int32(@_memaddr, 0)
else _get._set(v) else @_tg._at(_getp)._set(v)
end end
end end
@ -664,6 +683,48 @@ module DFHack
include Enumerable include Enumerable
end end
class DfStaticFlagarray < MemStruct
attr_accessor :_indexenum
def initialize(len, indexenum)
@len = len*8
@_indexenum = indexenum
end
def length
@len
end
def size ; length ; end
def [](idx)
idx = _indexenum.int(idx) if _indexenum
idx += length if idx < 0
return if idx < 0 or idx >= length
byte = DFHack.memory_read_int8(@_memaddr + idx/8)
(byte & (1 << (idx%8))) > 0
end
def []=(idx, v)
idx = _indexenum.int(idx) if _indexenum
idx += length if idx < 0
if idx >= length or idx < 0
raise 'index out of bounds'
else
byte = DFHack.memory_read_int8(@_memaddr + idx/8)
if (v == nil or v == false or v == 0)
byte &= 0xff ^ (1 << (idx%8))
else
byte |= (1 << (idx%8))
end
DFHack.memory_write_int8(@_memaddr + idx/8, byte)
end
end
def inspect
out = "#<DfStaticFlagarray"
each_with_index { |e, idx|
out << " #{_indexenum.sym(idx)}" if e
}
out << '>'
end
include Enumerable
end
class DfArray < Compound class DfArray < Compound
attr_accessor :_tglen, :_tg attr_accessor :_tglen, :_tg
def initialize(tglen, tg) def initialize(tglen, tg)

@ -578,6 +578,11 @@ static VALUE rb_dfmemory_read_float(VALUE self, VALUE addr)
return rb_float_new(*(float*)rb_num2ulong(addr)); return rb_float_new(*(float*)rb_num2ulong(addr));
} }
static VALUE rb_dfmemory_read_double(VALUE self, VALUE addr)
{
return rb_float_new(*(double*)rb_num2ulong(addr));
}
// memory writing (buffer) // memory writing (buffer)
static VALUE rb_dfmemory_write(VALUE self, VALUE addr, VALUE raw) static VALUE rb_dfmemory_write(VALUE self, VALUE addr, VALUE raw)
@ -613,6 +618,12 @@ static VALUE rb_dfmemory_write_float(VALUE self, VALUE addr, VALUE val)
return Qtrue; return Qtrue;
} }
static VALUE rb_dfmemory_write_double(VALUE self, VALUE addr, VALUE val)
{
*(double*)rb_num2ulong(addr) = rb_num2dbl(val);
return Qtrue;
}
// return memory permissions at address (eg "rx", nil if unmapped) // return memory permissions at address (eg "rx", nil if unmapped)
static VALUE rb_dfmemory_check(VALUE self, VALUE addr) static VALUE rb_dfmemory_check(VALUE self, VALUE addr)
{ {
@ -968,12 +979,14 @@ static void ruby_bind_dfhack(void) {
rb_define_singleton_method(rb_cDFHack, "memory_read_int16", RUBY_METHOD_FUNC(rb_dfmemory_read_int16), 1); rb_define_singleton_method(rb_cDFHack, "memory_read_int16", RUBY_METHOD_FUNC(rb_dfmemory_read_int16), 1);
rb_define_singleton_method(rb_cDFHack, "memory_read_int32", RUBY_METHOD_FUNC(rb_dfmemory_read_int32), 1); rb_define_singleton_method(rb_cDFHack, "memory_read_int32", RUBY_METHOD_FUNC(rb_dfmemory_read_int32), 1);
rb_define_singleton_method(rb_cDFHack, "memory_read_float", RUBY_METHOD_FUNC(rb_dfmemory_read_float), 1); rb_define_singleton_method(rb_cDFHack, "memory_read_float", RUBY_METHOD_FUNC(rb_dfmemory_read_float), 1);
rb_define_singleton_method(rb_cDFHack, "memory_read_double", RUBY_METHOD_FUNC(rb_dfmemory_read_double), 1);
rb_define_singleton_method(rb_cDFHack, "memory_write", RUBY_METHOD_FUNC(rb_dfmemory_write), 2); rb_define_singleton_method(rb_cDFHack, "memory_write", RUBY_METHOD_FUNC(rb_dfmemory_write), 2);
rb_define_singleton_method(rb_cDFHack, "memory_write_int8", RUBY_METHOD_FUNC(rb_dfmemory_write_int8), 2); rb_define_singleton_method(rb_cDFHack, "memory_write_int8", RUBY_METHOD_FUNC(rb_dfmemory_write_int8), 2);
rb_define_singleton_method(rb_cDFHack, "memory_write_int16", RUBY_METHOD_FUNC(rb_dfmemory_write_int16), 2); rb_define_singleton_method(rb_cDFHack, "memory_write_int16", RUBY_METHOD_FUNC(rb_dfmemory_write_int16), 2);
rb_define_singleton_method(rb_cDFHack, "memory_write_int32", RUBY_METHOD_FUNC(rb_dfmemory_write_int32), 2); rb_define_singleton_method(rb_cDFHack, "memory_write_int32", RUBY_METHOD_FUNC(rb_dfmemory_write_int32), 2);
rb_define_singleton_method(rb_cDFHack, "memory_write_float", RUBY_METHOD_FUNC(rb_dfmemory_write_float), 2); rb_define_singleton_method(rb_cDFHack, "memory_write_float", RUBY_METHOD_FUNC(rb_dfmemory_write_float), 2);
rb_define_singleton_method(rb_cDFHack, "memory_write_double", RUBY_METHOD_FUNC(rb_dfmemory_write_double), 2);
rb_define_singleton_method(rb_cDFHack, "memory_check", RUBY_METHOD_FUNC(rb_dfmemory_check), 1); rb_define_singleton_method(rb_cDFHack, "memory_check", RUBY_METHOD_FUNC(rb_dfmemory_check), 1);
rb_define_singleton_method(rb_cDFHack, "memory_patch", RUBY_METHOD_FUNC(rb_dfmemory_patch), 2); rb_define_singleton_method(rb_cDFHack, "memory_patch", RUBY_METHOD_FUNC(rb_dfmemory_patch), 2);

@ -63,12 +63,54 @@ module DFHack
} }
end end
def unit_testflagcurse(u, flag)
return false if u.curse.rem_tags1.send(flag)
return true if u.curse.add_tags1.send(flag)
return false if u.caste < 0
u.race_tg.caste[u.caste].flags[flag]
end
def unit_isfortmember(u)
# RE from viewscreen_unitlistst ctor
return false if df.gamemode != :DWARF or
u.mood == :Berserk or
unit_testflagcurse(u, :CRAZED) or
unit_testflagcurse(u, :OPPOSED_TO_LIFE) or
u.unknown8.unk2 or
u.flags3.ghostly or
u.flags1.marauder or u.flags1.active_invader or u.flags1.invader_origin or
u.flags1.forest or
u.flags1.merchant or u.flags1.diplomat
return true if u.flags1.tame
return false if u.flags2.underworld or u.flags2.resident or
u.flags2.visitor_uninvited or u.flags2.visitor or
u.civ_id == -1 or
u.civ_id != df.ui.civ_id
true
end
# return the page in viewscreen_unitlist where the unit would appear
def unit_category(u)
return if u.flags1.left or u.flags1.incoming
# return if hostile & unit_invisible(u) (hidden_in_ambush or caged+mapblock.hidden or caged+holder.ambush
return :Dead if u.flags1.dead
return :Dead if u.flags3.ghostly # hostile ?
return :Others if !unit_isfortmember(u)
casteflags = u.race_tg.caste[u.caste].flags if u.caste >= 0
return :Livestock if casteflags and (casteflags[:PET] or casteflags[:PET_EXOTIC])
return :Citizens if unit_testflagcurse(u, :CAN_SPEAK)
:Livestock
# some other stuff with ui.race_id ? (jobs only?)
end
def unit_iscitizen(u) def unit_iscitizen(u)
u.race == ui.race_id and u.civ_id == ui.civ_id and !u.flags1.dead and !u.flags1.merchant and !u.flags1.forest and unit_category(u) == :Citizens
!u.flags1.diplomat and !u.flags2.resident and !u.flags3.ghostly and end
!u.curse.add_tags1.OPPOSED_TO_LIFE and !u.curse.add_tags1.CRAZED and
u.mood != :Berserk def unit_ishostile(u)
# TODO check curse ; currently this should keep vampires, but may include werebeasts unit_category(u) == :Others and
# TODO
true
end end
# list workers (citizen, not crazy / child / inmood / noble) # list workers (citizen, not crazy / child / inmood / noble)

@ -7,10 +7,12 @@
//#include "df/viewscreen_petst.h" //#include "df/viewscreen_petst.h"
#include "df/viewscreen_storesst.h" #include "df/viewscreen_storesst.h"
#include "df/viewscreen_layer_stockpilest.h"
#include "df/viewscreen_tradegoodsst.h" #include "df/viewscreen_tradegoodsst.h"
#include "df/viewscreen_unitlistst.h" #include "df/viewscreen_unitlistst.h"
#include "df/interface_key.h" #include "df/interface_key.h"
#include "df/interfacest.h" #include "df/interfacest.h"
#include "df/layer_object_listst.h"
using std::set; using std::set;
using std::vector; using std::vector;
@ -79,6 +81,11 @@ public:
return true; return true;
} }
bool is_valid()
{
return valid;
}
// A new keystroke is received in a searchable screen // A new keystroke is received in a searchable screen
virtual bool process_input(set<df::interface_key> *input) virtual bool process_input(set<df::interface_key> *input)
{ {
@ -164,9 +171,9 @@ protected:
const S *viewscreen; const S *viewscreen;
vector <T> saved_list1, reference_list; vector <T> saved_list1, reference_list;
vector <V> saved_list2; vector <V> saved_list2;
vector <V> *sort_list2;
vector <int> saved_indexes; vector <int> saved_indexes;
bool valid;
bool redo_search; bool redo_search;
bool track_secondary_values; bool track_secondary_values;
string search_string; string search_string;
@ -240,12 +247,17 @@ protected:
} }
} }
saved_list2[saved_indexes[i]] = (*sort_list2)[adjusted_item_index]; update_saved_secondary_list_item(saved_indexes[i], adjusted_item_index);
} }
saved_indexes.clear(); saved_indexes.clear();
} }
} }
virtual void update_saved_secondary_list_item(size_t i, size_t j)
{
saved_list2[i] = (*sort_list2)[j];
}
// Store a copy of filtered list, used later to work out if filtered list has been sorted after filtering // Store a copy of filtered list, used later to work out if filtered list has been sorted after filtering
void store_reference_values() void store_reference_values()
{ {
@ -254,7 +266,7 @@ protected:
} }
// Shortcut to clear the search immediately // Shortcut to clear the search immediately
void clear_search() virtual void clear_search()
{ {
if (saved_list1.size() > 0) if (saved_list1.size() > 0)
{ {
@ -273,7 +285,7 @@ protected:
} }
// The actual sort // The actual sort
void do_search() virtual void do_search()
{ {
if (search_string.length() == 0) if (search_string.length() == 0)
{ {
@ -318,6 +330,7 @@ protected:
store_reference_values(); //Keep a copy, in case user sorts new list store_reference_values(); //Keep a copy, in case user sorts new list
if (cursor_pos)
*cursor_pos = 0; *cursor_pos = 0;
} }
@ -346,10 +359,9 @@ protected:
private: private:
vector <T> *sort_list1; vector <T> *sort_list1;
vector <V> *sort_list2;
int *cursor_pos; int *cursor_pos;
char select_key; char select_key;
bool valid;
bool entry_mode; bool entry_mode;
df::interface_key select_token; df::interface_key select_token;
@ -359,7 +371,8 @@ private:
}; };
template <class S, class T, class V> search_parent<S,T,V> *search_parent<S,T,V> ::lock = NULL; template <class S, class T, class V> search_parent<S,T,V> *search_parent<S,T,V> ::lock = NULL;
// Parent struct for the hooks // Parent struct for the hooks, use optional param D to generate multiple classes with same T & V
// but different static modules
template <class T, class V, typename D = void> template <class T, class V, typename D = void>
struct search_hook : T struct search_hook : T
{ {
@ -437,7 +450,7 @@ public:
if (screen != viewscreen && !reset_on_change()) if (screen != viewscreen && !reset_on_change())
return false; return false;
if (!valid) if (!is_valid())
{ {
viewscreen = screen; viewscreen = screen;
search_parent::init(&screen->item_cursor, &screen->items); search_parent::init(&screen->item_cursor, &screen->items);
@ -501,7 +514,7 @@ public:
if (screen != viewscreen && !reset_on_change()) if (screen != viewscreen && !reset_on_change())
return false; return false;
if (!valid) if (!is_valid())
{ {
viewscreen = screen; viewscreen = screen;
search_parent::init(&screen->cursor_pos[viewscreen->page], &screen->units[viewscreen->page], &screen->jobs[viewscreen->page]); search_parent::init(&screen->cursor_pos[viewscreen->page], &screen->units[viewscreen->page], &screen->jobs[viewscreen->page]);
@ -608,7 +621,7 @@ public:
if (screen != viewscreen && !reset_on_change()) if (screen != viewscreen && !reset_on_change())
return false; return false;
if (!valid) if (!is_valid())
{ {
viewscreen = screen; viewscreen = screen;
search_parent::init(&screen->trader_cursor, &screen->trader_items, &screen->trader_selected, 'q'); search_parent::init(&screen->trader_cursor, &screen->trader_items, &screen->trader_selected, 'q');
@ -637,7 +650,7 @@ public:
if (screen != viewscreen && !reset_on_change()) if (screen != viewscreen && !reset_on_change())
return false; return false;
if (!valid) if (!is_valid())
{ {
viewscreen = screen; viewscreen = screen;
search_parent::init(&screen->broker_cursor, &screen->broker_items, &screen->broker_selected, 'w'); search_parent::init(&screen->broker_cursor, &screen->broker_items, &screen->broker_selected, 'w');
@ -657,6 +670,84 @@ template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_fort_hook, render);
// //
//
// START: Stockpile screen search
//
class stockpile_search : public search_parent<df::viewscreen_layer_stockpilest, string *, bool *>
{
public:
void update_saved_secondary_list_item(size_t i, size_t j)
{
*saved_list2[i] = *(*sort_list2)[j];
}
string get_element_description(string *element) const
{
return *element;
}
void render() const
{
print_search_option(51, 23);
}
static df::layer_object_listst *getLayerList(const df::viewscreen_layer *layer, int idx)
{
return virtual_cast<df::layer_object_listst>(vector_get(layer->layer_objects,idx));
}
bool init(df::viewscreen_layer_stockpilest *screen)
{
if (screen != viewscreen && !reset_on_change())
return false;
auto list3 = getLayerList(screen, 2);
if (!list3->active)
{
if (is_valid())
{
clear_search();
reset_all();
}
return false;
}
if (!is_valid())
{
viewscreen = screen;
search_parent::init(&list3->cursor, &screen->item_names, &screen->item_status);
track_secondary_values = true;
}
return true;
}
void do_search()
{
search_parent::do_search();
auto list3 = getLayerList(viewscreen, 2);
list3->num_entries = viewscreen->item_names.size();
}
void clear_search()
{
search_parent::clear_search();
auto list3 = getLayerList(viewscreen, 2);
list3->num_entries = viewscreen->item_names.size();
}
};
typedef search_hook<df::viewscreen_layer_stockpilest, stockpile_search> stockpile_search_hook;
template<> IMPLEMENT_VMETHOD_INTERPOSE(stockpile_search_hook, feed);
template<> IMPLEMENT_VMETHOD_INTERPOSE(stockpile_search_hook, render);
//
// END: Stockpile screen search
//
DFHACK_PLUGIN("search"); DFHACK_PLUGIN("search");
@ -670,7 +761,9 @@ DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCom
!INTERPOSE_HOOK(trade_search_fort_hook, feed).apply() || !INTERPOSE_HOOK(trade_search_fort_hook, feed).apply() ||
!INTERPOSE_HOOK(trade_search_fort_hook, render).apply() || !INTERPOSE_HOOK(trade_search_fort_hook, render).apply() ||
!INTERPOSE_HOOK(stocks_search_hook, feed).apply() || !INTERPOSE_HOOK(stocks_search_hook, feed).apply() ||
!INTERPOSE_HOOK(stocks_search_hook, render).apply()) !INTERPOSE_HOOK(stocks_search_hook, render).apply() ||
!INTERPOSE_HOOK(stockpile_search_hook, feed).apply() ||
!INTERPOSE_HOOK(stockpile_search_hook, render).apply())
out.printerr("Could not insert Search hooks!\n"); out.printerr("Could not insert Search hooks!\n");
return CR_OK; return CR_OK;
@ -686,6 +779,8 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
INTERPOSE_HOOK(trade_search_fort_hook, render).remove(); INTERPOSE_HOOK(trade_search_fort_hook, render).remove();
INTERPOSE_HOOK(stocks_search_hook, feed).remove(); INTERPOSE_HOOK(stocks_search_hook, feed).remove();
INTERPOSE_HOOK(stocks_search_hook, render).remove(); INTERPOSE_HOOK(stocks_search_hook, render).remove();
INTERPOSE_HOOK(stockpile_search_hook, feed).remove();
INTERPOSE_HOOK(stockpile_search_hook, render).remove();
return CR_OK; return CR_OK;
} }
@ -697,6 +792,7 @@ DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_ch
trade_search_merc_hook::module.reset_on_change(); trade_search_merc_hook::module.reset_on_change();
trade_search_fort_hook::module.reset_on_change(); trade_search_fort_hook::module.reset_on_change();
stocks_search_hook::module.reset_on_change(); stocks_search_hook::module.reset_on_change();
stockpile_search_hook::module.reset_on_change();
break; break;
default: default:

@ -34,7 +34,6 @@ using std::set;
#include "Console.h" #include "Console.h"
#include "Export.h" #include "Export.h"
#include "PluginManager.h" #include "PluginManager.h"
#include "modules/Vegetation.h"
#include "modules/Maps.h" #include "modules/Maps.h"
#include "modules/Gui.h" #include "modules/Gui.h"
#include "TileTypes.h" #include "TileTypes.h"

@ -291,6 +291,15 @@ int ProtectedJob::cur_tick_idx = 0;
typedef std::map<std::pair<int,int>, bool> TMaterialCache; typedef std::map<std::pair<int,int>, bool> TMaterialCache;
static const size_t MAX_HISTORY_SIZE = 28;
enum HistoryItem {
HIST_COUNT = 0,
HIST_AMOUNT,
HIST_INUSE_COUNT,
HIST_INUSE_AMOUNT
};
struct ItemConstraint { struct ItemConstraint {
PersistentDataItem config; PersistentDataItem config;
PersistentDataItem history; PersistentDataItem history;
@ -313,6 +322,7 @@ struct ItemConstraint {
bool request_suspend, request_resume; bool request_suspend, request_resume;
bool is_active, cant_resume_reported; bool is_active, cant_resume_reported;
int low_stock_reported;
TMaterialCache material_cache; TMaterialCache material_cache;
@ -320,7 +330,7 @@ public:
ItemConstraint() ItemConstraint()
: is_craft(false), min_quality(item_quality::Ordinary), is_local(false), : is_craft(false), min_quality(item_quality::Ordinary), is_local(false),
weight(0), item_amount(0), item_count(0), item_inuse_amount(0), item_inuse_count(0), weight(0), item_amount(0), item_count(0), item_inuse_amount(0), item_inuse_count(0),
is_active(false), cant_resume_reported(false) is_active(false), cant_resume_reported(false), low_stock_reported(-1)
{} {}
int goalCount() { return config.ival(0); } int goalCount() { return config.ival(0); }
@ -340,6 +350,8 @@ public:
config.ival(2) &= ~1; config.ival(2) &= ~1;
} }
int curItemStock() { return goalByCount() ? item_count : item_amount; }
void init(const std::string &str) void init(const std::string &str)
{ {
config.val() = str; config.val() = str;
@ -349,7 +361,7 @@ public:
void computeRequest() void computeRequest()
{ {
int size = goalByCount() ? item_count : item_amount; int size = curItemStock();
request_resume = (size <= goalCount()-goalGap()); request_resume = (size <= goalCount()-goalGap());
request_suspend = (size >= goalCount()); request_suspend = (size >= goalCount());
} }
@ -360,36 +372,29 @@ public:
size_t history_size() { size_t history_size() {
return history.data_size() / hist_entry_size; return history.data_size() / hist_entry_size;
} }
size_t history_base(int idx) { int history_value(int idx, HistoryItem item) {
size_t hsize = history_size(); size_t hsize = history_size();
return ((history.ival(0)+hsize-idx) % hsize) * hist_entry_size; size_t base = ((history.ival(0)+1+idx) % hsize) * hist_entry_size;
} return history.get_int28(base + item*int28_size);
int history_count(int idx) {
return history.get_int28(history_base(idx) + 0*int28_size);
}
int history_amount(int idx) {
return history.get_int28(history_base(idx) + 1*int28_size);
}
int history_inuse_count(int idx) {
return history.get_int28(history_base(idx) + 2*int28_size);
}
int history_inuse_amount(int idx) {
return history.get_int28(history_base(idx) + 3*int28_size);
} }
int history_count(int idx) { return history_value(idx, HIST_COUNT); }
int history_amount(int idx) { return history_value(idx, HIST_AMOUNT); }
int history_inuse_count(int idx) { return history_value(idx, HIST_INUSE_COUNT); }
int history_inuse_amount(int idx) { return history_value(idx, HIST_INUSE_AMOUNT); }
void updateHistory() void updateHistory()
{ {
size_t buffer_size = history_size(); size_t buffer_size = history_size();
if (buffer_size < 28) if (buffer_size < MAX_HISTORY_SIZE && size_t(history.ival(0)+1) == buffer_size)
history.ensure_data(hist_entry_size*buffer_size++, hist_entry_size); history.ensure_data(hist_entry_size*++buffer_size);
history.ival(0) = (history.ival(0)+1) % buffer_size; history.ival(0) = (history.ival(0)+1) % buffer_size;
size_t base = history.ival(0) * hist_entry_size; size_t base = history.ival(0) * hist_entry_size;
history.set_int28(base + 0*int28_size, item_count); history.set_int28(base + HIST_COUNT*int28_size, item_count);
history.set_int28(base + 1*int28_size, item_amount); history.set_int28(base + HIST_AMOUNT*int28_size, item_amount);
history.set_int28(base + 2*int28_size, item_inuse_count); history.set_int28(base + HIST_INUSE_COUNT*int28_size, item_inuse_count);
history.set_int28(base + 3*int28_size, item_inuse_amount); history.set_int28(base + HIST_INUSE_AMOUNT*int28_size, item_inuse_amount);
} }
}; };
@ -1321,6 +1326,20 @@ static void update_jobs_by_constraints(color_ostream &out)
else if (ct->mat_mask.whole) else if (ct->mat_mask.whole)
info = bitfield_to_string(ct->mat_mask) + " " + info; info = bitfield_to_string(ct->mat_mask) + " " + info;
if (ct->low_stock_reported != DF_GLOBAL_VALUE(cur_season,-1))
{
int count = ct->goalCount(), gap = ct->goalGap();
if (count >= gap*3 && ct->curItemStock() < std::min(gap*2, (count-gap)/2))
{
ct->low_stock_reported = DF_GLOBAL_VALUE(cur_season,-1);
Gui::showAnnouncement("Stock level is low: " + info, COLOR_BROWN, true);
}
else
ct->low_stock_reported = -1;
}
if (is_running != ct->is_active) if (is_running != ct->is_active)
{ {
if (is_running && ct->request_resume) if (is_running && ct->request_resume)
@ -1384,6 +1403,25 @@ static void setEnabled(color_ostream &out, bool enable)
} }
} }
static void push_count_history(lua_State *L, ItemConstraint *icv)
{
size_t hsize = icv->history_size();
lua_createtable(L, hsize, 0);
for (size_t i = 0; i < hsize; i++)
{
lua_createtable(L, 0, 4);
Lua::SetField(L, icv->history_amount(i), -1, "cur_amount");
Lua::SetField(L, icv->history_count(i), -1, "cur_count");
Lua::SetField(L, icv->history_inuse_amount(i), -1, "cur_in_use_amount");
Lua::SetField(L, icv->history_inuse_count(i), -1, "cur_in_use_count");
lua_rawseti(L, -2, i+1);
}
}
static void push_constraint(lua_State *L, ItemConstraint *cv) static void push_constraint(lua_State *L, ItemConstraint *cv)
{ {
lua_newtable(L); lua_newtable(L);
@ -1430,19 +1468,31 @@ static void push_constraint(lua_State *L, ItemConstraint *cv)
lua_newtable(L); lua_newtable(L);
bool resumed = false, want_resumed = false;
for (size_t i = 0, j = 0; i < cv->jobs.size(); i++) for (size_t i = 0, j = 0; i < cv->jobs.size(); i++)
{ {
if (!cv->jobs[i]->isLive()) continue; if (!cv->jobs[i]->isLive()) continue;
Lua::PushDFObject(L, cv->jobs[i]->actual_job); Lua::PushDFObject(L, cv->jobs[i]->actual_job);
lua_rawseti(L, -2, ++j); lua_rawseti(L, -2, ++j);
if (cv->jobs[i]->want_resumed) {
want_resumed = true;
resumed = resumed || cv->jobs[i]->isActuallyResumed();
}
} }
lua_setfield(L, ctable, "jobs"); lua_setfield(L, ctable, "jobs");
if (want_resumed && !resumed)
Lua::SetField(L, true, ctable, "is_delayed");
} }
static int listConstraints(lua_State *L) static int listConstraints(lua_State *L)
{ {
lua_settop(L, 2);
auto job = Lua::CheckDFObject<df::job>(L, 1); auto job = Lua::CheckDFObject<df::job>(L, 1);
bool with_history = lua_toboolean(L, 2);
lua_pushnil(L); lua_pushnil(L);
@ -1467,6 +1517,13 @@ static int listConstraints(lua_State *L)
for (size_t i = 0; i < vec.size(); i++) for (size_t i = 0; i < vec.size(); i++)
{ {
push_constraint(L, vec[i]); push_constraint(L, vec[i]);
if (with_history)
{
push_count_history(L, vec[i]);
lua_setfield(L, -2, "history");
}
lua_rawseti(L, -2, i+1); lua_rawseti(L, -2, i+1);
} }
@ -1525,23 +1582,7 @@ static int getCountHistory(lua_State *L)
ItemConstraint *icv = get_constraint(out, token, NULL, false); ItemConstraint *icv = get_constraint(out, token, NULL, false);
if (icv) if (icv)
{ push_count_history(L, icv);
size_t hsize = icv->history_size();
lua_createtable(L, hsize, 0);
for (int i = hsize-1; i >= 0; i--)
{
lua_createtable(L, 0, 4);
Lua::SetField(L, icv->history_amount(i), -1, "cur_amount");
Lua::SetField(L, icv->history_count(i), -1, "cur_count");
Lua::SetField(L, icv->history_inuse_amount(i), -1, "cur_in_use_amount");
Lua::SetField(L, icv->history_inuse_count(i), -1, "cur_in_use_count");
lua_rawseti(L, -2, hsize-i); // reverse order
}
}
else else
lua_pushnil(L); lua_pushnil(L);

@ -17,18 +17,30 @@ class AutoFarm
@thresholds.default = v.to_i @thresholds.default = v.to_i
end end
def is_plantable(plant) def is_plantable (plant)
has_seed = plant.flags[:SEED]
season = df.cur_season season = df.cur_season
harvest = df.cur_season_tick + plant.growdur * 10 harvest = df.cur_season_tick + plant.growdur * 10
will_finish = harvest < 10080 will_finish = harvest < 10080
can_plant = plant.flags[season] can_plant = has_seed && plant.flags[season]
can_plant = can_plant && (will_finish || plant.flags[(season+1)%4]) can_plant = can_plant && (will_finish || plant.flags[(season+1)%4])
can_plant can_plant
end end
def find_plantable_plants def find_plantable_plants
plantable = {} plantable = {}
for i in 0..df.ui.tasks.known_plants.length-1 counts = Hash.new(0)
df.world.items.other[:SEEDS].each { |i|
if (!i.flags.dump && !i.flags.forbid && !i.flags.garbage_collect &&
!i.flags.hostile && !i.flags.on_fire && !i.flags.rotten &&
!i.flags.trader && !i.flags.in_building && !i.flags.construction &&
!i.flags.artifact)
counts[i.mat_index] = counts[i.mat_index] + i.stack_size
end
}
counts.keys.each { |i|
if df.ui.tasks.known_plants[i] if df.ui.tasks.known_plants[i]
plant = df.world.raws.plants.all[i] plant = df.world.raws.plants.all[i]
if is_plantable(plant) if is_plantable(plant)
@ -36,7 +48,8 @@ class AutoFarm
plantable[i] = :Underground if (plant.underground_depth_min > 0 || plant.underground_depth_max > 0) plantable[i] = :Underground if (plant.underground_depth_min > 0 || plant.underground_depth_max > 0)
end end
end end
end }
return plantable return plantable
end end

@ -11,23 +11,37 @@ def display_death_event(e)
end end
item = df.item_find(:selected) item = df.item_find(:selected)
unit = df.unit_find(:selected)
if !item or !item.kind_of?(DFHack::ItemBodyComponent) if !item or !item.kind_of?(DFHack::ItemBodyComponent)
item = df.world.items.other[:ANY_CORPSE].find { |i| df.at_cursor?(i) } item = df.world.items.other[:ANY_CORPSE].find { |i| df.at_cursor?(i) }
end end
if !item or !item.kind_of?(DFHack::ItemBodyComponent) if item and item.kind_of?(DFHack::ItemBodyComponent)
puts "Please select a corpse in the loo'k' menu"
else
hf = item.hist_figure_id hf = item.hist_figure_id
if hf == -1 elsif unit
hf = unit.hist_figure_id
end
if not hf
puts "Please select a corpse in the loo'k' menu, or an unit in the 'u'nitlist screen"
elsif hf == -1
# TODO try to retrieve info from the unit (u = item.unit_tg) # TODO try to retrieve info from the unit (u = item.unit_tg)
puts "Not a historical figure, cannot death find info" puts "Not a historical figure, cannot death find info"
else
histfig = df.world.history.figures.binsearch(hf)
unit = histfig ? df.unit_find(histfig.unit_id) : nil
if unit and not unit.flags1.dead and not unit.flags3.ghostly
puts "#{unit.name} is not dead yet !"
else else
events = df.world.history.events events = df.world.history.events
(0...events.length).reverse_each { |i| (0...events.length).reverse_each { |i|
if events[i].kind_of?(DFHack::HistoryEventHistFigureDiedst) and events[i].victim_hf == hf e = events[i]
display_death_event(events[i]) if e.kind_of?(DFHack::HistoryEventHistFigureDiedst) and e.victim_hf == hf
display_death_event(e)
break break
end end
} }

@ -0,0 +1,103 @@
-- Read the tiles from the screen and display info about them.
local utils = require 'utils'
local gui = require 'gui'
InspectScreen = defclass(InspectScreen, gui.Screen)
function InspectScreen:init(args)
local w,h = dfhack.screen.getWindowSize()
self.cursor_x = math.floor(w/2)
self.cursor_y = math.floor(h/2)
end
function InspectScreen:computeFrame(parent_rect)
local sw, sh = parent_rect.width, parent_rect.height
self.cursor_x = math.max(0, math.min(self.cursor_x, sw-1))
self.cursor_y = math.max(0, math.min(self.cursor_y, sh-1))
local frame = { w = 14, r = 1, h = 10, t = 1 }
if self.cursor_x > sw/2 then
frame = { w = 14, l = 1, h = 10, t = 1 }
end
return gui.compute_frame_body(sw, sh, frame, 1, 0, false)
end
function InspectScreen:onRenderFrame(dc, rect)
self:renderParent()
self.cursor_pen = dfhack.screen.readTile(self.cursor_x, self.cursor_y)
if gui.blink_visible(100) then
dfhack.screen.paintTile({ch='X',fg=COLOR_LIGHTGREEN}, self.cursor_x, self.cursor_y)
end
dc:fill(rect, {ch=' ',fg=COLOR_WHITE,bg=COLOR_CYAN})
end
local FG_PEN = {fg=COLOR_WHITE,bg=COLOR_BLACK,tile_color=true}
local BG_PEN = {fg=COLOR_BLACK,bg=COLOR_WHITE,tile_color=true}
local TXT_PEN = {fg=COLOR_WHITE}
function InspectScreen:onRenderBody(dc)
dc:pen(COLOR_WHITE, COLOR_CYAN)
if self.cursor_pen then
local info = self.cursor_pen
dc:string('CH: '):char(info.ch, FG_PEN):char(info.ch, BG_PEN):string(' '):string(''..info.ch,TXT_PEN):newline()
local fgcolor = info.fg
local fgstr = info.fg
if info.bold then
fgcolor = (fgcolor+8)%16
fgstr = fgstr..'+8'
end
dc:string('FG: '):string('NN',{fg=fgcolor}):string(' '):string(''..fgstr,TXT_PEN)
dc:seek(dc.width-1):char(info.ch,{fg=info.fg,bold=info.bold}):newline()
dc:string('BG: '):string('NN',{fg=info.bg}):string(' '):string(''..info.bg,TXT_PEN)
dc:seek(dc.width-1):char(info.ch,{fg=COLOR_BLACK,bg=info.bg}):newline()
local bstring = 'false'
if info.bold then bstring = 'true' end
dc:string('Bold: '..bstring):newline():newline()
if info.tile and gui.USE_GRAPHICS then
dc:string('TL: '):tile(' ', info.tile, FG_PEN):tile(' ', info.tile, BG_PEN):string(' '..info.tile):newline()
if info.tile_color then
dc:string('Color: true')
elseif info.tile_fg then
dc:string('FG: '):string('NN',{fg=info.tile_fg}):string(' '):string(''..info.tile_fg,TXT_PEN):newline()
dc:string('BG: '):string('NN',{fg=info.tile_bg}):string(' '):string(''..info.tile_bg,TXT_PEN):newline()
end
end
else
dc:string('Invalid', COLOR_LIGHTRED)
end
end
local MOVEMENT_KEYS = {
CURSOR_UP = { 0, -1, 0 }, CURSOR_DOWN = { 0, 1, 0 },
CURSOR_LEFT = { -1, 0, 0 }, CURSOR_RIGHT = { 1, 0, 0 },
CURSOR_UPLEFT = { -1, -1, 0 }, CURSOR_UPRIGHT = { 1, -1, 0 },
CURSOR_DOWNLEFT = { -1, 1, 0 }, CURSOR_DOWNRIGHT = { 1, 1, 0 },
CURSOR_UP_FAST = { 0, -1, 0, true }, CURSOR_DOWN_FAST = { 0, 1, 0, true },
CURSOR_LEFT_FAST = { -1, 0, 0, true }, CURSOR_RIGHT_FAST = { 1, 0, 0, true },
CURSOR_UPLEFT_FAST = { -1, -1, 0, true }, CURSOR_UPRIGHT_FAST = { 1, -1, 0, true },
CURSOR_DOWNLEFT_FAST = { -1, 1, 0, true }, CURSOR_DOWNRIGHT_FAST = { 1, 1, 0, true },
}
function InspectScreen:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
else
for k,v in pairs(MOVEMENT_KEYS) do
if keys[k] then
local delta = 1
if v[4] then
delta = 10
end
self.cursor_x = self.cursor_x + delta*v[1]
self.cursor_y = self.cursor_y + delta*v[2]
self:updateLayout()
return
end
end
end
end
InspectScreen{}:show()

@ -66,32 +66,84 @@ function is_caste_mat(iobj)
end end
function describe_material(iobj) function describe_material(iobj)
local matline = 'any material' local matflags = utils.list_bitfield_flags(iobj.mat_mask)
if #matflags > 0 then
matflags = 'any '..table.concat(matflags, '/')
else
matflags = nil
end
if is_caste_mat(iobj) then if is_caste_mat(iobj) then
matline = 'no material' return 'no material'
elseif (iobj.mat_type or -1) >= 0 then elseif (iobj.mat_type or -1) >= 0 then
local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index) local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index)
local matline
if info then if info then
matline = info:toString() matline = info:toString()
else else
matline = iobj.mat_type..':'..iobj.mat_index matline = iobj.mat_type..':'..iobj.mat_index
end end
return matline, matflags
else
return matflags or 'any material'
end
end
function current_stock(iobj)
if iobj.goal_by_count then
return iobj.cur_count
else
return iobj.cur_amount
end
end
function if_by_count(iobj,bc,ba)
if iobj.goal_by_count then
return bc
else
return ba
end
end
function compute_trend(history,field)
local count = #history
if count == 0 then
return 0
end end
return matline local sumX,sumY,sumXY,sumXX = 0,0,0,0
for i,v in ipairs(history) do
sumX = sumX + i
sumY = sumY + v[field]
sumXY = sumXY + i*v[field]
sumXX = sumXX + i*i
end
return (count * sumXY - sumX * sumY) / (count * sumXX - sumX * sumX)
end end
------------------------
-- RANGE EDITOR GROUP --
------------------------
local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false } local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false }
RangeEditor = defclass(RangeEditor, widgets.Label) RangeEditor = defclass(RangeEditor, widgets.Label)
RangeEditor.ATTRS { RangeEditor.ATTRS {
get_cb = DEFAULT_NIL, get_cb = DEFAULT_NIL,
save_cb = DEFAULT_NIL save_cb = DEFAULT_NIL,
keys = {
count = 'CUSTOM_SHIFT_I',
modify = 'CUSTOM_SHIFT_R',
min_dec = 'BUILDING_TRIGGER_MIN_SIZE_DOWN',
min_inc = 'BUILDING_TRIGGER_MIN_SIZE_UP',
max_dec = 'BUILDING_TRIGGER_MAX_SIZE_DOWN',
max_inc = 'BUILDING_TRIGGER_MAX_SIZE_UP',
}
} }
function RangeEditor:init(args) function RangeEditor:init(args)
self:setText{ self:setText{
{ key = 'BUILDING_TRIGGER_ENABLE_CREATURE', { key = self.keys.count,
text = function() text = function()
local cons = self.get_cb() or null_cons local cons = self.get_cb() or null_cons
if cons.goal_by_count then if cons.goal_by_count then
@ -101,21 +153,21 @@ function RangeEditor:init(args)
end end
end, end,
on_activate = self:callback('onChangeUnit') }, on_activate = self:callback('onChangeUnit') },
{ key = 'BUILDING_TRIGGER_ENABLE_MAGMA', text = ': Modify', { key = self.keys.modify, text = ': Range',
on_activate = self:callback('onEditRange') }, on_activate = self:callback('onEditRange') },
NEWLINE, ' ', NEWLINE, ' ',
{ key = 'BUILDING_TRIGGER_MIN_SIZE_DOWN', { key = self.keys.min_dec,
on_activate = self:callback('onIncRange', 'goal_gap', 5) }, on_activate = self:callback('onIncRange', 'goal_gap', 2) },
{ key = 'BUILDING_TRIGGER_MIN_SIZE_UP', { key = self.keys.min_inc,
on_activate = self:callback('onIncRange', 'goal_gap', -1) }, on_activate = self:callback('onIncRange', 'goal_gap', -1) },
{ text = function() { text = function()
local cons = self.get_cb() or null_cons local cons = self.get_cb() or null_cons
return string.format(': Min %-4d ', cons.goal_value - cons.goal_gap) return string.format(': Min %-4d ', cons.goal_value - cons.goal_gap)
end }, end },
{ key = 'BUILDING_TRIGGER_MAX_SIZE_DOWN', { key = self.keys.max_dec,
on_activate = self:callback('onIncRange', 'goal_value', -1) }, on_activate = self:callback('onIncRange', 'goal_value', -1) },
{ key = 'BUILDING_TRIGGER_MAX_SIZE_UP', { key = self.keys.max_inc,
on_activate = self:callback('onIncRange', 'goal_value', 5) }, on_activate = self:callback('onIncRange', 'goal_value', 2) },
{ text = function() { text = function()
local cons = self.get_cb() or null_cons local cons = self.get_cb() or null_cons
return string.format(': Max %-4d', cons.goal_value) return string.format(': Max %-4d', cons.goal_value)
@ -156,12 +208,16 @@ end
function RangeEditor:onIncRange(field, delta) function RangeEditor:onIncRange(field, delta)
local cons = self.get_cb() local cons = self.get_cb()
if not cons.goal_by_count then if not cons.goal_by_count then
delta = delta * 5 delta = delta * 2
end end
cons[field] = math.max(1, cons[field] + delta) cons[field] = math.max(1, cons[field] + delta*5)
self.save_cb(cons) self.save_cb(cons)
end end
---------------------------
-- NEW CONSTRAINT DIALOG --
---------------------------
NewConstraint = defclass(NewConstraint, gui.FramedScreen) NewConstraint = defclass(NewConstraint, gui.FramedScreen)
NewConstraint.focus_path = 'workflow/new' NewConstraint.focus_path = 'workflow/new'
@ -177,7 +233,7 @@ NewConstraint.ATTRS {
} }
function NewConstraint:init(args) function NewConstraint:init(args)
self.constraint = args.constraint or {} self.constraint = args.constraint or { item_type = -1 }
rawset_default(self.constraint, { goal_value = 10, goal_gap = 5, goal_by_count = false }) rawset_default(self.constraint, { goal_value = 10, goal_gap = 5, goal_by_count = false })
local matlist = {} local matlist = {}
@ -202,8 +258,16 @@ function NewConstraint:init(args)
frame = { l = 1, t = 2, w = 26 }, frame = { l = 1, t = 2, w = 26 },
text = { text = {
'Type: ', 'Type: ',
{ pen = COLOR_LIGHTCYAN, { pen = function()
text = function() return describe_item_type(self.constraint) end }, if self:isValid() then return COLOR_LIGHTCYAN else return COLOR_LIGHTRED end
end,
text = function()
if self:isValid() then
return describe_item_type(self.constraint)
else
return 'item not set'
end
end },
NEWLINE, ' ', NEWLINE, ' ',
{ key = 'CUSTOM_T', text = ': Select, ', { key = 'CUSTOM_T', text = ': Select, ',
on_activate = self:callback('chooseType') }, on_activate = self:callback('chooseType') },
@ -239,7 +303,7 @@ function NewConstraint:init(args)
} }
}, },
widgets.Label{ widgets.Label{
frame = { l = 0, t = 13 }, frame = { l = 0, t = 14 },
text = { text = {
'Desired range: ', 'Desired range: ',
{ pen = COLOR_LIGHTCYAN, { pen = COLOR_LIGHTCYAN,
@ -255,7 +319,7 @@ function NewConstraint:init(args)
} }
}, },
RangeEditor{ RangeEditor{
frame = { l = 1, t = 15 }, frame = { l = 1, t = 16 },
get_cb = self:cb_getfield('constraint'), get_cb = self:cb_getfield('constraint'),
save_cb = self:callback('onRangeChange'), save_cb = self:callback('onRangeChange'),
}, },
@ -277,6 +341,7 @@ function NewConstraint:init(args)
{ key = 'LEAVESCREEN', text = ': Cancel, ', { key = 'LEAVESCREEN', text = ': Cancel, ',
on_activate = self:callback('dismiss') }, on_activate = self:callback('dismiss') },
{ key = 'MENU_CONFIRM', key_sep = ': ', { key = 'MENU_CONFIRM', key_sep = ': ',
enabled = self:callback('isValid'),
text = function() text = function()
if self.is_existing then return 'Update' else return 'Create new' end if self.is_existing then return 'Update' else return 'Create new' end
end, end,
@ -295,9 +360,17 @@ function NewConstraint:postinit()
self:onChange() self:onChange()
end end
function NewConstraint:isValid()
return self.constraint.item_type >= 0 or self.constraint.is_craft
end
function NewConstraint:onChange() function NewConstraint:onChange()
local token = workflow.constraintToToken(self.constraint) local token = workflow.constraintToToken(self.constraint)
local out = workflow.findConstraint(token) local out
if self:isValid() then
out = workflow.findConstraint(token)
end
if out then if out then
self.constraint = out self.constraint = out
@ -390,6 +463,364 @@ function NewConstraint:onRangeChange()
cons.goal_gap = math.max(1, math.min(cons.goal_gap, cons.goal_value-1)) cons.goal_gap = math.max(1, math.min(cons.goal_gap, cons.goal_value-1))
end end
------------------------------
-- CONSTRAINT HISTORY GRAPH --
------------------------------
HistoryGraph = defclass(HistoryGraph, widgets.Widget)
HistoryGraph.ATTRS {
frame_inset = 1,
history_pen = COLOR_CYAN,
}
function HistoryGraph:init(info)
end
function HistoryGraph:setData(history, bars)
self.history = history or {}
self.bars = bars or {}
local maxval = 1
for i,v in ipairs(self.history) do
maxval = math.max(maxval, v)
end
for i,v in ipairs(self.bars) do
maxval = math.max(maxval, v.value)
end
self.max_value = maxval
end
function HistoryGraph:onRenderFrame(dc,rect)
dc:fill(rect.x1,rect.y1,rect.x1,rect.y2,{ch='\xb3', fg=COLOR_BROWN})
dc:fill(rect.x1,rect.y2,rect.x2,rect.y2,{ch='\xc4', fg=COLOR_BROWN})
dc:seek(rect.x1,rect.y1):char('\x1e', COLOR_BROWN)
dc:seek(rect.x1,rect.y2):char('\xc5', COLOR_BROWN)
dc:seek(rect.x2,rect.y2):char('\x10', COLOR_BROWN)
dc:seek(rect.x1,rect.y2-1):char('0', COLOR_BROWN)
end
function HistoryGraph:onRenderBody(dc)
local coeff = (dc.height-1)/self.max_value
for i,v in ipairs(self.bars) do
local y = dc.height-1-math.floor(0.5 + coeff*v.value)
dc:fill(0,y,dc.width-1,y,v.pen or {ch='-', fg=COLOR_GREEN})
end
local xbase = dc.width-1-#self.history
for i,v in ipairs(self.history) do
local x = xbase + i
local y = dc.height-1-math.floor(0.5 + coeff*v)
dc:seek(x,y):char('*', self.history_pen)
end
end
------------------------------
-- GLOBAL CONSTRAINT SCREEN --
------------------------------
ConstraintList = defclass(ConstraintList, gui.FramedScreen)
ConstraintList.focus_path = 'workflow/list'
ConstraintList.ATTRS {
frame_title = 'Workflow Status',
frame_inset = 0,
frame_background = COLOR_BLACK,
frame_style = gui.BOUNDARY_FRAME,
}
function ConstraintList:init(args)
local fwidth_cb = self:cb_getfield('fwidth')
self.fwidth = 20
self.sort_by_severity = false
self:addviews{
widgets.Panel{
frame = { l = 0, r = 31 },
frame_inset = 1,
on_layout = function(body)
self.fwidth = body.width - (12+1+1+7+1+1+1+7)
end,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
text_pen = COLOR_CYAN,
text = {
{ text = 'Item', width = 12 }, ' ',
{ text = 'Material etc', width = fwidth_cb }, ' ',
{ text = 'Stock / Limit' },
}
},
widgets.FilteredList{
view_id = 'list',
frame = { t = 2, b = 2 },
edit_below = true,
not_found_label = 'No matching constraints',
edit_pen = COLOR_LIGHTCYAN,
text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK },
cursor_pen = { fg = COLOR_WHITE, bg = COLOR_GREEN },
on_select = self:callback('onSelectConstraint'),
},
widgets.Label{
frame = { b = 0, h = 1 },
text = {
{ key = 'CUSTOM_SHIFT_A', text = ': Add',
on_activate = self:callback('onNewConstraint') }, ', ',
{ key = 'CUSTOM_SHIFT_X', text = ': Delete',
on_activate = self:callback('onDeleteConstraint') }, ', ',
{ key = 'CUSTOM_SHIFT_O', text = ': Severity Order',
on_activate = self:callback('onSwitchSort'),
pen = function()
if self.sort_by_severity then
return COLOR_LIGHTCYAN
else
return COLOR_WHITE
end
end },
}
}
}
},
widgets.Panel{
frame = { w = 30, r = 0, h = 6, t = 0 },
frame_inset = 1,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
enabled = self:callback('isAnySelected'),
text = {
{ text = function()
local cur = self:getCurConstraint()
if cur then
return string.format(
'Currently %d (%d in use)',
current_stock(cur),
if_by_count(cur, cur.cur_in_use_count, cur.cur_in_use_amount)
)
else
return 'No constraint selected'
end
end }
}
},
RangeEditor{
frame = { l = 0, t = 2 },
enabled = self:callback('isAnySelected'),
get_cb = self:callback('getCurConstraint'),
save_cb = self:callback('saveConstraint'),
keys = {
count = 'CUSTOM_SHIFT_I',
modify = 'CUSTOM_SHIFT_R',
min_dec = 'SECONDSCROLL_PAGEUP',
min_inc = 'SECONDSCROLL_PAGEDOWN',
max_dec = 'SECONDSCROLL_UP',
max_inc = 'SECONDSCROLL_DOWN',
}
},
}
},
widgets.Widget{
active = false,
frame = { w = 1, r = 30 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
widgets.Widget{
active = false,
frame = { w = 30, r = 0, h = 1, t = 6 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
HistoryGraph{
view_id = 'graph',
frame = { w = 30, r = 0, t = 7, b = 0 },
}
}
self:initListChoices(nil, args.select_token)
end
function stock_trend_color(cons)
local stock = current_stock(cons)
if stock >= cons.goal_value - cons.goal_gap then
return COLOR_LIGHTGREEN, 0
elseif stock <= cons.goal_gap then
return COLOR_LIGHTRED, 4
elseif stock >= cons.goal_value - 2*cons.goal_gap then
return COLOR_GREEN, 1
elseif stock <= 2*cons.goal_gap then
return COLOR_RED, 3
else
local trend = if_by_count(cons, cons.trend_count, cons.trend_amount)
if trend > 0.3 then
return COLOR_GREEN, 1
elseif trend < -0.3 then
return COLOR_RED, 3
else
return COLOR_GREY, 2
end
end
end
function ConstraintList:initListChoices(clist, sel_token)
clist = clist or workflow.listConstraints(nil, true)
local fwidth_cb = self:cb_getfield('fwidth')
local choices = {}
for i,cons in ipairs(clist) do
cons.trend_count = compute_trend(cons.history, 'cur_count')
cons.trend_amount = compute_trend(cons.history, 'cur_amount')
local itemstr = describe_item_type(cons)
local matstr,matflagstr = describe_material(cons)
if matflagstr then
matstr = matflagstr .. ' ' .. matstr
end
if cons.min_quality > 0 or cons.is_local then
local lst = {}
if cons.is_local then
table.insert(lst, 'local')
end
if cons.min_quality > 0 then
table.insert(lst, string.lower(df.item_quality[cons.min_quality]))
end
matstr = matstr .. ' ('..table.concat(lst,',')..')'
end
local goal_color = COLOR_GREY
if #cons.jobs == 0 then
goal_color = COLOR_RED
elseif cons.is_delayed then
goal_color = COLOR_YELLOW
end
table.insert(choices, {
text = {
{ text = itemstr, width = 12, pad_char = ' ' }, ' ',
{ text = matstr, width = fwidth_cb, pad_char = ' ' }, ' ',
{ text = curry(current_stock,cons), width = 7, rjustify = true,
pen = function() return { fg = stock_trend_color(cons) } end },
{ text = curry(if_by_count,cons,'S','I'), gap = 1,
pen = { fg = COLOR_GREY } },
{ text = function() return cons.goal_value end, gap = 1,
pen = { fg = goal_color } }
},
severity = select(2, stock_trend_color(cons)),
search_key = itemstr .. ' | ' .. matstr,
token = cons.token,
obj = cons
})
end
self:setChoices(choices, sel_token)
end
function ConstraintList:isAnySelected()
return self.subviews.list:getSelected() ~= nil
end
function ConstraintList:getCurConstraint()
local selidx,selobj = self.subviews.list:getSelected()
if selobj then return selobj.obj end
end
function ConstraintList:onSwitchSort()
self.sort_by_severity = not self.sort_by_severity
self:setChoices(self.subviews.list:getChoices())
end
function ConstraintList:setChoices(choices, sel_token)
if self.sort_by_severity then
table.sort(choices, function(a,b)
return a.severity > b.severity
or (a.severity == b.severity and
current_stock(a.obj)/a.obj.goal_value < current_stock(b.obj)/b.obj.goal_value)
end)
else
table.sort(choices, function(a,b) return a.search_key < b.search_key end)
end
local selidx = nil
if sel_token then
selidx = utils.linear_index(choices, sel_token, 'token')
end
local list = self.subviews.list
local filter = list:getFilter()
list:setChoices(choices, selidx)
if filter ~= '' then
list:setFilter(filter, selidx)
if selidx and list:getSelected() ~= selidx then
list:setFilter('', selidx)
end
end
end
function ConstraintList:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
else
ConstraintList.super.onInput(self, keys)
end
end
function ConstraintList:onNewConstraint()
NewConstraint{
on_submit = self:callback('saveConstraint')
}:show()
end
function ConstraintList:saveConstraint(cons)
local out = workflow.setConstraint(cons.token, cons.goal_by_count, cons.goal_value, cons.goal_gap)
self:initListChoices(nil, out.token)
end
function ConstraintList:onDeleteConstraint()
local cons = self:getCurConstraint()
dlg.showYesNoPrompt(
'Delete Constraint',
'Really delete the current constraint?',
COLOR_YELLOW,
function()
workflow.deleteConstraint(cons.token)
self:initListChoices()
end
)
end
function ConstraintList:onSelectConstraint(idx,item)
local history, bars
if item then
local cons = item.obj
local vfield = if_by_count(cons, 'cur_count', 'cur_amount')
bars = {
{ value = cons.goal_value - cons.goal_gap, pen = {ch='-', fg=COLOR_GREEN} },
{ value = cons.goal_value, pen = {ch='-', fg=COLOR_LIGHTGREEN} },
}
history = {}
for i,v in ipairs(cons.history or {}) do
table.insert(history, v[vfield])
end
table.insert(history, cons[vfield])
end
self.subviews.graph:setData(history, bars)
end
-------------------------------
-- WORKSHOP JOB INFO OVERLAY --
-------------------------------
JobConstraints = defclass(JobConstraints, guidm.MenuOverlay) JobConstraints = defclass(JobConstraints, guidm.MenuOverlay)
JobConstraints.focus_path = 'workflow/job' JobConstraints.focus_path = 'workflow/job'
@ -425,14 +856,20 @@ function JobConstraints:init(args)
widgets.Label{ widgets.Label{
frame = { l = 0, b = 0 }, frame = { l = 0, b = 0 },
text = { text = {
{ key = 'CUSTOM_N', text = ': New limit, ', { key = 'CUSTOM_SHIFT_A', text = ': Add limit, ',
on_activate = self:callback('onNewConstraint') }, on_activate = self:callback('onNewConstraint') },
{ key = 'CUSTOM_X', text = ': Delete', { key = 'CUSTOM_SHIFT_X', text = ': Delete',
enabled = self:callback('isAnySelected'), enabled = self:callback('isAnySelected'),
on_activate = self:callback('onDeleteConstraint') }, on_activate = self:callback('onDeleteConstraint') },
NEWLINE, NEWLINE, NEWLINE, NEWLINE,
{ key = 'LEAVESCREEN', text = ': Back', { key = 'LEAVESCREEN', text = ': Back',
on_activate = self:callback('dismiss') } on_activate = self:callback('dismiss') },
' ',
{ key = 'CUSTOM_SHIFT_S', text = ': Status',
on_activate = function()
local sel = self:getCurConstraint()
ConstraintList{ select_token = (sel or {}).token }:show()
end }
} }
}, },
} }
@ -480,24 +917,12 @@ function JobConstraints:initListChoices(clist, sel_token)
end end
itemstr = itemstr .. ' ('..table.concat(lst,',')..')' itemstr = itemstr .. ' ('..table.concat(lst,',')..')'
end end
local matstr = describe_material(cons) local matstr,matflagstr = describe_material(cons)
local matflagstr = ''
local matflags = utils.list_bitfield_flags(cons.mat_mask)
if #matflags > 0 then
matflags[1] = 'any '..matflags[1]
if matstr == 'any material' then
matstr = table.concat(matflags, ', ')
matflags = {}
end
end
if #matflags > 0 then
matflagstr = table.concat(matflags, ', ')
end
table.insert(choices, { table.insert(choices, {
text = { text = {
goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE, goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE,
' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', matflagstr ' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', (matflagstr or '')
}, },
token = cons.token, token = cons.token,
obj = cons obj = cons
@ -538,32 +963,30 @@ function JobConstraints:onNewConstraint()
local choices = {} local choices = {}
for i,cons in ipairs(variants) do for i,cons in ipairs(variants) do
local itemstr = describe_item_type(cons) local itemstr = describe_item_type(cons)
local matstr = describe_material(cons) local matstr,matflags = describe_material(cons)
local matflags = utils.list_bitfield_flags(cons.mat_mask) if matflags then
if #matflags > 0 then matstr = matflags..' '..matstr
local fstr = table.concat(matflags, '/')
if matstr == 'any material' then
matstr = 'any '..fstr
else
matstr = 'any '..fstr..' '..matstr
end
end end
table.insert(choices, { text = itemstr..' of '..matstr, obj = cons }) table.insert(choices, { text = itemstr..' of '..matstr, obj = cons })
end end
dlg.showListPrompt( dlg.ListBox{
'New limit', frame_title = 'Add limit',
'Select one of the possible outputs:', text = 'Select one of the possible outputs:',
COLOR_WHITE, text_pen = COLOR_WHITE,
choices, choices = choices,
function(idx,item) on_select = function(idx,item)
self:saveConstraint(item.obj)
end,
select2_hint = 'Advanced',
on_select2 = function(idx,item)
NewConstraint{ NewConstraint{
constraint = item.obj, constraint = item.obj,
on_submit = self:callback('saveConstraint') on_submit = self:callback('saveConstraint')
}:show() }:show()
end end,
) }:show()
end end
function JobConstraints:onDeleteConstraint() function JobConstraints:onDeleteConstraint()
@ -589,13 +1012,18 @@ function JobConstraints:onInput(keys)
end end
end end
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then local args = {...}
if args[1] == 'status' then
check_enabled(function() ConstraintList{}:show() end)
else
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then
qerror("This script requires a workshop job selected in the 'q' mode") qerror("This script requires a workshop job selected in the 'q' mode")
end end
local job = dfhack.gui.getSelectedJob() local job = dfhack.gui.getSelectedJob()
check_enabled(function() check_enabled(function()
check_repeat(job, function() check_repeat(job, function()
local clist = workflow.listConstraints(job) local clist = workflow.listConstraints(job)
if not clist then if not clist then
@ -604,5 +1032,5 @@ check_enabled(function()
end end
JobConstraints{ job = job, clist = clist }:show() JobConstraints{ job = job, clist = clist }:show()
end) end)
end) end)
end

@ -47,9 +47,12 @@ def lever_descr(bld, idx=nil)
}.flatten.each { |r| }.flatten.each { |r|
# linked building description # linked building description
tg = r.building_tg tg = r.building_tg
state = tg.gate_flags.closed ? 'closed' : 'opened' state = ''
state << ', closing' if tg.gate_flags.closing if tg.respond_to?(:gate_flags)
state << ', opening' if tg.gate_flags.opening state << (tg.gate_flags.closed ? 'closed' : 'opened')
state << ", closing (#{tg.timer})" if tg.gate_flags.closing
state << ", opening (#{tg.timer})" if tg.gate_flags.opening
end
ret << (descr + " linked to #{tg._rtti_classname} ##{tg.id} @[#{tg.centerx}, #{tg.centery}, #{tg.z}] #{state}") ret << (descr + " linked to #{tg._rtti_classname} ##{tg.id} @[#{tg.centerx}, #{tg.centery}, #{tg.z}] #{state}")

@ -0,0 +1,26 @@
-- On map load writes the current season to gamelog.txt
local seasons = {
[0] = 'Spring',
[1] = 'Summer',
[2] = 'Autumn',
[3] = 'Winter',
}
local args = {...}
local function write_gamelog(msg)
local log = io.open('gamelog.txt', 'a')
log:write(msg.."\n")
log:close()
end
if args[1] == 'disable' then
dfhack.onStateChange[_ENV] = nil
else
dfhack.onStateChange[_ENV] = function(op)
if op == SC_WORLD_LOADED then
write_gamelog(seasons[df.global.cur_season]..' has arrived on the calendar.')
end
end
end