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>
<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>
</dl>
<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>
<p>Specifies a pen to paint as one tile before the main part of the token.</p>
</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>
<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>
@ -2848,11 +2859,16 @@ this may be extended with mouse click support.</p>
</tr>
<tr class="field"><th class="field-name">icon_pen:</th><td class="field-body">Default pen for icons.</td>
</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 class="field"><th class="field-name">on_submit:</th><td class="field-body">Enter key callback; if specified, the list reacts to the key
and calls it as <tt class="docutils literal">on_submit(index,choice)</tt>.</td>
</tr>
<tr class="field"><th class="field-name">on_submit2:</th><td class="field-body">Shift-Enter key callback; if specified, the list reacts to the key
and calls it as <tt class="docutils literal">on_submit2(index,choice)</tt>.</td>
</tr>
<tr class="field"><th class="field-name">row_height:</th><td class="field-body">Height of every row in text lines.</td>
</tr>
<tr class="field"><th class="field-name">icon_width:</th><td class="field-body">If not <em>nil</em>, the specified number of character columns
@ -2908,6 +2924,9 @@ with the following fields:</p>
<li><p class="first"><tt class="docutils literal">list:submit()</tt></p>
<p>Call the <tt class="docutils literal">on_submit</tt> callback, as if the Enter key was handled.</p>
</li>
<li><p class="first"><tt class="docutils literal">list:submit2()</tt></p>
<p>Call the <tt class="docutils literal">on_submit2</tt> callback, as if the Shift-Enter key was handled.</p>
</li>
</ul>
</div>
<div class="section" id="filteredlist-class">
@ -2922,6 +2941,8 @@ supports:</p>
<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>
<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"><td>&nbsp;</td><td class="field-body">Specifies the text of the label shown when no items match the filter.</td>
</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.
``_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.
@ -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.
* ``token.width = ...``
If specified either as a value or a callback, the text field is padded
or truncated to the specified number.
* ``token.pad_char = '?'``
If specified together with ``width``, the padding area is filled with
this character instead of just being skipped over.
* ``token.key = '...'``
Specifies the keycode associated with the token. The string description
@ -2775,8 +2788,12 @@ It has the following attributes:
:inactive_pen: If specified, used for the cursor when the widget is not active.
:icon_pen: Default pen for icons.
:on_select: Selection change callback; called as ``on_select(index,choice)``.
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
and calls it as ``on_submit(index,choice)``.
:on_submit2: Shift-Enter key callback; if specified, the list reacts to the key
and calls it as ``on_submit2(index,choice)``.
:row_height: Height of every row in text lines.
:icon_width: If not *nil*, the specified number of character columns
are reserved to the left of the list item for the icons.
@ -2826,6 +2843,10 @@ The list supports the following methods:
Call the ``on_submit`` callback, as if the Enter key was handled.
* ``list:submit2()``
Call the ``on_submit2`` callback, as if the Shift-Enter key was handled.
FilteredList class
------------------
@ -2836,6 +2857,7 @@ In addition to passing through all attributes supported by List, it
supports:
:edit_pen: If specified, used instead of ``cursor_pen`` for the edit field.
:edit_below: If true, the edit field is placed below the list instead of above.
:not_found_label: Specifies the text of the label shown when no items match the filter.
The list choices may include the following attributes:
@ -2932,6 +2954,36 @@ sort
Does not export any native functions as of now. Instead, it
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

23
NEWS

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

@ -2873,7 +2873,7 @@ directly to the main dwarf mode screen.</p>
</div>
<div class="section" id="search">
<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" />
<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').
@ -2890,6 +2890,13 @@ filter).</p>
are actually visible in the list; the same effect applies to the Trade
Value numbers displayed by the screen. Because of this, pressing the 't'
key while search is active clears the search instead of executing the trade.</p>
<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 class="section" id="automaterial">
<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
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>
<p>Pressing 'c' switches the current constraint between counting stacks or items.
Pressing 'm' 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
items and expanding the range each gives a 5x bonus).</p>
<p>Pressing 'n' produces a list of possible outputs of this job as guessed by
<p>Pressing 'I' switches the current constraint between counting stacks or items.
Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting
items and expanding the range each gives a 2x bonus).</p>
<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
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>,
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>
<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
suit your need, and set the item count range.</p>
<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 class="section" id="gui-assign-rack">
<h2><a class="toc-backref" href="#id146">gui/assign-rack</a></h2>

@ -2074,7 +2074,7 @@ directly to the main dwarf mode screen.
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
@ -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'
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
============
@ -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
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 'm' 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
items and expanding the range each gives a 5x bonus).
Pressing 'I' switches the current constraint between counting stacks or items.
Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting
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
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``,
@ -2309,14 +2319,35 @@ can be used for troubleshooting jobs that don't match the right constraints.
.. image:: images/workflow-new1.png
After selecting one of the presented outputs, the interface proceeds to the
If you select one of the outputs with Enter, the matching constraint is simply
added to the list. If you use Shift-Enter, the interface proceeds to the
next dialog, which allows you to edit the suggested constraint parameters to
suit your need, and set the item count range.
.. image:: images/workflow-new2.png
If you don't need advanced settings, you can just press 'y' to confirm creation.
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

@ -91,6 +91,7 @@ keybinding add Alt-A@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workshop-job
# workflow front-end
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
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
tweak military-training
###########
# Scripts #
###########
# write the correct season to gamelog on world load
soundsense-season
#######################################################
# 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/Screen.h
include/modules/Translation.h
include/modules/Vegetation.h
include/modules/Vermin.h
include/modules/World.h
include/modules/Graphic.h
@ -142,7 +141,6 @@ modules/Materials.cpp
modules/Notes.cpp
modules/Screen.cpp
modules/Translation.cpp
modules/Vegetation.cpp
modules/Vermin.cpp
modules/World.cpp
modules/Graphic.cpp

@ -343,6 +343,50 @@ command_result Core::runCommand(color_ostream &out, const std::string &command)
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)
{
if (!first.empty())
@ -665,10 +709,14 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve
if(res == CR_NOT_IMPLEMENTED)
{
auto filename = getHackPath() + "scripts/" + first;
std::string completed;
if (fileExists(filename + ".lua"))
res = runLuaScript(con, first, parts);
else if (plug_mgr->eval_ruby && fileExists(filename + ".rb"))
res = runRubyScript(con, plug_mgr, first, parts);
else if (try_autocomplete(con, first, completed))
return runCommand(con, completed, parts);
else
con.printerr("%s is not a recognized command.\n", first.c_str());
}
@ -733,7 +781,6 @@ void fIOthread(void * iodata)
{
string command = "";
int ret = con.lineedit("[DFHack]# ",command, main_history);
fprintf(stderr,"Command: [%s]\n",command.c_str());
if(ret == -2)
{
cerr << "Console is shutting down properly." << endl;
@ -747,14 +794,10 @@ void fIOthread(void * iodata)
else if(ret)
{
// a proper, non-empty command was entered
fprintf(stderr,"Adding command to history\n");
main_history.add(command);
fprintf(stderr,"Saving history\n");
main_history.save("dfhack.history");
}
fprintf(stderr,"Running command\n");
auto rv = core->runCommand(con, command);
if (rv == CR_NOT_IMPLEMENTED)

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

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

@ -32,7 +32,6 @@ distribution.
#include "Export.h"
#include "Module.h"
#include "modules/Vegetation.h"
#include <vector>
#include "BitArray.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 = {
_MOUSE_L = true,
_MOUSE_R = true,
_MOUSE_L_DOWN = true,
_MOUSE_R_DOWN = 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)
end
function compute_frame_body(wavail, havail, spec, inset, gap)
function compute_frame_body(wavail, havail, spec, inset, gap, inner_frame)
gap = gap or 0
local l,t,r,b = parse_inset(inset)
local rect = compute_frame_rect(wavail, havail, spec, gap*2+l+r, gap*2+t+b)
local xgap,ygap = 0,0
if inner_frame then
xgap,ygap = gap*2+l+r, gap*2+t+b
end
local rect = compute_frame_rect(wavail, havail, spec, xgap, ygap)
local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
return rect, body
end
@ -623,7 +629,7 @@ end
function FramedScreen:computeFrame(parent_rect)
local sw, sh = parent_rect.width, parent_rect.height
local fw, fh = self:getWantedFrameSize(parent_rect)
return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1)
return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1, true)
end
function FramedScreen:onRenderFrame(dc, rect)

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

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

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

@ -664,14 +664,24 @@ int dfhack_lua_viewscreen::do_input(lua_State *L)
if (enabler && enabler->tracking_on)
{
if (enabler->mouse_lbut) {
if (enabler->mouse_lbut_down) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_L");
}
if (enabler->mouse_rbut) {
if (enabler->mouse_rbut_down) {
lua_pushboolean(L, true);
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);

@ -115,6 +115,9 @@ void Translation::setNickname(df::language_name *name, std::string nick)
if (!name->has_name)
{
if (nick.empty())
return;
*name = df::language_name();
name->language = 0;
@ -122,6 +125,18 @@ void Translation::setNickname(df::language_name *name, std::string 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)

@ -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]))
{
result.item_type = gen->item_type;
result.item_subtype = gen->item_subtype;
result.item_type = spec->candidate->getType();
result.item_subtype = spec->candidate->getSubtype();
result.type = spec->candidate->getActualMaterial();
result.index = spec->candidate->getActualMaterialIndex();
result.valid = true;
@ -294,7 +294,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
{
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)
{
@ -304,7 +304,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
return;
}
}
else if (ui_build_selector->is_grouped)
else
{
last_used_moved = true;
move_material_to_top(get_last_used_material());

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

@ -11,7 +11,6 @@ using std::string;
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <modules/Vegetation.h>
#include <modules/Maps.h>
#include <modules/Gui.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
* in BOXes marked for Squad Equipment, either directly or via
* 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_
* for ammo assigned to that specific _squad_. Otherwise, if
* 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 ts = spec->flags.bits.use_training;
// no-use ammo assumed to be combat
if (((cs || !ts) && combat) || (ts && train))
// no-use ammo assumed to fit any category
if (((cs || !ts) && combat) || ((ts || !cs) && train))
{
if (binsearch_index(spec->assigned, item->id) >= 0)
return true;

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

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

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

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

@ -9,17 +9,19 @@
#include "Console.h"
#include "Export.h"
#include "PluginManager.h"
#include "modules/Vegetation.h"
#include "modules/Maps.h"
#include "modules/Gui.h"
#include "TileTypes.h"
#include "modules/MapCache.h"
#include "df/plant.h"
using std::vector;
using std::string;
using namespace DFHack;
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_immolate (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 (what == do_immolate)
p->is_burning = true;
p->damage_flags.bits.is_burning = true;
p->hitpoints = 0;
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(what == do_immolate)
tree->is_burning = true;
tree->damage_flags.bits.is_burning = true;
tree->hitpoints = 0;
didit = true;
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 &&
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;
}
@ -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));
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/inclusion_type.h"
#include "df/viewscreen_choose_start_sitest.h"
#include "df/plant.h"
using namespace DFHack;
using namespace df::enums;

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

@ -35,6 +35,9 @@ module DFHack
def float
Float.new
end
def double
Double.new
end
def bit(shift, enum=nil)
BitField.new(shift, 1, enum)
end
@ -75,6 +78,9 @@ module DFHack
def df_flagarray(indexenum=nil)
DfFlagarray.new(indexenum)
end
def df_static_flagarray(len, indexenum=nil)
DfStaticFlagarray.new(len, indexenum)
end
def df_array(tglen)
DfArray.new(tglen, yield)
end
@ -237,6 +243,19 @@ module DFHack
_set(0.0)
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
attr_accessor :_shift, :_len, :_enum
def initialize(shift, len, enum=nil)
@ -308,7 +327,7 @@ module DFHack
DFHack.memory_write_int32(@_memaddr, v)
end
when nil; DFHack.memory_write_int32(@_memaddr, 0)
else _get._set(v)
else @_tg._at(_getp)._set(v)
end
end
@ -664,6 +683,48 @@ module DFHack
include Enumerable
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
attr_accessor :_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));
}
static VALUE rb_dfmemory_read_double(VALUE self, VALUE addr)
{
return rb_float_new(*(double*)rb_num2ulong(addr));
}
// memory writing (buffer)
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;
}
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)
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_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_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_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_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_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_patch", RUBY_METHOD_FUNC(rb_dfmemory_patch), 2);

@ -63,12 +63,54 @@ module DFHack
}
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)
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
!u.flags1.diplomat and !u.flags2.resident and !u.flags3.ghostly and
!u.curse.add_tags1.OPPOSED_TO_LIFE and !u.curse.add_tags1.CRAZED and
u.mood != :Berserk
# TODO check curse ; currently this should keep vampires, but may include werebeasts
unit_category(u) == :Citizens
end
def unit_ishostile(u)
unit_category(u) == :Others and
# TODO
true
end
# list workers (citizen, not crazy / child / inmood / noble)

@ -7,10 +7,12 @@
//#include "df/viewscreen_petst.h"
#include "df/viewscreen_storesst.h"
#include "df/viewscreen_layer_stockpilest.h"
#include "df/viewscreen_tradegoodsst.h"
#include "df/viewscreen_unitlistst.h"
#include "df/interface_key.h"
#include "df/interfacest.h"
#include "df/layer_object_listst.h"
using std::set;
using std::vector;
@ -79,6 +81,11 @@ public:
return true;
}
bool is_valid()
{
return valid;
}
// A new keystroke is received in a searchable screen
virtual bool process_input(set<df::interface_key> *input)
{
@ -164,9 +171,9 @@ protected:
const S *viewscreen;
vector <T> saved_list1, reference_list;
vector <V> saved_list2;
vector <V> *sort_list2;
vector <int> saved_indexes;
bool valid;
bool redo_search;
bool track_secondary_values;
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();
}
}
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
void store_reference_values()
{
@ -254,7 +266,7 @@ protected:
}
// Shortcut to clear the search immediately
void clear_search()
virtual void clear_search()
{
if (saved_list1.size() > 0)
{
@ -273,7 +285,7 @@ protected:
}
// The actual sort
void do_search()
virtual void do_search()
{
if (search_string.length() == 0)
{
@ -318,6 +330,7 @@ protected:
store_reference_values(); //Keep a copy, in case user sorts new list
if (cursor_pos)
*cursor_pos = 0;
}
@ -346,10 +359,9 @@ protected:
private:
vector <T> *sort_list1;
vector <V> *sort_list2;
int *cursor_pos;
char select_key;
bool valid;
bool entry_mode;
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;
// 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>
struct search_hook : T
{
@ -437,7 +450,7 @@ public:
if (screen != viewscreen && !reset_on_change())
return false;
if (!valid)
if (!is_valid())
{
viewscreen = screen;
search_parent::init(&screen->item_cursor, &screen->items);
@ -501,7 +514,7 @@ public:
if (screen != viewscreen && !reset_on_change())
return false;
if (!valid)
if (!is_valid())
{
viewscreen = screen;
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())
return false;
if (!valid)
if (!is_valid())
{
viewscreen = screen;
search_parent::init(&screen->trader_cursor, &screen->trader_items, &screen->trader_selected, 'q');
@ -637,7 +650,7 @@ public:
if (screen != viewscreen && !reset_on_change())
return false;
if (!valid)
if (!is_valid())
{
viewscreen = screen;
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");
@ -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, render).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");
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(stocks_search_hook, feed).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;
}
@ -697,6 +792,7 @@ DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_ch
trade_search_merc_hook::module.reset_on_change();
trade_search_fort_hook::module.reset_on_change();
stocks_search_hook::module.reset_on_change();
stockpile_search_hook::module.reset_on_change();
break;
default:

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

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

@ -17,18 +17,30 @@ class AutoFarm
@thresholds.default = v.to_i
end
def is_plantable(plant)
def is_plantable (plant)
has_seed = plant.flags[:SEED]
season = df.cur_season
harvest = df.cur_season_tick + plant.growdur * 10
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
end
def find_plantable_plants
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]
plant = df.world.raws.plants.all[i]
if is_plantable(plant)
@ -36,7 +48,8 @@ class AutoFarm
plantable[i] = :Underground if (plant.underground_depth_min > 0 || plant.underground_depth_max > 0)
end
end
end
}
return plantable
end

@ -11,23 +11,37 @@ def display_death_event(e)
end
item = df.item_find(:selected)
unit = df.unit_find(:selected)
if !item or !item.kind_of?(DFHack::ItemBodyComponent)
item = df.world.items.other[:ANY_CORPSE].find { |i| df.at_cursor?(i) }
end
if !item or !item.kind_of?(DFHack::ItemBodyComponent)
puts "Please select a corpse in the loo'k' menu"
else
if item and item.kind_of?(DFHack::ItemBodyComponent)
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)
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
events = df.world.history.events
(0...events.length).reverse_each { |i|
if events[i].kind_of?(DFHack::HistoryEventHistFigureDiedst) and events[i].victim_hf == hf
display_death_event(events[i])
e = events[i]
if e.kind_of?(DFHack::HistoryEventHistFigureDiedst) and e.victim_hf == hf
display_death_event(e)
break
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
function describe_material(iobj)
local matline = 'any material'
local matflags = utils.list_bitfield_flags(iobj.mat_mask)
if #matflags > 0 then
matflags = 'any '..table.concat(matflags, '/')
else
matflags = nil
end
if is_caste_mat(iobj) then
matline = 'no material'
return 'no material'
elseif (iobj.mat_type or -1) >= 0 then
local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index)
local matline
if info then
matline = info:toString()
else
matline = iobj.mat_type..':'..iobj.mat_index
end
return matline, matflags
else
return matflags or 'any material'
end
end
function current_stock(iobj)
if iobj.goal_by_count then
return iobj.cur_count
else
return iobj.cur_amount
end
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
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
------------------------
-- RANGE EDITOR GROUP --
------------------------
local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false }
RangeEditor = defclass(RangeEditor, widgets.Label)
RangeEditor.ATTRS {
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)
self:setText{
{ key = 'BUILDING_TRIGGER_ENABLE_CREATURE',
{ key = self.keys.count,
text = function()
local cons = self.get_cb() or null_cons
if cons.goal_by_count then
@ -101,21 +153,21 @@ function RangeEditor:init(args)
end
end,
on_activate = self:callback('onChangeUnit') },
{ key = 'BUILDING_TRIGGER_ENABLE_MAGMA', text = ': Modify',
{ key = self.keys.modify, text = ': Range',
on_activate = self:callback('onEditRange') },
NEWLINE, ' ',
{ key = 'BUILDING_TRIGGER_MIN_SIZE_DOWN',
on_activate = self:callback('onIncRange', 'goal_gap', 5) },
{ key = 'BUILDING_TRIGGER_MIN_SIZE_UP',
{ key = self.keys.min_dec,
on_activate = self:callback('onIncRange', 'goal_gap', 2) },
{ key = self.keys.min_inc,
on_activate = self:callback('onIncRange', 'goal_gap', -1) },
{ text = function()
local cons = self.get_cb() or null_cons
return string.format(': Min %-4d ', cons.goal_value - cons.goal_gap)
end },
{ key = 'BUILDING_TRIGGER_MAX_SIZE_DOWN',
{ key = self.keys.max_dec,
on_activate = self:callback('onIncRange', 'goal_value', -1) },
{ key = 'BUILDING_TRIGGER_MAX_SIZE_UP',
on_activate = self:callback('onIncRange', 'goal_value', 5) },
{ key = self.keys.max_inc,
on_activate = self:callback('onIncRange', 'goal_value', 2) },
{ text = function()
local cons = self.get_cb() or null_cons
return string.format(': Max %-4d', cons.goal_value)
@ -156,12 +208,16 @@ end
function RangeEditor:onIncRange(field, delta)
local cons = self.get_cb()
if not cons.goal_by_count then
delta = delta * 5
delta = delta * 2
end
cons[field] = math.max(1, cons[field] + delta)
cons[field] = math.max(1, cons[field] + delta*5)
self.save_cb(cons)
end
---------------------------
-- NEW CONSTRAINT DIALOG --
---------------------------
NewConstraint = defclass(NewConstraint, gui.FramedScreen)
NewConstraint.focus_path = 'workflow/new'
@ -177,7 +233,7 @@ NewConstraint.ATTRS {
}
function NewConstraint:init(args)
self.constraint = args.constraint or {}
self.constraint = args.constraint or { item_type = -1 }
rawset_default(self.constraint, { goal_value = 10, goal_gap = 5, goal_by_count = false })
local matlist = {}
@ -202,8 +258,16 @@ function NewConstraint:init(args)
frame = { l = 1, t = 2, w = 26 },
text = {
'Type: ',
{ pen = COLOR_LIGHTCYAN,
text = function() return describe_item_type(self.constraint) end },
{ pen = function()
if self:isValid() then return COLOR_LIGHTCYAN else return COLOR_LIGHTRED end
end,
text = function()
if self:isValid() then
return describe_item_type(self.constraint)
else
return 'item not set'
end
end },
NEWLINE, ' ',
{ key = 'CUSTOM_T', text = ': Select, ',
on_activate = self:callback('chooseType') },
@ -239,7 +303,7 @@ function NewConstraint:init(args)
}
},
widgets.Label{
frame = { l = 0, t = 13 },
frame = { l = 0, t = 14 },
text = {
'Desired range: ',
{ pen = COLOR_LIGHTCYAN,
@ -255,7 +319,7 @@ function NewConstraint:init(args)
}
},
RangeEditor{
frame = { l = 1, t = 15 },
frame = { l = 1, t = 16 },
get_cb = self:cb_getfield('constraint'),
save_cb = self:callback('onRangeChange'),
},
@ -277,6 +341,7 @@ function NewConstraint:init(args)
{ key = 'LEAVESCREEN', text = ': Cancel, ',
on_activate = self:callback('dismiss') },
{ key = 'MENU_CONFIRM', key_sep = ': ',
enabled = self:callback('isValid'),
text = function()
if self.is_existing then return 'Update' else return 'Create new' end
end,
@ -295,9 +360,17 @@ function NewConstraint:postinit()
self:onChange()
end
function NewConstraint:isValid()
return self.constraint.item_type >= 0 or self.constraint.is_craft
end
function NewConstraint:onChange()
local token = workflow.constraintToToken(self.constraint)
local out = workflow.findConstraint(token)
local out
if self:isValid() then
out = workflow.findConstraint(token)
end
if out then
self.constraint = out
@ -390,6 +463,364 @@ function NewConstraint:onRangeChange()
cons.goal_gap = math.max(1, math.min(cons.goal_gap, cons.goal_value-1))
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.focus_path = 'workflow/job'
@ -425,14 +856,20 @@ function JobConstraints:init(args)
widgets.Label{
frame = { l = 0, b = 0 },
text = {
{ key = 'CUSTOM_N', text = ': New limit, ',
{ key = 'CUSTOM_SHIFT_A', text = ': Add limit, ',
on_activate = self:callback('onNewConstraint') },
{ key = 'CUSTOM_X', text = ': Delete',
{ key = 'CUSTOM_SHIFT_X', text = ': Delete',
enabled = self:callback('isAnySelected'),
on_activate = self:callback('onDeleteConstraint') },
NEWLINE, NEWLINE,
{ 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
itemstr = itemstr .. ' ('..table.concat(lst,',')..')'
end
local matstr = describe_material(cons)
local matflagstr = ''
local matflags = utils.list_bitfield_flags(cons.mat_mask)
if #matflags > 0 then
matflags[1] = 'any '..matflags[1]
if matstr == 'any material' then
matstr = table.concat(matflags, ', ')
matflags = {}
end
end
if #matflags > 0 then
matflagstr = table.concat(matflags, ', ')
end
local matstr,matflagstr = describe_material(cons)
table.insert(choices, {
text = {
goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE,
' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', matflagstr
' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', (matflagstr or '')
},
token = cons.token,
obj = cons
@ -538,32 +963,30 @@ function JobConstraints:onNewConstraint()
local choices = {}
for i,cons in ipairs(variants) do
local itemstr = describe_item_type(cons)
local matstr = describe_material(cons)
local matflags = utils.list_bitfield_flags(cons.mat_mask)
if #matflags > 0 then
local fstr = table.concat(matflags, '/')
if matstr == 'any material' then
matstr = 'any '..fstr
else
matstr = 'any '..fstr..' '..matstr
end
local matstr,matflags = describe_material(cons)
if matflags then
matstr = matflags..' '..matstr
end
table.insert(choices, { text = itemstr..' of '..matstr, obj = cons })
end
dlg.showListPrompt(
'New limit',
'Select one of the possible outputs:',
COLOR_WHITE,
choices,
function(idx,item)
dlg.ListBox{
frame_title = 'Add limit',
text = 'Select one of the possible outputs:',
text_pen = COLOR_WHITE,
choices = choices,
on_select = function(idx,item)
self:saveConstraint(item.obj)
end,
select2_hint = 'Advanced',
on_select2 = function(idx,item)
NewConstraint{
constraint = item.obj,
on_submit = self:callback('saveConstraint')
}:show()
end
)
end,
}:show()
end
function JobConstraints:onDeleteConstraint()
@ -589,13 +1012,18 @@ function JobConstraints:onInput(keys)
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")
end
end
local job = dfhack.gui.getSelectedJob()
local job = dfhack.gui.getSelectedJob()
check_enabled(function()
check_enabled(function()
check_repeat(job, function()
local clist = workflow.listConstraints(job)
if not clist then
@ -604,5 +1032,5 @@ check_enabled(function()
end
JobConstraints{ job = job, clist = clist }:show()
end)
end)
end)
end

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

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