-
+
This plugin makes reactions with names starting with SPATTER_ADD_
produce contaminants on the items instead of improvements. The produced
contaminants are immune to being washed away by water or destroyed by
diff --git a/Readme.rst b/Readme.rst
index e8a04de78..a2bc3afb2 100644
--- a/Readme.rst
+++ b/Readme.rst
@@ -1577,6 +1577,29 @@ Duplicate the selected job in a workshop:
* In 'q' mode, when a job is highlighted within a workshop or furnace building,
instantly duplicates the job.
+stockflow
+---------
+Allows the fortress bookkeeper to queue jobs through the manager.
+
+Usage:
+
+ ``stockflow enable``
+ Enable the plugin.
+ ``stockflow disable``
+ Disable the plugin.
+ ``stockflow list``
+ List any work order settings for your stockpiles.
+ ``stockflow status``
+ Display whether the plugin is enabled.
+
+While enabled, the 'q' menu of each stockpile will have two new options:
+ * j: Select a job to order, from an interface like the manager's screen.
+ * J: Cycle between several options for how many such jobs to order.
+
+Whenever the bookkeeper updates stockpile records, new work orders will
+be placed on the manager's queue for each such selection, reduced by the
+number of identical orders already in the queue.
+
workflow
--------
Manage control of repeat jobs.
@@ -2962,7 +2985,7 @@ in-game help.
gui/mod-manager
===============
-A way to simply install and remove small mods. It looks for specially formated mods in
+A way to simply install and remove small mods. It looks for specially formatted mods in
df subfolder 'mods'. Mods are not included, for example mods see: `github mini mod repository `_
.. image:: images/mod-manager.png
diff --git a/dfhack.init-example b/dfhack.init-example
index fa227dae4..c5ec9800f 100644
--- a/dfhack.init-example
+++ b/dfhack.init-example
@@ -192,6 +192,9 @@ enable automaterial
# Auto Syndrome
#autoSyndrome enable
+# allow the fortress bookkeeper to queue jobs through the manager
+stockflow enable
+
###########
# Scripts #
###########
diff --git a/library/Console-darwin.cpp b/library/Console-darwin.cpp
index 86cd657a1..f36973d5c 100644
--- a/library/Console-darwin.cpp
+++ b/library/Console-darwin.cpp
@@ -305,6 +305,33 @@ namespace DFHack
}
/// beep. maybe?
//void beep (void);
+ void back_word()
+ {
+ if (raw_cursor == 0)
+ return;
+ raw_cursor--;
+ while (raw_cursor > 0 && !isalnum(raw_buffer[raw_cursor]))
+ raw_cursor--;
+ while (raw_cursor > 0 && isalnum(raw_buffer[raw_cursor]))
+ raw_cursor--;
+ if (!isalnum(raw_buffer[raw_cursor]) && raw_cursor != 0)
+ raw_cursor++;
+ prompt_refresh();
+ }
+ void forward_word()
+ {
+ int len = raw_buffer.size();
+ if (raw_cursor == len)
+ return;
+ raw_cursor++;
+ while (raw_cursor <= len && !isalnum(raw_buffer[raw_cursor]))
+ raw_cursor++;
+ while (raw_cursor <= len && isalnum(raw_buffer[raw_cursor]))
+ raw_cursor++;
+ if (raw_cursor > len)
+ raw_cursor = len;
+ prompt_refresh();
+ }
/// A simple line edit (raw mode)
int lineedit(const std::string& prompt, std::string& output, recursive_mutex * lock, CommandHistory & ch)
{
@@ -478,14 +505,27 @@ namespace DFHack
break;
case 27: // escape sequence
lock->unlock();
- if(!read_char(seq[0]) || !read_char(seq[1]))
+ if (!read_char(seq[0]))
{
lock->lock();
return -2;
}
lock->lock();
- if(seq[0] == '[')
+ if (seq[0] == 'b')
{
+ back_word();
+ }
+ else if (seq[0] == 'f')
+ {
+ forward_word();
+ }
+ else if(seq[0] == '[')
+ {
+ if (!read_char(seq[1]))
+ {
+ lock->lock();
+ return -2;
+ }
if (seq[1] == 'D')
{
left_arrow:
@@ -545,6 +585,7 @@ namespace DFHack
else if (seq[1] > '0' && seq[1] < '7')
{
// extended escape
+ unsigned char seq3[3];
lock->unlock();
if(!read_char(seq2))
{
@@ -561,6 +602,24 @@ namespace DFHack
prompt_refresh();
}
}
+ if (!read_char(seq3[0]) || !read_char(seq3[1]))
+ {
+ lock->lock();
+ return -2;
+ }
+ if (seq2 == ';')
+ {
+ // Format: esc [ n ; n DIRECTION
+ // Ignore first character (second "n")
+ if (seq3[1] == 'C')
+ {
+ forward_word();
+ }
+ else if (seq3[1] == 'D')
+ {
+ back_word();
+ }
+ }
}
}
break;
diff --git a/library/Console-linux.cpp b/library/Console-linux.cpp
index f32fa1c2a..d4005af3c 100644
--- a/library/Console-linux.cpp
+++ b/library/Console-linux.cpp
@@ -307,6 +307,33 @@ namespace DFHack
}
/// beep. maybe?
//void beep (void);
+ void back_word()
+ {
+ if (raw_cursor == 0)
+ return;
+ raw_cursor--;
+ while (raw_cursor > 0 && !isalnum(raw_buffer[raw_cursor]))
+ raw_cursor--;
+ while (raw_cursor > 0 && isalnum(raw_buffer[raw_cursor]))
+ raw_cursor--;
+ if (!isalnum(raw_buffer[raw_cursor]) && raw_cursor != 0)
+ raw_cursor++;
+ prompt_refresh();
+ }
+ void forward_word()
+ {
+ int len = raw_buffer.size();
+ if (raw_cursor == len)
+ return;
+ raw_cursor++;
+ while (raw_cursor <= len && !isalnum(raw_buffer[raw_cursor]))
+ raw_cursor++;
+ while (raw_cursor <= len && isalnum(raw_buffer[raw_cursor]))
+ raw_cursor++;
+ if (raw_cursor > len)
+ raw_cursor = len;
+ prompt_refresh();
+ }
/// A simple line edit (raw mode)
int lineedit(const std::string& prompt, std::string& output, recursive_mutex * lock, CommandHistory & ch)
{
@@ -480,14 +507,27 @@ namespace DFHack
break;
case 27: // escape sequence
lock->unlock();
- if(!read_char(seq[0]) || !read_char(seq[1]))
+ if (!read_char(seq[0]))
{
lock->lock();
return -2;
}
lock->lock();
- if(seq[0] == '[')
+ if (seq[0] == 'b')
{
+ back_word();
+ }
+ else if (seq[0] == 'f')
+ {
+ forward_word();
+ }
+ else if(seq[0] == '[')
+ {
+ if (!read_char(seq[1]))
+ {
+ lock->lock();
+ return -2;
+ }
if (seq[1] == 'D')
{
left_arrow:
@@ -547,6 +587,7 @@ namespace DFHack
else if (seq[1] > '0' && seq[1] < '7')
{
// extended escape
+ unsigned char seq3[3];
lock->unlock();
if(!read_char(seq2))
{
@@ -563,6 +604,24 @@ namespace DFHack
prompt_refresh();
}
}
+ if (!read_char(seq3[0]) || !read_char(seq3[1]))
+ {
+ lock->lock();
+ return -2;
+ }
+ if (seq2 == ';')
+ {
+ // Format: esc [ n ; n DIRECTION
+ // Ignore first character (second "n")
+ if (seq3[1] == 'C')
+ {
+ forward_word();
+ }
+ else if (seq3[1] == 'D')
+ {
+ back_word();
+ }
+ }
}
}
break;
diff --git a/library/include/VTableInterpose.h b/library/include/VTableInterpose.h
index f93eb4176..ec950bfe2 100644
--- a/library/include/VTableInterpose.h
+++ b/library/include/VTableInterpose.h
@@ -42,12 +42,16 @@ namespace DFHack
struct my_hack : df::someclass {
typedef df::someclass interpose_base;
- DEFINE_VMETHOD_INTERPOSE(void, foo, (int arg)) {
+ // You may define additional methods here, but NOT non-static fields
+
+ DEFINE_VMETHOD_INTERPOSE(int, foo, (int arg)) {
// If needed by the code, claim the suspend lock.
// DO NOT USE THE USUAL CoreSuspender, OR IT WILL DEADLOCK!
// CoreSuspendClaimer suspend;
...
- INTERPOSE_NEXT(foo)(arg) // call the original
+ ... this->field ... // access fields of the df::someclass object
+ ...
+ int orig_retval = INTERPOSE_NEXT(foo)(arg); // call the original method
...
}
};
diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp
index 38d63d867..f25140217 100644
--- a/library/modules/Items.cpp
+++ b/library/modules/Items.cpp
@@ -234,6 +234,8 @@ ITEMDEF_VECTORS
#undef ITEM
default:
+ if (items[1] == "NONE")
+ return true;
break;
}
diff --git a/library/xml b/library/xml
index e0407d8bb..851f52d5e 160000
--- a/library/xml
+++ b/library/xml
@@ -1 +1 @@
-Subproject commit e0407d8bbbc965dbb62826d481f27940972ffd66
+Subproject commit 851f52d5e9eae6fc81adadd10e53bd2cc42bfd21
diff --git a/plugins/3dveins.cpp b/plugins/3dveins.cpp
index 432d93cf6..c9e15aae5 100644
--- a/plugins/3dveins.cpp
+++ b/plugins/3dveins.cpp
@@ -42,6 +42,7 @@ using namespace MapExtras;
using namespace DFHack::Random;
using df::global::world;
+using df::global::gametype;
command_result cmd_3dveins(color_ostream &out, std::vector & parameters);
@@ -1573,6 +1574,12 @@ command_result cmd_3dveins(color_ostream &con, std::vector & parame
return CR_FAILURE;
}
+ if (*gametype != game_type::DWARF_MAIN && *gametype != game_type::DWARF_RECLAIM)
+ {
+ con.printerr("Must be used in fortress mode!\n");
+ return CR_FAILURE;
+ }
+
VeinGenerator generator(con);
con.print("Collecting statistics...\n");
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index a73651608..b803afc3d 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -114,6 +114,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename)
DFHACK_PLUGIN(jobutils jobutils.cpp)
DFHACK_PLUGIN(workflow workflow.cpp LINK_LIBRARIES lua)
+ DFHACK_PLUGIN(stockflow stockflow.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(showmood showmood.cpp)
DFHACK_PLUGIN(fixveins fixveins.cpp)
DFHACK_PLUGIN(fixpositions fixpositions.cpp)
diff --git a/plugins/autotrade.cpp b/plugins/autotrade.cpp
index e31fccdbc..5eacbf2d7 100644
--- a/plugins/autotrade.cpp
+++ b/plugins/autotrade.cpp
@@ -502,7 +502,7 @@ static command_result autotrade_cmd(color_ostream &out, vector & parame
{
if (parameters.size() == 1 && toLower(parameters[0])[0] == 'v')
{
- out << "Building Plan" << endl << "Version: " << PLUGIN_VERSION << endl;
+ out << "Autotrade" << endl << "Version: " << PLUGIN_VERSION << endl;
}
}
diff --git a/plugins/command-prompt.cpp b/plugins/command-prompt.cpp
index d7a8e230c..3cdd83ab3 100644
--- a/plugins/command-prompt.cpp
+++ b/plugins/command-prompt.cpp
@@ -7,6 +7,7 @@
#include
#include
+#include
#include
#include
@@ -55,9 +56,14 @@ public:
df::global::gps->display_frames=show_fps;
}
- void add_response(color_value v,std::string s)
+ void add_response(color_value v, std::string s)
{
- responses.push_back(std::make_pair(v,s));
+ std::stringstream ss(s);
+ std::string part;
+ while (std::getline(ss, part))
+ {
+ responses.push_back(std::make_pair(v, part + '\n'));
+ }
}
protected:
std::list > responses;
@@ -166,16 +172,24 @@ void viewscreen_commandpromptst::feed(std::set *events)
DFHACK_PLUGIN("command-prompt");
command_result show_prompt(color_ostream &out, std::vector & parameters)
{
+ if (Gui::getCurFocus() == "dfhack/commandprompt")
+ {
+ Screen::dismiss(Gui::getCurViewscreen(true));
+ }
std::string params;
for(size_t i=0;i &commands)
{
commands.push_back(PluginCommand(
- "command-prompt","Shows a command prompt on window.",show_prompt,false,
+ "command-prompt","Shows a command prompt on window.",show_prompt,hotkey_allow_all,
"command-prompt [entry] - shows a cmd prompt in df window. Entry is used for default prefix (e.g. ':lua')"
));
return CR_OK;
diff --git a/plugins/createitem.cpp b/plugins/createitem.cpp
index 6442e818a..89c2ade36 100644
--- a/plugins/createitem.cpp
+++ b/plugins/createitem.cpp
@@ -26,8 +26,10 @@
#include "df/reaction_product_itemst.h"
#include "df/tool_uses.h"
-using namespace std;
+using std::string;
+using std::vector;
using namespace DFHack;
+using namespace df::enums;
using df::global::world;
using df::global::ui;
@@ -71,8 +73,8 @@ bool makeItem (df::reaction_product_itemst *prod, df::unit *unit, bool second_it
vector out_items;
vector in_reag;
vector in_items;
- bool is_gloves = (prod->item_type == df::item_type::GLOVES);
- bool is_shoes = (prod->item_type == df::item_type::SHOES);
+ bool is_gloves = (prod->item_type == item_type::GLOVES);
+ bool is_shoes = (prod->item_type == item_type::SHOES);
df::item *container = NULL;
df::building *building = NULL;
@@ -81,9 +83,9 @@ bool makeItem (df::reaction_product_itemst *prod, df::unit *unit, bool second_it
if (dest_building != -1)
building = df::building::find(dest_building);
- prod->produce(unit, &out_items, &in_reag, &in_items, 1, df::job_skill::NONE,
+ prod->produce(unit, &out_items, &in_reag, &in_items, 1, job_skill::NONE,
df::historical_entity::find(unit->civ_id),
- ((*gametype == df::game_type::DWARF_MAIN) || (*gametype == df::game_type::DWARF_RECLAIM)) ? df::world_site::find(ui->site_id) : NULL);
+ ((*gametype == game_type::DWARF_MAIN) || (*gametype == game_type::DWARF_RECLAIM)) ? df::world_site::find(ui->site_id) : NULL);
if (!out_items.size())
return false;
// if we asked to make shoes and we got twice as many as we asked, then we're okay
@@ -130,7 +132,7 @@ bool makeItem (df::reaction_product_itemst *prod, df::unit *unit, bool second_it
command_result df_createitem (color_ostream &out, vector & parameters)
{
string item_str, material_str;
- df::item_type item_type = df::item_type::NONE;
+ df::item_type item_type = item_type::NONE;
int16_t item_subtype = -1;
int16_t mat_type = -1;
int32_t mat_index = -1;
@@ -156,23 +158,23 @@ command_result df_createitem (color_ostream &out, vector & parameters)
}
switch (item->getType())
{
- case df::item_type::FLASK:
- case df::item_type::BARREL:
- case df::item_type::BUCKET:
- case df::item_type::ANIMALTRAP:
- case df::item_type::BOX:
- case df::item_type::BIN:
- case df::item_type::BACKPACK:
- case df::item_type::QUIVER:
+ case item_type::FLASK:
+ case item_type::BARREL:
+ case item_type::BUCKET:
+ case item_type::ANIMALTRAP:
+ case item_type::BOX:
+ case item_type::BIN:
+ case item_type::BACKPACK:
+ case item_type::QUIVER:
break;
- case df::item_type::TOOL:
- if (item->hasToolUse(df::tool_uses::LIQUID_CONTAINER))
+ case item_type::TOOL:
+ if (item->hasToolUse(tool_uses::LIQUID_CONTAINER))
break;
- if (item->hasToolUse(df::tool_uses::FOOD_STORAGE))
+ if (item->hasToolUse(tool_uses::FOOD_STORAGE))
break;
- if (item->hasToolUse(df::tool_uses::SMALL_OBJECT_STORAGE))
+ if (item->hasToolUse(tool_uses::SMALL_OBJECT_STORAGE))
break;
- if (item->hasToolUse(df::tool_uses::TRACK_CART))
+ if (item->hasToolUse(tool_uses::TRACK_CART))
break;
default:
out.printerr("The selected item cannot be used for item storage!\n");
@@ -195,22 +197,22 @@ command_result df_createitem (color_ostream &out, vector & parameters)
}
switch (building->getType())
{
- case df::building_type::Coffin:
- case df::building_type::Furnace:
- case df::building_type::TradeDepot:
- case df::building_type::Shop:
- case df::building_type::Box:
- case df::building_type::Weaponrack:
- case df::building_type::Armorstand:
- case df::building_type::Workshop:
- case df::building_type::Cabinet:
- case df::building_type::SiegeEngine:
- case df::building_type::Trap:
- case df::building_type::AnimalTrap:
- case df::building_type::Cage:
- case df::building_type::Wagon:
- case df::building_type::NestBox:
- case df::building_type::Hive:
+ case building_type::Coffin:
+ case building_type::Furnace:
+ case building_type::TradeDepot:
+ case building_type::Shop:
+ case building_type::Box:
+ case building_type::Weaponrack:
+ case building_type::Armorstand:
+ case building_type::Workshop:
+ case building_type::Cabinet:
+ case building_type::SiegeEngine:
+ case building_type::Trap:
+ case building_type::AnimalTrap:
+ case building_type::Cage:
+ case building_type::Wagon:
+ case building_type::NestBox:
+ case building_type::Hive:
break;
default:
out.printerr("The selected building cannot be used for item storage!\n");
@@ -252,28 +254,31 @@ command_result df_createitem (color_ostream &out, vector & parameters)
MaterialInfo material;
vector tokens;
- if (!item.find(item_str))
+ if (item.find(item_str))
{
- out.printerr("Unrecognized item type!\n");
+ item_type = item.type;
+ item_subtype = item.subtype;
+ }
+ if (item_type == item_type::NONE)
+ {
+ out.printerr("You must specify a valid item type to create!\n");
return CR_FAILURE;
}
- item_type = item.type;
- item_subtype = item.subtype;
switch (item.type)
{
- case df::item_type::INSTRUMENT:
- case df::item_type::TOY:
- case df::item_type::WEAPON:
- case df::item_type::ARMOR:
- case df::item_type::SHOES:
- case df::item_type::SHIELD:
- case df::item_type::HELM:
- case df::item_type::GLOVES:
- case df::item_type::AMMO:
- case df::item_type::PANTS:
- case df::item_type::SIEGEAMMO:
- case df::item_type::TRAPCOMP:
- case df::item_type::TOOL:
+ case item_type::INSTRUMENT:
+ case item_type::TOY:
+ case item_type::WEAPON:
+ case item_type::ARMOR:
+ case item_type::SHOES:
+ case item_type::SHIELD:
+ case item_type::HELM:
+ case item_type::GLOVES:
+ case item_type::AMMO:
+ case item_type::PANTS:
+ case item_type::SIEGEAMMO:
+ case item_type::TRAPCOMP:
+ case item_type::TOOL:
if (item_subtype == -1)
{
out.printerr("You must specify a subtype!\n");
@@ -289,12 +294,12 @@ command_result df_createitem (color_ostream &out, vector & parameters)
mat_index = material.index;
break;
- case df::item_type::REMAINS:
- case df::item_type::FISH:
- case df::item_type::FISH_RAW:
- case df::item_type::VERMIN:
- case df::item_type::PET:
- case df::item_type::EGG:
+ case item_type::REMAINS:
+ case item_type::FISH:
+ case item_type::FISH_RAW:
+ case item_type::VERMIN:
+ case item_type::PET:
+ case item_type::EGG:
split_string(&tokens, material_str, ":");
if (tokens.size() != 2)
{
@@ -331,9 +336,9 @@ command_result df_createitem (color_ostream &out, vector & parameters)
}
break;
- case df::item_type::CORPSE:
- case df::item_type::CORPSEPIECE:
- case df::item_type::FOOD:
+ case item_type::CORPSE:
+ case item_type::CORPSEPIECE:
+ case item_type::FOOD:
out.printerr("Cannot create that type of item!\n");
return CR_FAILURE;
break;
@@ -376,16 +381,16 @@ command_result df_createitem (color_ostream &out, vector & parameters)
prod->count = count;
switch (item_type)
{
- case df::item_type::BAR:
- case df::item_type::POWDER_MISC:
- case df::item_type::LIQUID_MISC:
- case df::item_type::DRINK:
+ case item_type::BAR:
+ case item_type::POWDER_MISC:
+ case item_type::LIQUID_MISC:
+ case item_type::DRINK:
prod->product_dimension = 150;
break;
- case df::item_type::THREAD:
+ case item_type::THREAD:
prod->product_dimension = 15000;
break;
- case df::item_type::CLOTH:
+ case item_type::CLOTH:
prod->product_dimension = 10000;
break;
default:
diff --git a/plugins/lua/stockflow.lua b/plugins/lua/stockflow.lua
new file mode 100644
index 000000000..489a72705
--- /dev/null
+++ b/plugins/lua/stockflow.lua
@@ -0,0 +1,1129 @@
+local _ENV = mkmodule('plugins.stockflow')
+
+local gui = require "gui"
+local utils = require "utils"
+
+reaction_list = reaction_list or {}
+saved_orders = saved_orders or {}
+jobs_to_create = jobs_to_create or {}
+
+triggers = {
+ {filled = false, divisor = 1, name = "Per empty space"},
+ {filled = true, divisor = 1, name = "Per stored item"},
+ {filled = false, divisor = 2, name = "Per two empty spaces"},
+ {filled = true, divisor = 2, name = "Per two stored items"},
+ {filled = false, divisor = 3, name = "Per three empty spaces"},
+ {filled = true, divisor = 3, name = "Per three stored items"},
+ {filled = false, divisor = 4, name = "Per four empty spaces"},
+ {filled = true, divisor = 4, name = "Per four stored items"},
+ {name = "Never"},
+}
+
+local job_types = df.job_type
+
+entry_ints = {
+ stockpile_id = 1,
+ order_number = 2,
+ trigger_number = 3,
+}
+
+PageSize = 16
+FirstRow = 4
+CenterCol = 38
+
+-- Populate the reaction and stockpile order lists.
+-- To be called whenever a world is loaded.
+function initialize_world()
+ reaction_list = collect_reactions()
+ saved_orders = collect_orders()
+ jobs_to_create = {}
+end
+
+-- Clear all caches.
+-- Called when a world is loaded, or when the plugin is disabled.
+function clear_caches()
+ -- Free the C++ objects in the reaction list.
+ for _, value in ipairs(reaction_list) do
+ value.order:delete()
+ end
+ reaction_list = {}
+ saved_orders = {}
+ jobs_to_create = {}
+end
+
+function trigger_name(cache)
+ local trigger = triggers[cache.entry.ints[entry_ints.trigger_number]]
+ return trigger and trigger.name or "Never"
+end
+
+function list_orders()
+ local listed = false
+ for _, spec in pairs(saved_orders) do
+ local num = spec.stockpile.stockpile_number
+ local name = spec.entry.value
+ local trigger = trigger_name(spec)
+ print("Stockpile #"..num, name, trigger)
+ listed = true
+ end
+
+ if not listed then
+ print("No manager jobs have been set for your stockpiles.")
+ print("Use j in a stockpile menu to create one...")
+ end
+end
+
+-- Save the stockpile jobs for later creation.
+-- Called when the bookkeeper starts updating stockpile records.
+function start_bookkeeping()
+ result = {}
+ for reaction_id, quantity in pairs(check_stockpiles()) do
+ local amount = order_quantity(reaction_list[reaction_id].order, quantity)
+ if amount > 0 then
+ result[reaction_id] = amount
+ end
+ end
+
+ jobs_to_create = result
+end
+
+-- Insert any saved jobs.
+-- Called when the bookkeeper finishes updating stockpile records.
+function finish_bookkeeping()
+ for reaction, amount in pairs(jobs_to_create) do
+ create_orders(reaction_list[reaction].order, amount)
+ end
+
+ jobs_to_create = {}
+end
+
+function stockpile_settings(sp)
+ local order = saved_orders[sp.id]
+ if not order then
+ return "No job selected", ""
+ end
+
+ return order.entry.value, trigger_name(order)
+end
+
+-- Toggle the trigger condition for a stockpile.
+function toggle_trigger(sp)
+ local saved = saved_orders[sp.id]
+ if saved then
+ saved.entry.ints[entry_ints.trigger_number] = (saved.entry.ints[entry_ints.trigger_number] % #triggers) + 1
+ saved.entry:save()
+ end
+end
+
+function collect_orders()
+ local result = {}
+ local entries = dfhack.persistent.get_all("stockflow/entry", true)
+ if entries then
+ local stockpiles = df.global.world.buildings.other.STOCKPILE
+ for _, entry in ipairs(entries) do
+ local spid = entry.ints[entry_ints.stockpile_id]
+ local stockpile = utils.binsearch(stockpiles, spid, "id")
+ if stockpile then
+ -- Todo: What if entry.value ~= reaction_list[order_number].name?
+ result[spid] = {
+ stockpile = stockpile,
+ entry = entry,
+ }
+ end
+ end
+ end
+
+ return result
+end
+
+-- Choose an order that the stockpile should add to the manager queue.
+function select_order(stockpile)
+ screen:reset(stockpile)
+ screen:show()
+end
+
+function reaction_entry(job_type, values, name)
+ local order = df.manager_order:new()
+ -- These defaults differ from the newly created order's.
+ order:assign{
+ job_type = job_type,
+ unk_2 = -1,
+ item_subtype = -1,
+ mat_type = -1,
+ mat_index = -1,
+ }
+
+ if values then
+ -- Override default attributes.
+ order:assign(values)
+ end
+
+ return {
+ name = name or df.job_type.attrs[job_type].caption,
+ order = order,
+ }
+end
+
+function resource_reactions(reactions, job_type, mat_info, keys, items, options)
+ local values = {}
+ for key, value in pairs(mat_info.management) do
+ values[key] = value
+ end
+
+ for _, itemid in ipairs(keys) do
+ local itemdef = items[itemid]
+ local start = options.verb or mat_info.verb or "Make"
+ if options.adjective then
+ start = start.." "..itemdef.adjective
+ end
+
+ if (not options.permissible) or options.permissible(itemdef) then
+ local item_name = " "..itemdef[options.name_field or "name"]
+ if options.capitalize then
+ item_name = string.gsub(item_name, " .", string.upper)
+ end
+
+ values.item_subtype = itemid
+ table.insert(reactions, reaction_entry(job_type, values, start.." "..mat_info.adjective..item_name))
+ end
+ end
+end
+
+function material_reactions(reactions, itemtypes, mat_info)
+ -- Expects a list of {job_type, verb, item_name} tuples.
+ for _, row in ipairs(itemtypes) do
+ local line = row[2].." "..mat_info.adjective
+ if row[3] then
+ line = line.." "..row[3]
+ end
+
+ table.insert(reactions, reaction_entry(row[1], mat_info.management, line))
+ end
+end
+
+function clothing_reactions(reactions, mat_info, filter)
+ local resources = df.historical_entity.find(df.global.ui.civ_id).resources
+ local itemdefs = df.global.world.raws.itemdefs
+ resource_reactions(reactions, job_types.MakeArmor, mat_info, resources.armor_type, itemdefs.armor, {permissible = filter})
+ resource_reactions(reactions, job_types.MakePants, mat_info, resources.pants_type, itemdefs.pants, {permissible = filter})
+ resource_reactions(reactions, job_types.MakeGloves, mat_info, resources.gloves_type, itemdefs.gloves, {permissible = filter})
+ resource_reactions(reactions, job_types.MakeHelm, mat_info, resources.helm_type, itemdefs.helms, {permissible = filter})
+ resource_reactions(reactions, job_types.MakeShoes, mat_info, resources.shoes_type, itemdefs.shoes, {permissible = filter})
+end
+
+-- Find the reaction types that should be listed in the management interface.
+function collect_reactions()
+ -- The sequence here tries to match the native manager screen.
+ -- It should also be possible to collect the sequence from somewhere native,
+ -- but I currently can only find it while the job selection screen is active.
+ -- Even that list doesn't seem to include their names.
+ local result = {}
+
+ local materials = {
+ rock = {
+ adjective = "rock",
+ management = {mat_type = 0},
+ },
+ }
+
+ for _, name in ipairs{"wood", "cloth", "leather", "silk", "yarn", "bone", "shell", "tooth", "horn", "pearl"} do
+ materials[name] = {
+ adjective = name,
+ management = {material_category = {[name] = true}},
+ }
+ end
+
+ materials.wood.adjective = "wooden"
+ materials.tooth.adjective = "ivory/tooth"
+ materials.leather.clothing_flag = "LEATHER"
+
+ -- Collection and Entrapment
+ table.insert(result, reaction_entry(job_types.CollectWebs))
+ table.insert(result, reaction_entry(job_types.CollectSand))
+ table.insert(result, reaction_entry(job_types.CollectClay))
+ table.insert(result, reaction_entry(job_types.CatchLiveLandAnimal))
+ table.insert(result, reaction_entry(job_types.CatchLiveFish))
+
+ -- Cutting, encrusting, and metal extraction.
+ local rock_types = df.global.world.raws.inorganics
+ for rock_id = #rock_types-1, 0, -1 do
+ local material = rock_types[rock_id].material
+ local rock_name = material.state_adj.Solid
+ if material.flags.IS_STONE or material.flags.IS_GEM then
+ table.insert(result, reaction_entry(job_types.CutGems, {
+ mat_type = 0,
+ mat_index = rock_id,
+ }, "Cut "..rock_name))
+
+ table.insert(result, reaction_entry(job_types.EncrustWithGems, {
+ mat_type = 0,
+ mat_index = rock_id,
+ item_category = {finished_goods = true},
+ }, "Encrust Finished Goods With "..rock_name))
+
+ table.insert(result, reaction_entry(job_types.EncrustWithGems, {
+ mat_type = 0,
+ mat_index = rock_id,
+ item_category = {furniture = true},
+ }, "Encrust Furniture With "..rock_name))
+
+ table.insert(result, reaction_entry(job_types.EncrustWithGems, {
+ mat_type = 0,
+ mat_index = rock_id,
+ item_category = {ammo = true},
+ }, "Encrust Ammo With "..rock_name))
+ end
+
+ if #rock_types[rock_id].metal_ore.mat_index > 0 then
+ table.insert(result, reaction_entry(job_types.SmeltOre, {mat_type = 0, mat_index = rock_id}, "Smelt "..rock_name.." Ore"))
+ end
+
+ if #rock_types[rock_id].thread_metal.mat_index > 0 then
+ table.insert(result, reaction_entry(job_types.ExtractMetalStrands, {mat_type = 0, mat_index = rock_id}))
+ end
+ end
+
+ -- Glass cutting and encrusting, with different job numbers.
+ -- We could search the entire table, but glass is less subject to raws.
+ local glass_types = df.global.world.raws.mat_table.builtin
+ local glasses = {}
+ for glass_id = 3, 5 do
+ local material = glass_types[glass_id]
+ local glass_name = material.state_adj.Solid
+ if material.flags.IS_GLASS then
+ -- For future use.
+ table.insert(glasses, {
+ adjective = glass_name,
+ management = {mat_type = glass_id},
+ })
+
+ table.insert(result, reaction_entry(job_types.CutGlass, {mat_type = glass_id}, "Cut "..glass_name))
+
+ table.insert(result, reaction_entry(job_types.EncrustWithGlass, {
+ mat_type = glass_id,
+ item_category = {finished_goods = true},
+ }, "Encrust Finished Goods With "..glass_name))
+
+ table.insert(result, reaction_entry(job_types.EncrustWithGlass, {
+ mat_type = glass_id,
+ item_category = {furniture = true},
+ }, "Encrust Furniture With "..glass_name))
+
+ table.insert(result, reaction_entry(job_types.EncrustWithGlass, {
+ mat_type = glass_id,
+ item_category = {ammo = true},
+ }, "Encrust Ammo With "..glass_name))
+ end
+ end
+
+ -- Dyeing
+ table.insert(result, reaction_entry(job_types.DyeThread))
+ table.insert(result, reaction_entry(job_types.DyeCloth))
+
+ -- Sew Image
+ cloth_mats = {materials.cloth, materials.silk, materials.yarn, materials.leather}
+ for _, material in ipairs(cloth_mats) do
+ material_reactions(result, {{job_types.SewImage, "Sew", "Image"}}, material)
+ end
+
+ for _, spec in ipairs{materials.bone, materials.shell, materials.tooth, materials.horn, materials.pearl} do
+ material_reactions(result, {{job_types.DecorateWith, "Decorate With"}}, spec)
+ end
+
+ table.insert(result, reaction_entry(job_types.MakeTotem))
+ table.insert(result, reaction_entry(job_types.ButcherAnimal))
+ table.insert(result, reaction_entry(job_types.MillPlants))
+ table.insert(result, reaction_entry(job_types.MakePotashFromLye))
+ table.insert(result, reaction_entry(job_types.MakePotashFromAsh))
+
+ -- Kitchen
+ table.insert(result, reaction_entry(job_types.PrepareMeal, {mat_type = 2}, "Prepare Easy Meal"))
+ table.insert(result, reaction_entry(job_types.PrepareMeal, {mat_type = 3}, "Prepare Fine Meal"))
+ table.insert(result, reaction_entry(job_types.PrepareMeal, {mat_type = 4}, "Prepare Lavish Meal"))
+
+ -- Brew Drink
+ table.insert(result, reaction_entry(job_types.BrewDrink))
+
+ -- Weaving
+ table.insert(result, reaction_entry(job_types.WeaveCloth, {material_category = {plant = true}}, "Weave Thread into Cloth"))
+ table.insert(result, reaction_entry(job_types.WeaveCloth, {material_category = {silk = true}}, "Weave Thread into Silk"))
+ table.insert(result, reaction_entry(job_types.WeaveCloth, {material_category = {yarn = true}}, "Weave Yarn into Cloth"))
+
+ -- Extracts, farmer's workshop, and wood burning
+ table.insert(result, reaction_entry(job_types.ExtractFromPlants))
+ table.insert(result, reaction_entry(job_types.ExtractFromRawFish))
+ table.insert(result, reaction_entry(job_types.ExtractFromLandAnimal))
+ table.insert(result, reaction_entry(job_types.PrepareRawFish))
+ table.insert(result, reaction_entry(job_types.MakeCheese))
+ table.insert(result, reaction_entry(job_types.MilkCreature))
+ table.insert(result, reaction_entry(job_types.ShearCreature))
+ table.insert(result, reaction_entry(job_types.SpinThread))
+ table.insert(result, reaction_entry(job_types.MakeLye))
+ table.insert(result, reaction_entry(job_types.ProcessPlants))
+ table.insert(result, reaction_entry(job_types.ProcessPlantsBag))
+ table.insert(result, reaction_entry(job_types.ProcessPlantsVial))
+ table.insert(result, reaction_entry(job_types.ProcessPlantsBarrel))
+ table.insert(result, reaction_entry(job_types.MakeCharcoal))
+ table.insert(result, reaction_entry(job_types.MakeAsh))
+
+ -- Reactions defined in the raws.
+ -- Not all reactions are allowed to the civilization.
+ -- That includes "Make sharp rock" by default.
+ local entity = df.historical_entity.find(df.global.ui.civ_id)
+ for _, reaction_id in ipairs(entity.entity_raw.workshops.permitted_reaction_id) do
+ local reaction = df.global.world.raws.reactions[reaction_id]
+ local name = string.gsub(reaction.name, "^.", string.upper)
+ table.insert(result, reaction_entry(job_types.CustomReaction, {reaction_name = reaction.code}, name))
+ end
+
+ -- Metal forging
+ local itemdefs = df.global.world.raws.itemdefs
+ for rock_id = 0, #rock_types - 1 do
+ local material = rock_types[rock_id].material
+ local rock_name = material.state_adj.Solid
+ local mat_flags = {
+ adjective = rock_name,
+ management = {mat_type = 0, mat_index = rock_id},
+ verb = "Forge",
+ }
+
+ if material.flags.IS_METAL then
+ table.insert(result, reaction_entry(job_types.StudWith, mat_flags.management, "Stud With "..rock_name))
+
+ if material.flags.ITEMS_WEAPON then
+ -- Todo: Are these really the right flags to check?
+ resource_reactions(result, job_types.MakeWeapon, mat_flags, entity.resources.weapon_type, itemdefs.weapons, {
+ permissible = (function(itemdef) return itemdef.skill_ranged == -1 end),
+ })
+
+ -- Is this entirely disconnected from the entity?
+ material_reactions(result, {{MakeBallistaArrowHead, "Forge", "Ballista Arrow Head"}}, mat_flags)
+
+ resource_reactions(result, job_types.MakeTrapComponent, mat_flags, entity.resources.trapcomp_type, itemdefs.trapcomps, {
+ adjective = true,
+ })
+
+ resource_reactions(result, job_types.AssembleSiegeAmmo, mat_flags, entity.resources.siegeammo_type, itemdefs.siege_ammo, {
+ verb = "Assemble",
+ })
+ end
+
+ if material.flags.ITEMS_WEAPON_RANGED then
+ resource_reactions(result, job_types.MakeWeapon, mat_flags, entity.resources.weapon_type, itemdefs.weapons, {
+ permissible = (function(itemdef) return itemdef.skill_ranged >= 0 end),
+ })
+ end
+
+ if material.flags.ITEMS_DIGGER then
+ -- Todo: Ranged or training digging weapons?
+ resource_reactions(result, job_types.MakeWeapon, mat_flags, entity.resources.digger_type, itemdefs.weapons, {
+ })
+ end
+
+ if material.flags.ITEMS_AMMO then
+ resource_reactions(result, job_types.MakeAmmo, mat_flags, entity.resources.ammo_type, itemdefs.ammo, {
+ name_field = "name_plural",
+ })
+ end
+
+ if material.flags.ITEMS_ANVIL then
+ material_reactions(result, {{job_types.ForgeAnvil, "Forge", "Anvil"}}, mat_flags)
+ end
+
+ if material.flags.ITEMS_ARMOR then
+ local metalclothing = (function(itemdef) return itemdef.props.flags.METAL end)
+ clothing_reactions(result, mat_flags, metalclothing)
+ resource_reactions(result, job_types.MakeShield, mat_flags, entity.resources.shield_type, itemdefs.shields, {
+ })
+ end
+
+ if material.flags.ITEMS_SOFT then
+ local metalclothing = (function(itemdef) return itemdef.props.flags.SOFT and not itemdef.props.flags.METAL end)
+ clothing_reactions(result, mat_flags, metalclothing)
+ end
+
+ if material.flags.ITEMS_HARD then
+ resource_reactions(result, job_types.MakeTool, mat_flags, entity.resources.tool_type, itemdefs.tools, {
+ permissible = (function(itemdef) return itemdef.flags.HARD_MAT end),
+ capitalize = true,
+ })
+ end
+
+ if material.flags.ITEMS_METAL then
+ resource_reactions(result, job_types.MakeTool, mat_flags, entity.resources.tool_type, itemdefs.tools, {
+ permissible = (function(itemdef) return itemdef.flags.METAL_MAT end),
+ capitalize = true,
+ })
+ end
+
+ if material.flags.ITEMS_HARD then
+ material_reactions(result, {
+ {job_types.ConstructDoor, "Construct", "Door"},
+ {job_types.ConstructFloodgate, "Construct", "Floodgate"},
+ {job_types.ConstructHatchCover, "Construct", "Hatch Cover"},
+ {job_types.ConstructGrate, "Construct", "Grate"},
+ {job_types.ConstructThrone, "Construct", "Throne"},
+ {job_types.ConstructCoffin, "Construct", "Sarcophagus"},
+ {job_types.ConstructTable, "Construct", "Table"},
+ {job_types.ConstructSplint, "Construct", "Splint"},
+ {job_types.ConstructCrutch, "Construct", "Crutch"},
+ {job_types.ConstructArmorStand, "Construct", "Armor Stand"},
+ {job_types.ConstructWeaponRack, "Construct", "Weapon Rack"},
+ {job_types.ConstructCabinet, "Construct", "Cabinet"},
+ {job_types.MakeGoblet, "Forge", "Goblet"},
+ {job_types.MakeInstrument, "Forge", "Instrument"},
+ {job_types.MakeToy, "Forge", "Toy"},
+ {job_types.ConstructStatue, "Construct", "Statue"},
+ {job_types.ConstructBlocks, "Construct", "Blocks"},
+ {job_types.MakeAnimalTrap, "Forge", "Animal Trap"},
+ {job_types.MakeBarrel, "Forge", "Barrel"},
+ {job_types.MakeBucket, "Forge", "Bucket"},
+ {job_types.ConstructBin, "Construct", "Bin"},
+ {job_types.MakePipeSection, "Forge", "Pipe Section"},
+ {job_types.MakeCage, "Forge", "Cage"},
+ {job_types.MintCoins, "Mint", "Coins"},
+ {job_types.ConstructChest, "Construct", "Chest"},
+ {job_types.MakeFlask, "Forge", "Flask"},
+ {job_types.MakeChain, "Forge", "Chain"},
+ {job_types.MakeCrafts, "Make", "Crafts"},
+ }, mat_flags)
+ end
+
+ if material.flags.ITEMS_SOFT then
+ material_reactions(result, {
+ {job_types.MakeBackpack, "Make", "Backpack"},
+ {job_types.MakeQuiver, "Make", "Quiver"},
+ {job_types.ConstructCatapultParts, "Construct", "Catapult Parts"},
+ {job_types.ConstructBallistaParts, "Construct", "Ballista Parts"},
+ }, mat_flags)
+ end
+ end
+ end
+
+ -- Traction Bench
+ table.insert(result, reaction_entry(job_types.ConstructTractionBench))
+
+ -- Non-metal weapons
+ resource_reactions(result, job_types.MakeWeapon, materials.wood, entity.resources.weapon_type, itemdefs.weapons, {
+ permissible = (function(itemdef) return itemdef.skill_ranged >= 0 end),
+ })
+
+ resource_reactions(result, job_types.MakeWeapon, materials.wood, entity.resources.training_weapon_type, itemdefs.weapons, {
+ })
+
+ resource_reactions(result, job_types.MakeWeapon, materials.bone, entity.resources.weapon_type, itemdefs.weapons, {
+ permissible = (function(itemdef) return itemdef.skill_ranged >= 0 end),
+ })
+
+ resource_reactions(result, job_types.MakeWeapon, materials.rock, entity.resources.weapon_type, itemdefs.weapons, {
+ permissible = (function(itemdef) return itemdef.flags.CAN_STONE end),
+ })
+
+ -- Wooden items
+ -- Closely related to the ITEMS_HARD list.
+ material_reactions(result, {
+ {job_types.ConstructDoor, "Construct", "Door"},
+ {job_types.ConstructFloodgate, "Construct", "Floodgate"},
+ {job_types.ConstructHatchCover, "Construct", "Hatch Cover"},
+ {job_types.ConstructGrate, "Construct", "Grate"},
+ {job_types.ConstructThrone, "Construct", "Chair"},
+ {job_types.ConstructCoffin, "Construct", "Casket"},
+ {job_types.ConstructTable, "Construct", "Table"},
+ {job_types.ConstructArmorStand, "Construct", "Armor Stand"},
+ {job_types.ConstructWeaponRack, "Construct", "Weapon Rack"},
+ {job_types.ConstructCabinet, "Construct", "Cabinet"},
+ {job_types.MakeGoblet, "Make", "Cup"},
+ {job_types.MakeInstrument, "Make", "Instrument"},
+ }, materials.wood)
+
+ resource_reactions(result, job_types.MakeTool, materials.wood, entity.resources.tool_type, itemdefs.tools, {
+ -- permissible = (function(itemdef) return itemdef.flags.WOOD_MAT end),
+ capitalize = true,
+ })
+
+ material_reactions(result, {
+ {job_types.MakeToy, "Make", "Toy"},
+ {job_types.ConstructBlocks, "Construct", "Blocks"},
+ {job_types.ConstructSplint, "Construct", "Splint"},
+ {job_types.ConstructCrutch, "Construct", "Crutch"},
+ {job_types.MakeAnimalTrap, "Make", "Animal Trap"},
+ {job_types.MakeBarrel, "Make", "Barrel"},
+ {job_types.MakeBucket, "Make", "Bucket"},
+ {job_types.ConstructBin, "Construct", "Bin"},
+ {job_types.MakeCage, "Make", "Cage"},
+ {job_types.MakePipeSection, "Make", "Pipe Section"},
+ }, materials.wood)
+
+ resource_reactions(result, job_types.MakeTrapComponent, materials.wood, entity.resources.trapcomp_type, itemdefs.trapcomps, {
+ permissible = (function(itemdef) return itemdef.flags.WOOD end),
+ adjective = true,
+ })
+
+ -- Rock items
+ material_reactions(result, {
+ {job_types.ConstructDoor, "Construct", "Door"},
+ {job_types.ConstructFloodgate, "Construct", "Floodgate"},
+ {job_types.ConstructHatchCover, "Construct", "Hatch Cover"},
+ {job_types.ConstructGrate, "Construct", "Grate"},
+ {job_types.ConstructThrone, "Construct", "Throne"},
+ {job_types.ConstructCoffin, "Construct", "Coffin"},
+ {job_types.ConstructTable, "Construct", "Table"},
+ {job_types.ConstructArmorStand, "Construct", "Armor Stand"},
+ {job_types.ConstructWeaponRack, "Construct", "Weapon Rack"},
+ {job_types.ConstructCabinet, "Construct", "Cabinet"},
+ {job_types.MakeGoblet, "Make", "Mug"},
+ {job_types.MakeInstrument, "Make", "Instrument"},
+ }, materials.rock)
+
+ resource_reactions(result, job_types.MakeTool, materials.rock, entity.resources.tool_type, itemdefs.tools, {
+ permissible = (function(itemdef) return itemdef.flags.HARD_MAT end),
+ capitalize = true,
+ })
+
+ material_reactions(result, {
+ {job_types.MakeToy, "Make", "Toy"},
+ {job_types.ConstructQuern, "Construct", "Quern"},
+ {job_types.ConstructMillstone, "Construct", "Millstone"},
+ {job_types.ConstructSlab, "Construct", "Slab"},
+ {job_types.ConstructStatue, "Construct", "Statue"},
+ {job_types.ConstructBlocks, "Construct", "Blocks"},
+ }, materials.rock)
+
+ -- Glass items
+ for _, mat_info in ipairs(glasses) do
+ material_reactions(result, {
+ {job_types.ConstructDoor, "Construct", "Portal"},
+ {job_types.ConstructFloodgate, "Construct", "Floodgate"},
+ {job_types.ConstructHatchCover, "Construct", "Hatch Cover"},
+ {job_types.ConstructGrate, "Construct", "Grate"},
+ {job_types.ConstructThrone, "Construct", "Throne"},
+ {job_types.ConstructCoffin, "Construct", "Coffin"},
+ {job_types.ConstructTable, "Construct", "Table"},
+ {job_types.ConstructArmorStand, "Construct", "Armor Stand"},
+ {job_types.ConstructWeaponRack, "Construct", "Weapon Rack"},
+ {job_types.ConstructCabinet, "Construct", "Cabinet"},
+ {job_types.MakeGoblet, "Make", "Goblet"},
+ {job_types.MakeInstrument, "Make", "Instrument"},
+ }, mat_info)
+
+ resource_reactions(result, job_types.MakeTool, mat_info, entity.resources.tool_type, itemdefs.tools, {
+ permissible = (function(itemdef) return itemdef.flags.HARD_MAT end),
+ capitalize = true,
+ })
+
+ material_reactions(result, {
+ {job_types.MakeToy, "Make", "Toy"},
+ {job_types.ConstructStatue, "Construct", "Statue"},
+ {job_types.ConstructBlocks, "Construct", "Blocks"},
+ {job_types.MakeCage, "Make", "Terrarium"},
+ {job_types.MakePipeSection, "Make", "Tube"},
+ }, mat_info)
+
+ resource_reactions(result, job_types.MakeTrapComponent, mat_info, entity.resources.trapcomp_type, itemdefs.trapcomps, {
+ adjective = true,
+ })
+ end
+
+ -- Bed, specified as wooden.
+ table.insert(result, reaction_entry(job_types.ConstructBed, materials.wood.management))
+
+ -- Windows
+ for _, mat_info in ipairs(glasses) do
+ material_reactions(result, {
+ {job_types.MakeWindow, "Make", "Window"},
+ }, mat_info)
+ end
+
+ -- Rock Mechanisms
+ table.insert(result, reaction_entry(job_types.ConstructMechanisms, materials.rock.management))
+
+ resource_reactions(result, job_types.AssembleSiegeAmmo, materials.wood, entity.resources.siegeammo_type, itemdefs.siege_ammo, {
+ verb = "Assemble",
+ })
+
+ for _, mat_info in ipairs(glasses) do
+ material_reactions(result, {
+ {job_types.MakeRawGlass, "Make Raw", nil},
+ }, mat_info)
+ end
+
+ material_reactions(result, {
+ {job_types.MakeBackpack, "Make", "Backpack"},
+ {job_types.MakeQuiver, "Make", "Quiver"},
+ }, materials.leather)
+
+ for _, material in ipairs(cloth_mats) do
+ clothing_reactions(result, material, (function(itemdef) return itemdef.props.flags[material.clothing_flag or "SOFT"] end))
+ end
+
+ -- Boxes, Bags, and Ropes
+ boxmats = {
+ {mats = {materials.wood}, box = "Chest"},
+ {mats = {materials.rock}, box = "Coffer"},
+ {mats = glasses, box = "Box", flask = "Vial"},
+ {mats = {materials.cloth}, box = "Bag", chain = "Rope"},
+ {mats = {materials.leather}, box = "Bag", flask = "Waterskin"},
+ {mats = {materials.silk, materials.yarn}, box = "Bag", chain = "Rope"},
+ }
+ for _, boxmat in ipairs(boxmats) do
+ for _, mat in ipairs(boxmat.mats) do
+ material_reactions(result, {{job_types.ConstructChest, "Construct", boxmat.box}}, mat)
+ if boxmat.chain then
+ material_reactions(result, {{job_types.MakeChain, "Make", boxmat.chain}}, mat)
+ end
+ if boxmat.flask then
+ material_reactions(result, {{job_types.MakeFlask, "Make", boxmat.flask}}, mat)
+ end
+ end
+ end
+
+ for _, mat in ipairs{
+ materials.wood,
+ materials.rock,
+ materials.cloth,
+ materials.leather,
+ materials.shell,
+ materials.bone,
+ materials.silk,
+ materials.tooth,
+ materials.horn,
+ materials.pearl,
+ materials.yarn,
+ } do
+ material_reactions(result, {{job_types.MakeCrafts, "Make", "Crafts"}}, mat)
+ end
+
+ -- Siege engine parts
+ table.insert(result, reaction_entry(job_types.ConstructCatapultParts, materials.wood.management))
+ table.insert(result, reaction_entry(job_types.ConstructBallistaParts, materials.wood.management))
+
+ for _, mat in ipairs{materials.wood, materials.bone} do
+ resource_reactions(result, job_types.MakeAmmo, mat, entity.resources.ammo_type, itemdefs.ammo, {
+ name_field = "name_plural",
+ })
+ end
+
+ -- BARRED and SCALED as flag names don't quite seem to fit, here.
+ clothing_reactions(result, materials.bone, (function(itemdef) return itemdef.props.flags.BARRED end))
+ clothing_reactions(result, materials.shell, (function(itemdef) return itemdef.props.flags.SCALED end))
+
+ for _, mat in ipairs{materials.wood, materials.leather} do
+ resource_reactions(result, job_types.MakeShield, mat, entity.resources.shield_type, itemdefs.shields, {})
+ end
+
+ -- Melt a Metal Object
+ table.insert(result, reaction_entry(job_types.MeltMetalObject))
+
+ return result
+end
+
+screen = gui.FramedScreen {
+ frame_title = "Select Stockpile Order",
+}
+
+function screen:onRenderBody(dc)
+ -- Emulates the built-in manager screen.
+ dc:seek(1, 1):string("Type in parts of the name to narrow your search. ", COLOR_WHITE)
+ dc:string(gui.getKeyDisplay("LEAVESCREEN"), COLOR_LIGHTGREEN)
+ dc:string(" to abort.", COLOR_WHITE)
+ dc:seek(1, PageSize + 5):string(self.search_string, COLOR_LIGHTCYAN)
+ for _, item in ipairs(self.displayed) do
+ dc:seek(item.x, item.y):string(item.name, item.color)
+ end
+end
+
+function screen:onInput(keys)
+ if keys.LEAVESCREEN then
+ self:dismiss()
+ elseif keys.SELECT then
+ self:dismiss()
+ local selected = self.reactions[self.position]
+ if selected then
+ store_order(self.stockpile, selected.index)
+ end
+ elseif keys.STANDARDSCROLL_UP then
+ self.position = self.position - 1
+ elseif keys.STANDARDSCROLL_DOWN then
+ self.position = self.position + 1
+ elseif keys.STANDARDSCROLL_LEFT then
+ self.position = self.position - PageSize
+ elseif keys.STANDARDSCROLL_RIGHT then
+ self.position = self.position + PageSize
+ elseif keys.STANDARDSCROLL_PAGEUP then
+ -- Moves to the first item displayed on the new page, for some reason.
+ self.position = self.position - PageSize*2 - ((self.position-1) % (PageSize*2))
+ elseif keys.STANDARDSCROLL_PAGEDOWN then
+ -- Moves to the first item displayed on the new page, for some reason.
+ self.position = self.position + PageSize*2 - ((self.position-1) % (PageSize*2))
+ elseif keys.STRING_A000 then
+ -- This seems like an odd way to check for Backspace.
+ self.search_string = string.sub(self.search_string, 1, -2)
+ elseif keys._STRING and keys._STRING >= 32 then
+ -- This interface only accepts letters and spaces.
+ local char = string.char(keys._STRING)
+ if char == " " or string.find(char, "^%a") then
+ self.search_string = self.search_string .. string.upper(char)
+ end
+ end
+
+ self:refilter()
+end
+
+function screen:reset(stockpile)
+ self.stockpile = stockpile
+ self.search_string = ""
+ self.position = 1
+ self:refilter()
+end
+
+function matchall(haystack, needles)
+ for _, needle in ipairs(needles) do
+ if not string.find(haystack, needle) then
+ return false
+ end
+ end
+
+ return true
+end
+
+function splitstring(full, pattern)
+ local last = string.len(full)
+ local result = {}
+ local n = 1
+ while n <= last do
+ local start, stop = string.find(full, pattern, n)
+ if not start then
+ result[#result+1] = string.sub(full, n)
+ break
+ elseif start > n then
+ result[#result+1] = string.sub(full, n, start - 1)
+ end
+
+ if stop < n then
+ -- The pattern matches an empty string.
+ -- Avoid an infinite loop.
+ break
+ end
+
+ n = stop + 1
+ end
+
+ return result
+end
+
+function screen:refilter()
+ local filtered = {}
+ local needles = splitstring(self.search_string, " ")
+ for key, value in ipairs(reaction_list) do
+ if matchall(string.upper(value.name), needles) then
+ filtered[#filtered+1] = {
+ index = key,
+ name = value.name
+ }
+ end
+ end
+
+ if self.position < 1 then
+ self.position = #filtered
+ elseif self.position > #filtered then
+ self.position = 1
+ end
+
+ local start = 1
+ while self.position >= start + PageSize*2 do
+ start = start + PageSize*2
+ end
+
+ local displayed = {}
+ for n = 0, PageSize*2 - 1 do
+ local item = filtered[start + n]
+ if not item then
+ break
+ end
+ local name = item.name
+
+ local x = 1
+ local y = FirstRow + n
+ if n >= PageSize then
+ x = CenterCol
+ y = y - PageSize
+ name = " "..name
+ end
+
+ local color = COLOR_CYAN
+ if start + n == self.position then
+ color = COLOR_LIGHTCYAN
+ end
+
+ displayed[n + 1] = {
+ x = x,
+ y = y,
+ name = name,
+ color = color,
+ }
+ end
+
+ self.reactions = filtered
+ self.displayed = displayed
+end
+
+function store_order(stockpile, order_number)
+ local name = reaction_list[order_number].name
+ -- print("Setting stockpile #"..stockpile.stockpile_number.." to "..name.." (#"..order_number..")")
+ local saved = saved_orders[stockpile.id]
+ if saved then
+ saved.entry.value = name
+ saved.entry.ints[entry_ints.order_number] = order_number
+ saved.entry:save()
+ else
+ saved_orders[stockpile.id] = {
+ stockpile = stockpile,
+ entry = dfhack.persistent.save{
+ key = "stockflow/entry/"..stockpile.id,
+ value = name,
+ ints = {
+ stockpile.id,
+ order_number,
+ 1,
+ },
+ },
+ }
+ end
+end
+
+-- Compare the job specification of two orders.
+function orders_match(a, b)
+ local fields = {
+ "job_type",
+ "item_subtype",
+ "reaction_name",
+ "mat_type",
+ "mat_index",
+ }
+
+ for _, fieldname in ipairs(fields) do
+ if a[fieldname] ~= b[fieldname] then
+ return false
+ end
+ end
+
+ local subtables = {
+ "item_category",
+ "material_category",
+ }
+
+ for _, fieldname in ipairs(subtables) do
+ local aa = a[fieldname]
+ local bb = b[fieldname]
+ for key, value in ipairs(aa) do
+ if bb[key] ~= value then
+ return false
+ end
+ end
+ end
+
+ return true
+end
+
+-- Reduce the quantity by the number of matching orders in the queue.
+function order_quantity(order, quantity)
+ local amount = quantity
+ for _, managed in ipairs(df.global.world.manager_orders) do
+ if orders_match(order, managed) then
+ amount = amount - managed.amount_left
+ if amount < 0 then
+ return 0
+ end
+ end
+ end
+
+ if amount > 30 then
+ -- Respect the quantity limit.
+ -- With this many in the queue, we can wait for the next cycle.
+ return 30
+ end
+
+ return amount
+end
+
+-- Place a new copy of the order onto the manager's queue.
+function create_orders(order, amount)
+ local new_order = order:new()
+ new_order.amount_left = amount
+ new_order.amount_total = amount
+ -- Todo: Create in a validated state if the fortress is small enough?
+ new_order.is_validated = 0
+ df.global.world.manager_orders:insert('#', new_order)
+end
+
+function findItemsAtTile(x, y, z)
+ -- There should be a faster and easier way to do this...
+ local found = {}
+ for _, item in ipairs(df.global.world.items.all) do
+ -- local ix, iy, iz = dfhack.items.getPosition(item)
+ if item.pos.x == x and item.pos.y == y and
+ item.pos.z == z and item.flags.on_ground then
+ found[#found+1] = item
+ end
+ end
+
+ return found
+end
+
+function countContents(container, settings)
+ local total = 0
+ local blocking = false
+ for _, item in ipairs(dfhack.items.getContainedItems(container)) do
+ if item.flags.container then
+ -- Recursively count the total of items contained.
+ -- Not likely to go more than two levels deep.
+ local subtotal, subblocked = countContents(item, settings)
+ if subtotal > 0 then
+ -- Ignore the inner container itself;
+ -- generally, only the contained items matter.
+ total = total + subtotal
+ elseif subblocked then
+ blocking = true
+ elseif matches_stockpile(item, settings) then
+ -- The container may or may not be empty,
+ -- but is stockpiled as a container itself.
+ total = total + 1
+ else
+ blocking = true
+ end
+ elseif matches_stockpile(item, settings) then
+ total = total + 1
+ else
+ blocking = true
+ end
+ end
+
+ return total, blocking
+end
+
+function check_stockpiles(verbose)
+ local result = {}
+ for _, spec in pairs(saved_orders) do
+ local trigger = triggers[spec.entry.ints[entry_ints.trigger_number]]
+ if trigger and trigger.divisor then
+ local reaction = spec.entry.ints[entry_ints.order_number]
+ local filled, empty = check_pile(spec.stockpile, verbose)
+ local amount = trigger.filled and filled or empty
+ amount = (amount - (amount % trigger.divisor)) / trigger.divisor
+ result[reaction] = (result[reaction] or 0) + amount
+ end
+ end
+
+ return result
+end
+
+function check_pile(sp, verbose)
+ local numspaces = 0
+ local filled = 0
+ local empty = 0
+ for y = sp.y1, sp.y2 do
+ for x = sp.x1, sp.x2 do
+ -- Sadly, the obvious check currently fails when y == sp.y2
+ if dfhack.buildings.containsTile(sp, x, y) or
+ (y == sp.y2 and dfhack.buildings.findAtTile(x, y, sp.z) == sp) then
+ numspaces = numspaces + 1
+ local designation, occupancy = dfhack.maps.getTileFlags(x, y, sp.z)
+ if not designation.liquid_type then
+ if not occupancy.item then
+ empty = empty + 1
+ else
+ local item_count, blocked = count_pile_items(sp, x, y)
+ if item_count > 0 then
+ filled = filled + item_count
+ elseif not blocked then
+ empty = empty + 1
+ end
+ end
+ end
+ end
+ end
+ end
+
+ if verbose then
+ print("Stockpile #"..sp.stockpile_number,
+ string.format("%3d spaces", numspaces),
+ string.format("%4d items", filled),
+ string.format("%4d empty spaces", empty))
+ end
+
+ return filled, empty
+end
+
+function count_pile_items(sp, x, y)
+ local item_count = 0
+ local blocked = false
+ for _, item in ipairs(findItemsAtTile(x, y, sp.z)) do
+ if item:isAssignedToThisStockpile(sp.id) then
+ -- This is a bin or barrel associated with the stockpile.
+ -- If it's empty, it doesn't count as blocking the stockpile space.
+ -- Oddly, when empty, item.flags.container might be false.
+ local subtotal, subblocked = countContents(item, sp.settings)
+ item_count = item_count + subtotal
+ if subblocked then
+ blocked = true
+ end
+ elseif matches_stockpile(item, sp.settings) then
+ item_count = item_count + 1
+ else
+ blocked = true
+ end
+ end
+
+ return item_count, blocked
+end
+
+function matches_stockpile(item, settings)
+ -- Check whether the item matches the stockpile.
+ -- FIXME: This is starting to look like a whole lot of work.
+ if df.item_barst:is_instance(item) then
+ return settings.flags.bars_blocks
+ elseif df.item_blocksst:is_instance(item) then
+ return settings.flags.bars_blocks
+ elseif df.item_smallgemst:is_instance(item) then
+ return settings.flags.gems
+ elseif df.item_boulderst:is_instance(item) then
+ return settings.flags.stone
+ elseif df.item_woodst:is_instance(item) then
+ return settings.flags.wood
+ elseif df.item_seedsst:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_meatst:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_plantst:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_leavesst:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_cheesest:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_globst:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_fishst:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_fish_rawst:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_foodst:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_drinkst:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_eggst:is_instance(item) then
+ return settings.flags.food
+ elseif df.item_skin_tannedst:is_instance(item) then
+ return settings.flags.leather
+ elseif df.item_remainsst:is_instance(item) then
+ return settings.flags.refuse
+ elseif df.item_verminst:is_instance(item) then
+ return settings.flags.animals
+ elseif df.item_petst:is_instance(item) then
+ return settings.flags.animals
+ elseif df.item_threadst:is_instance(item) then
+ return settings.flags.cloth
+ end
+
+ return true
+end
+
+return _ENV
diff --git a/plugins/mousequery.cpp b/plugins/mousequery.cpp
index 9cc6bb783..0fe337017 100644
--- a/plugins/mousequery.cpp
+++ b/plugins/mousequery.cpp
@@ -28,7 +28,7 @@ using namespace df::enums::ui_sidebar_mode;
DFHACK_PLUGIN("mousequery");
-#define PLUGIN_VERSION 0.17
+#define PLUGIN_VERSION 0.18
static int32_t last_clicked_x, last_clicked_y, last_clicked_z;
static int32_t last_pos_x, last_pos_y, last_pos_z;
@@ -539,6 +539,13 @@ struct mousequery_hook : public df::viewscreen_dwarfmodest
if (mx < 1 || mx > right_margin - 2 || my < 1 || my > gps->dimy - 2)
mpos_valid = false;
+ // Check if in lever binding mode
+ if (Gui::getFocusString(Core::getTopViewscreen()) ==
+ "dwarfmode/QueryBuilding/Some/Lever/AddJob")
+ {
+ return;
+ }
+
if (mpos_valid)
{
if (mpos.x != last_move_pos.x || mpos.y != last_move_pos.y || mpos.z != last_move_pos.z)
diff --git a/plugins/reveal.cpp b/plugins/reveal.cpp
index 91ab75686..9bd63e47e 100644
--- a/plugins/reveal.cpp
+++ b/plugins/reveal.cpp
@@ -449,22 +449,22 @@ command_result revflood(color_ostream &out, vector & params)
}
if(sides)
{
- flood.push(foo(DFCoord(current.x + 1, current.y ,current.z),0));
- flood.push(foo(DFCoord(current.x + 1, current.y + 1 ,current.z),0));
- flood.push(foo(DFCoord(current.x, current.y + 1 ,current.z),0));
- flood.push(foo(DFCoord(current.x - 1, current.y + 1 ,current.z),0));
- flood.push(foo(DFCoord(current.x - 1, current.y ,current.z),0));
- flood.push(foo(DFCoord(current.x - 1, current.y - 1 ,current.z),0));
- flood.push(foo(DFCoord(current.x, current.y - 1 ,current.z),0));
- flood.push(foo(DFCoord(current.x + 1, current.y - 1 ,current.z),0));
+ flood.push(foo(DFCoord(current.x + 1, current.y ,current.z),false));
+ flood.push(foo(DFCoord(current.x + 1, current.y + 1 ,current.z),false));
+ flood.push(foo(DFCoord(current.x, current.y + 1 ,current.z),false));
+ flood.push(foo(DFCoord(current.x - 1, current.y + 1 ,current.z),false));
+ flood.push(foo(DFCoord(current.x - 1, current.y ,current.z),false));
+ flood.push(foo(DFCoord(current.x - 1, current.y - 1 ,current.z),false));
+ flood.push(foo(DFCoord(current.x, current.y - 1 ,current.z),false));
+ flood.push(foo(DFCoord(current.x + 1, current.y - 1 ,current.z),false));
}
if(above)
{
- flood.push(foo(DFCoord(current.x, current.y ,current.z + 1),1));
+ flood.push(foo(DFCoord(current.x, current.y ,current.z + 1),true));
}
if(below)
{
- flood.push(foo(DFCoord(current.x, current.y ,current.z - 1),0));
+ flood.push(foo(DFCoord(current.x, current.y ,current.z - 1),false));
}
}
MCache->WriteAll();
diff --git a/plugins/ruby/ruby.rb b/plugins/ruby/ruby.rb
index 47924dcdf..edce8ac84 100644
--- a/plugins/ruby/ruby.rb
+++ b/plugins/ruby/ruby.rb
@@ -210,9 +210,17 @@ module DFHack
out.last << wl.words[name.words[1]].forms[name.parts_of_speech[1]] if name.words[1] >= 0
end
if name.words[5] >= 0
- out << 'the '
+ out << 'the'
out.last.capitalize! if out.length == 1
- (2..5).each { |i| out.last << wl.words[name.words[i]].forms[name.parts_of_speech[i]] if name.words[i] >= 0 }
+ out << wl.words[name.words[2]].forms[name.parts_of_speech[2]] if name.words[2] >= 0
+ out << wl.words[name.words[3]].forms[name.parts_of_speech[3]] if name.words[3] >= 0
+ if name.words[4] >= 0
+ out << wl.words[name.words[4]].forms[name.parts_of_speech[4]]
+ out.last << '-'
+ else
+ out << ''
+ end
+ out.last << wl.words[name.words[5]].forms[name.parts_of_speech[5]]
end
if name.words[6] >= 0
out << 'of'
diff --git a/plugins/search.cpp b/plugins/search.cpp
index e0213bf06..72001275c 100644
--- a/plugins/search.cpp
+++ b/plugins/search.cpp
@@ -180,6 +180,10 @@ public:
{
// Query typing mode
+ if (input->empty())
+ {
+ return false;
+ }
df::interface_key last_token = *input->rbegin();
if (last_token >= interface_key::STRING_A032 && last_token <= interface_key::STRING_A126)
{
diff --git a/plugins/stockflow.cpp b/plugins/stockflow.cpp
new file mode 100644
index 000000000..adff25a04
--- /dev/null
+++ b/plugins/stockflow.cpp
@@ -0,0 +1,393 @@
+/*
+ * Stockflow plugin.
+ * For best effect, place "stockflow enable" in your dfhack.init configuration,
+ * or set AUTOENABLE to true.
+ */
+
+#include "uicommon.h"
+#include "LuaTools.h"
+
+#include "df/building_stockpilest.h"
+#include "df/job.h"
+#include "df/viewscreen_dwarfmodest.h"
+
+#include "modules/Gui.h"
+#include "modules/Maps.h"
+#include "modules/World.h"
+
+using namespace DFHack;
+using namespace std;
+
+using df::global::world;
+using df::global::ui;
+using df::building_stockpilest;
+
+DFHACK_PLUGIN("stockflow");
+
+#define AUTOENABLE false
+#ifdef DFHACK_PLUGIN_IS_ENABLED
+DFHACK_PLUGIN_IS_ENABLED(enabled);
+#else
+bool enabled = false;
+#endif
+
+const char *tagline = "Allows the fortress bookkeeper to queue jobs through the manager.";
+const char *usage = (
+ " stockflow enable\n"
+ " Enable the plugin.\n"
+ " stockflow disable\n"
+ " Disable the plugin.\n"
+ " stockflow list\n"
+ " List any work order settings for your stockpiles.\n"
+ " stockflow status\n"
+ " Display whether the plugin is enabled.\n"
+ "\n"
+ "While enabled, the 'q' menu of each stockpile will have two new options:\n"
+ " j: Select a job to order, from an interface like the manager's screen.\n"
+ " J: Cycle between several options for how many such jobs to order.\n"
+ "\n"
+ "Whenever the bookkeeper updates stockpile records, new work orders will\n"
+ "be placed on the manager's queue for each such selection, reduced by the\n"
+ "number of identical orders already in the queue.\n"
+);
+
+/*
+ * Lua interface.
+ * Currently calls out to Lua functions, but never back in.
+ */
+class LuaHelper {
+public:
+ void cycle(color_ostream &out) {
+ bool found = false;
+ for (df::job_list_link* link = &df::global::world->job_list; link != NULL; link = link->next) {
+ if (link->item == NULL) continue;
+ if (link->item->job_type == job_type::UpdateStockpileRecords) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found && !bookkeeping) {
+ command_method("start_bookkeeping", out);
+ bookkeeping = true;
+ } else if (bookkeeping && !found) {
+ command_method("finish_bookkeeping", out);
+ bookkeeping = false;
+ }
+ }
+
+ void init() {
+ stockpile_id = -1;
+ initialized = false;
+ bookkeeping = false;
+ }
+
+ bool reset(color_ostream &out, bool load) {
+ stockpile_id = -1;
+ bookkeeping = false;
+ if (load) {
+ return initialized = command_method("initialize_world", out);
+ } else if (initialized) {
+ initialized = false;
+ return command_method("clear_caches", out);
+ }
+
+ return true;
+ }
+
+ bool command_method(const char *method, color_ostream &out) {
+ // Calls a lua function with no parameters.
+
+ // Suspension is required for "stockflow enable" from the command line,
+ // but may be overkill for other situations.
+ CoreSuspender suspend;
+
+ auto L = Lua::Core::State;
+ Lua::StackUnwinder top(L);
+
+ if (!lua_checkstack(L, 1))
+ return false;
+
+ if (!Lua::PushModulePublic(out, L, "plugins.stockflow", method))
+ return false;
+
+ if (!Lua::SafeCall(out, L, 0, 0))
+ return false;
+
+ return true;
+ }
+
+ bool stockpile_method(const char *method, building_stockpilest *sp) {
+ // Combines the select_order and toggle_trigger method calls,
+ // because they share the same signature.
+ CoreSuspendClaimer suspend;
+
+ auto L = Lua::Core::State;
+ color_ostream_proxy out(Core::getInstance().getConsole());
+
+ Lua::StackUnwinder top(L);
+
+ if (!lua_checkstack(L, 2))
+ return false;
+
+ if (!Lua::PushModulePublic(out, L, "plugins.stockflow", method))
+ return false;
+
+ Lua::Push(L, sp);
+
+ if (!Lua::SafeCall(out, L, 1, 0))
+ return false;
+
+ // Invalidate the string cache.
+ stockpile_id = -1;
+
+ return true;
+ }
+
+ bool collect_settings(building_stockpilest *sp) {
+ // Find strings representing the job to order, and the trigger condition.
+ // There might be a memory leak here; C++ is odd like that.
+ auto L = Lua::Core::State;
+ color_ostream_proxy out(Core::getInstance().getConsole());
+
+ CoreSuspendClaimer suspend;
+ Lua::StackUnwinder top(L);
+
+ if (!lua_checkstack(L, 2))
+ return false;
+
+ if (!Lua::PushModulePublic(out, L, "plugins.stockflow", "stockpile_settings"))
+ return false;
+
+ Lua::Push(L, sp);
+
+ if (!Lua::SafeCall(out, L, 1, 2))
+ return false;
+
+ if (!lua_isstring(L, -1))
+ return false;
+
+ current_trigger = lua_tostring(L, -1);
+ lua_pop(L, 1);
+
+ if (!lua_isstring(L, -1))
+ return false;
+
+ current_job = lua_tostring(L, -1);
+ lua_pop(L, 1);
+
+ stockpile_id = sp->id;
+
+ return true;
+ }
+
+ void draw(building_stockpilest *sp) {
+ if (sp->id != stockpile_id) {
+ if (!collect_settings(sp)) {
+ Core::printerr("Stockflow job collection failed!\n");
+ return;
+ }
+ }
+
+ auto dims = Gui::getDwarfmodeViewDims();
+ int left_margin = dims.menu_x1 + 1;
+ int x = left_margin;
+ int y = 14;
+
+ OutputHotkeyString(x, y, current_job, "j", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED);
+ if (*current_trigger)
+ OutputHotkeyString(x, y, current_trigger, " J", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED);
+ }
+
+
+private:
+ long stockpile_id;
+ bool initialized;
+ bool bookkeeping;
+ const char *current_job;
+ const char *current_trigger;
+};
+
+static LuaHelper helper;
+
+#define DELTA_TICKS 600
+
+DFhackCExport command_result plugin_onupdate(color_ostream &out) {
+ if (!enabled)
+ return CR_OK;
+
+ if (!Maps::IsValid())
+ return CR_OK;
+
+ static decltype(world->frame_counter) last_frame_count = 0;
+
+ if (DFHack::World::ReadPauseState())
+ return CR_OK;
+
+ if (world->frame_counter - last_frame_count < DELTA_TICKS)
+ return CR_OK;
+
+ last_frame_count = world->frame_counter;
+
+ helper.cycle(out);
+
+ return CR_OK;
+}
+
+
+/*
+ * Interface hooks
+ */
+struct stockflow_hook : public df::viewscreen_dwarfmodest {
+ typedef df::viewscreen_dwarfmodest interpose_base;
+
+ bool handleInput(set *input) {
+ building_stockpilest *sp = get_selected_stockpile();
+ if (!sp)
+ return false;
+
+ if (input->count(interface_key::CUSTOM_J)) {
+ // Select a new order for this stockpile.
+ if (!helper.stockpile_method("select_order", sp)) {
+ Core::printerr("Stockflow order selection failed!\n");
+ }
+
+ return true;
+ } else if (input->count(interface_key::CUSTOM_SHIFT_J)) {
+ // Toggle the order trigger for this stockpile.
+ if (!helper.stockpile_method("toggle_trigger", sp)) {
+ Core::printerr("Stockflow trigger toggle failed!\n");
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) {
+ if (!handleInput(input))
+ INTERPOSE_NEXT(feed)(input);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, render, ()) {
+ INTERPOSE_NEXT(render)();
+
+ building_stockpilest *sp = get_selected_stockpile();
+ if (sp)
+ helper.draw(sp);
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(stockflow_hook, feed);
+IMPLEMENT_VMETHOD_INTERPOSE(stockflow_hook, render);
+
+
+static bool apply_hooks(color_ostream &out, bool enabling) {
+ if (enabling && !gps) {
+ out.printerr("Stockflow needs graphics.\n");
+ return false;
+ }
+
+ if (!INTERPOSE_HOOK(stockflow_hook, feed).apply(enabling) || !INTERPOSE_HOOK(stockflow_hook, render).apply(enabling)) {
+ out.printerr("Could not %s stockflow hooks!\n", enabling? "insert": "remove");
+ return false;
+ }
+
+ if (!helper.reset(out, enabling && Maps::IsValid())) {
+ out.printerr("Could not reset stockflow world data!\n");
+ return false;
+ }
+
+ return true;
+}
+
+static command_result stockflow_cmd(color_ostream &out, vector & parameters) {
+ bool desired = enabled;
+ if (parameters.size() == 1) {
+ if (parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1") {
+ desired = true;
+ } else if (parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0") {
+ desired = false;
+ } else if (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?") {
+ out.print("%s: %s\nUsage:\n%s", name, tagline, usage);
+ return CR_OK;
+ } else if (parameters[0] == "list") {
+ if (!enabled) {
+ out.printerr("Stockflow is not currently enabled.\n");
+ return CR_FAILURE;
+ }
+
+ if (!Maps::IsValid()) {
+ out.printerr("You haven't loaded a map yet.\n");
+ return CR_FAILURE;
+ }
+
+ // Tell Lua to list any saved stockpile orders.
+ return helper.command_method("list_orders", out)? CR_OK: CR_FAILURE;
+ } else if (parameters[0] != "status") {
+ return CR_WRONG_USAGE;
+ }
+ } else if (parameters.size() > 1) {
+ return CR_WRONG_USAGE;
+ }
+
+ if (desired != enabled) {
+ if (!apply_hooks(out, desired)) {
+ return CR_FAILURE;
+ }
+ }
+
+ out.print("Stockflow is %s %s.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled");
+ enabled = desired;
+ return CR_OK;
+}
+
+
+DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
+ if (event == DFHack::SC_MAP_LOADED) {
+ if (!helper.reset(out, enabled)) {
+ out.printerr("Could not load stockflow world data!\n");
+ return CR_FAILURE;
+ }
+ } else if (event == DFHack::SC_MAP_UNLOADED) {
+ if (!helper.reset(out, false)) {
+ out.printerr("Could not unload stockflow world data!\n");
+ return CR_FAILURE;
+ }
+ }
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_enable(color_ostream& out, bool enable) {
+ /* Accept the "enable stockflow"/"disable stockflow" syntax, where available. */
+ /* Same as "stockflow enable"/"stockflow disable" except without the status line. */
+ if (enable != enabled) {
+ if (!apply_hooks(out, enable)) {
+ return CR_FAILURE;
+ }
+
+ enabled = enable;
+ }
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) {
+ helper.init();
+ if (AUTOENABLE) {
+ if (!apply_hooks(out, true)) {
+ return CR_FAILURE;
+ }
+
+ enabled = true;
+ }
+
+ commands.push_back(PluginCommand(name, tagline, stockflow_cmd, false, usage));
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown(color_ostream &out) {
+ return plugin_enable(out, false);
+}
diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp
index 35d1e15b8..81840b67b 100644
--- a/plugins/strangemood.cpp
+++ b/plugins/strangemood.cpp
@@ -126,7 +126,7 @@ int getCreatedMetalBars (int32_t idx)
return 0;
}
-void selectWord (const df::world_raws::T_language::T_word_table &table, int32_t &word, df::enum_field &part, int mode)
+void selectWord (const df::language_word_table &table, int32_t &word, df::enum_field &part, int mode)
{
if (table.parts[mode].size())
{
@@ -142,7 +142,7 @@ void selectWord (const df::world_raws::T_language::T_word_table &table, int32_t
}
}
-void generateName(df::language_name &output, int language, int mode, const df::world_raws::T_language::T_word_table &table1, const df::world_raws::T_language::T_word_table &table2)
+void generateName(df::language_name &output, int language, int mode, const df::language_word_table &table1, const df::language_word_table &table2)
{
for (int i = 0; i < 100; i++)
{
diff --git a/scripts/gui/mod-manager.lua b/scripts/gui/mod-manager.lua
index 6d56d88a6..210109807 100644
--- a/scripts/gui/mod-manager.lua
+++ b/scripts/gui/mod-manager.lua
@@ -1,8 +1,29 @@
+-- a graphical mod manager for df
local gui=require 'gui'
local widgets=require 'gui.widgets'
local entity_file=dfhack.getDFPath().."/raw/objects/entity_default.txt"
local init_file=dfhack.getDFPath().."/raw/init.lua"
+local mod_dir=dfhack.getDFPath().."/hack/mods"
+--[[ mod format: lua script that defines:
+ name - a name that is displayed in list
+ author - mod author, also displayed
+ description - mod description
+ OPTIONAL:
+ raws_list - a list (table) of file names that need to be copied over to df raws
+ patch_entity - a chunk of text to patch entity TODO: add settings to which entities to add
+ patch_init - a chunk of lua to add to lua init
+ patch_dofile - a list (table) of files to add to lua init as "dofile"
+ patch_files - a table of files to patch:
+ filename - a filename (in raws folder) to patch
+ patch - what to add
+ after - a string after which to insert
+ MORE OPTIONAL:
+ guard - a token that is used in raw files to find editions and remove them on uninstall
+ guard_init - a token for lua file
+ [pre|post]_(un)install - callback functions. Can trigger more complicated behavior
+]]
+
function fileExists(filename)
local file=io.open(filename,"rb")
if file==nil then
@@ -12,6 +33,10 @@ function fileExists(filename)
return true
end
end
+if not fileExists(init_file) then
+ local initFile=io.open(initFileName,"a")
+ initFile:close()
+end
function copyFile(from,to) --oh so primitive
local filefrom=io.open(from,"rb")
local fileto=io.open(to,"w+b")
@@ -27,6 +52,16 @@ function patchInit(initFileName,patch_guard,code)
code,patch_guard[2]))
initFile:close()
end
+function patchDofile( initFileName,patch_guard,dofile_list )
+ local initFile=io.open(initFileName,"a")
+ initFile:write(patch_guard[1].."\n")
+ for _,v in ipairs(dofile_list) do
+ local fixed_path=mod_dir:gsub("\\","/")
+ initFile:write(string.format("dofile('%s/%s')\n",fixed_path,v))
+ end
+ initFile:write(patch_guard[2].."\n")
+ initFile:close()
+end
function patchFile(file_name,patch_guard,after_string,code)
local input_lines=patch_guard[1].."\n"..code.."\n"..patch_guard[2]
@@ -109,15 +144,19 @@ manager=defclass(manager,gui.FramedScreen)
function manager:init(args)
self.mods={}
local mods=self.mods
- local mlist=dfhack.internal.getDir("mods")
+ local mlist=dfhack.internal.getDir(mod_dir)
+
+ if #mlist==0 then
+ qerror("Mod directory not found! Are you sure it is in:"..mod_dir)
+ end
for k,v in ipairs(mlist) do
if v~="." and v~=".." then
- local f,modData=pcall(dofile,"mods/".. v .. "/init.lua")
+ local f,modData=pcall(dofile,mod_dir.."/".. v .. "/init.lua")
if f then
mods[modData.name]=modData
modData.guard=modData.guard or {">>"..modData.name.." patch","<