Merge pull request #3285 from myk002/myk_stockpiles

[logistics] unified plugin for automelt/autotrade/autodump; stockpiles integration
develop
Myk 2023-06-12 13:17:03 -07:00 committed by GitHub
commit 1673c797e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1844 additions and 2181 deletions

@ -78,14 +78,11 @@
# Display DFHack version on title screen
#enable title-version
# Allow DFHack tools to overlay functionality and information on the DF screen
enable overlay
# Allow buildings to be placed now and built later when materials are available
# Enable system services
enable buildingplan
#Allow designated stockpiles to automatically mark items for melting
enable automelt
enable confirm
enable logistics
enable overlay
# Dwarf Manipulator (simple in-game Dwarf Therapist replacement)
#enable manipulator
@ -97,28 +94,18 @@ enable automelt
#enable automaterial
# Other interface improvement tools
enable \
confirm
# dwarfmonitor \
# mousequery \
# autogems \
# autodump \
# automelt \
# autotrade \
# buildingplan \
# trackstop \
# zone \
# stocks \
# autochop \
# stockpiles
#
#end a line with a backslash to make it continue to the next line. The \ is deleted for the final command.
# Multiline commands are ONLY supported for scripts like dfhack.init. You cannot do multiline command manually on the DFHack console.
# You cannot extend a commented line.
# You can comment out the extension of a line.
# enable mouse controls and sand indicator in embark screen
#embark-tools enable sticky sand mouse
# enable option to enter embark assistant
#enable embark-assistant

@ -26,6 +26,20 @@ Moved frequently used materials to the top of the materials list when building
buildings. Also offered extended options when building constructions. All
functionality has been merged into `buildingplan`.
.. _automelt:
automelt
========
Automatically mark items for melting when they are brought to a monitored
stockpile. Merged into `logistics`.
.. _autotrade:
autotrade
=========
Automatically mark items for trading when they are brought to a monitored
stockpile. Merged into `logistics`.
.. _autounsuspend:
autounsuspend
@ -56,7 +70,7 @@ Replaced by `gui/launcher --minimal <gui/launcher>`.
create-items
============
Replaced by `gui/create-item --multi <gui/create-item>`.
Replaced by `gui/create-item`.
.. _deteriorateclothes:
@ -172,6 +186,13 @@ Tool that warned the user when the ``dfhack.init`` file did not exist. Now that
``dfhack.init`` is autogenerated in ``dfhack-config/init``, this warning is no
longer necessary.
.. _gui/stockpiles:
gui/stockpiles
==============
Provided import/export dialogs. Converted to an `overlay` that displays when
a stockpile is selected.
.. _masspit:
masspit

@ -34,6 +34,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
# Future
## New Plugins
- `logistics`: automatically mark and route items or animals that come to monitored stockpiles. options are toggleable on an overlay that comes up when you have a stockpile selected.
## Fixes
- `dig-now`: clear item occupancy flags for channeled tiles that had items on them

@ -1,55 +0,0 @@
automelt
========
.. dfhack-tool::
:summary: Quickly designate items to be melted.
:tags: fort productivity items stockpiles
:no-command:
Automelt checks monitor-enabled stockpiles once every in-game day, and will mark valid items for melting.
Please see `gui/automelt` for the interactive configuration dialog.
Usage
-----
::
enable automelt
automelt [status]
automelt designate
automelt (monitor|nomonitor) <stockpile>[,<stockpile>...]
Examples
--------
Automatically monitor stockpile ("melt"), marking new valid items for melting. This also immediately marks all present items for melting::
enable automelt
automelt monitor melt
Enable monitoring for ("Stockpile #52"), which has not been given a custom name::
automelt monitor "Stockpile #52"
Enable monitoring for ("Stockpile #52"), which has not been given a custom name::
automelt monitor 52
Enable monitoring for multiple stockpiles ("Stockpile #52", "Stockpile #35", and "melt")::
automelt monitor 52,"Stockpile #35",melt
Commands
--------
``status``
Shows current configuration and relevant statistics
``designate``
Designates items in monitored stockpiles for melting right now. This works even if ``automelt`` is not currently enabled.
``(no)monitor <stockpile>``
Enable/disable monitoring of a given stockpile. Works with either the stockpile's name (if set) or ID.
If the stockpile has no custom name set, you may designate it by either the full name as reported by
the status command, or by just the number.

@ -1,18 +0,0 @@
autotrade
=========
.. dfhack-tool::
:summary: Quickly designate items to be traded.
:tags: unavailable fort productivity items stockpiles
:no-command:
When `enabled <enable>`, this plugin adds an option to the :kbd:`q` menu for
stockpiles. When the ``autotrade`` option is selected for the stockpile, any
items placed in the stockpile will automatically be designated to be traded.
Usage
-----
::
enable autotrade

@ -0,0 +1,68 @@
logistics
=========
.. dfhack-tool::
:summary: Automatically mark and route items in monitored stockpiles.
:tags: fort auto animals items stockpiles
Commands act upon the stockpile selected in the UI unless another stockpile
identifier is specified on the commandline.
When the plugin is enabled, it checks stockpiles marked with automelt,
autotrade, autodump, and/or autotrain features twice every in-game day, and
will mark valid items/animals in those stockpiles for melting, trading,
dumping, and/or training, respectively. Note that items will only be marked for
trading if a caravan is approaching or is already at the trade depot.
Usage
-----
::
enable logistics
logistics [status]
logistics now
logistics add [melt] [trade] [dump] [train] [<options>]
logistics clear [all] [<options>]
Examples
--------
``logistics``
Print a summary of all your stockpiles, their ``logistics`` configuration,
and the number of items that are designated (or can be designated) by each
of the ``logistics`` processors.
``logistics now``
Designate items in monitored stockpiles according to the current
configuration. This works regardless of whether ``logistics`` is currently
enabled.
``logistics add melt``
Register the currently selected stockpile for automelting. Meltable items
that are brought to this stockpile will be designated for melting.
``logistics add melt trade -s goblinite``
Register the stockpile(s) named "goblinite" for automatic melting and
automatic trading. Items will be marked for melting, but any items still in
the stockpile when a caravan shows up will be brought to the trade depot
for trading.
``logistics clear``
Unregisters the currently selected stockpile from any monitoring. Any
currently designated items will remain designated.
``logistics clear -s 12,15,goblinite``
Unregisters the stockpiles with stockpile numbers 12 and 15, along with any
stockpiles named "goblinite", from any monitoring.
``logistics clear all``
Unregister all stockpiles from any monitoring.
Options
-------
``-s``, ``--stockpile <name or number>[,<name or number>...]``
Causes the command to act upon stockpiles with the given names or numbers
instead of the stockpile that is currently selected in the UI. Note that
the numbers are the stockpile numbers, not the building ids.

@ -2,11 +2,11 @@ stockpiles
==========
.. dfhack-tool::
:summary: Import, export, or modify stockpile settings and features.
:summary: Import, export, or modify stockpile settings.
:tags: fort design productivity stockpiles
If you are importing or exporting setting and don't want to specify a building
ID, select a stockpile in the UI before running the command.
Commands act upon the stockpile selected in the UI unless another stockpile
identifier is specified on the commandline.
Usage
-----
@ -15,8 +15,8 @@ Usage
stockpiles [status]
stockpiles list [<search>]
stockpiles export <name> [<options>]
stockpiles import <name> [<options>]
stockpiles export <name> [<options>]
Exported stockpile settings are saved in the ``dfhack-config/stockpiles``
folder, where you can view and delete them, if desired. Names can only
@ -61,9 +61,9 @@ Examples
Options
-------
``-s``, ``--stockpile <id>``
Specify a specific stockpile ID instead of using the one currently selected
in the UI.
``-s``, ``--stockpile <name or id>``
Specify a specific stockpile by name or internal ID instead of using the
stockpile currently selected in the UI.
``-i``, ``--include <comma separated list of elements to include>``
When exporting, you can include this option to select only specific elements
of the stockpile to record. If not specified, everything is included. When

@ -83,9 +83,8 @@ dfhack_plugin(autodump autodump.cpp)
dfhack_plugin(autofarm autofarm.cpp)
#dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static)
add_subdirectory(autolabor)
dfhack_plugin(automelt automelt.cpp LINK_LIBRARIES lua)
dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua)
#dfhack_plugin(autotrade autotrade.cpp)
dfhack_plugin(autoslab autoslab.cpp)
dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua)
#dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua)
#dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua)
@ -129,13 +128,13 @@ dfhack_plugin(hotkeys hotkeys.cpp LINK_LIBRARIES lua)
dfhack_plugin(lair lair.cpp)
dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua)
dfhack_plugin(luasocket luasocket.cpp LINK_LIBRARIES clsocket lua dfhack-tinythread)
dfhack_plugin(logistics logistics.cpp LINK_LIBRARIES lua)
#dfhack_plugin(manipulator manipulator.cpp)
#dfhack_plugin(map-render map-render.cpp LINK_LIBRARIES lua)
dfhack_plugin(misery misery.cpp LINK_LIBRARIES lua)
#dfhack_plugin(mode mode.cpp)
#dfhack_plugin(mousequery mousequery.cpp)
dfhack_plugin(nestboxes nestboxes.cpp)
dfhack_plugin(autoslab autoslab.cpp)
dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static lua)
dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua)
dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua)

@ -41,236 +41,6 @@ DFHACK_PLUGIN("autodump");
REQUIRE_GLOBAL(gps);
REQUIRE_GLOBAL(world);
/* TODO: merge with stockpiles plugin
// Stockpile interface START
static const string PERSISTENCE_KEY = "autodump/stockpiles";
static void mark_all_in_stockpiles(vector<PersistentStockpileInfo> &stockpiles)
{
std::vector<df::item*> &items = world->items.other[items_other_id::IN_PLAY];
// Precompute a bitmask with the bad flags
df::item_flags bad_flags;
bad_flags.whole = 0;
#define F(x) bad_flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(artifact);
F(spider_web); F(owned); F(in_job);
#undef F
size_t marked_count = 0;
for (size_t i = 0; i < items.size(); i++)
{
df::item *item = items[i];
if (item->flags.whole & bad_flags.whole)
continue;
for (auto it = stockpiles.begin(); it != stockpiles.end(); it++)
{
if (!it->inStockpile(item))
continue;
++marked_count;
item->flags.bits.dump = true;
}
}
if (marked_count)
Gui::showAnnouncement("Marked " + int_to_string(marked_count) + " items to dump", COLOR_GREEN, false);
}
class StockpileMonitor
{
public:
bool isMonitored(df::building_stockpilest *sp)
{
for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++)
{
if (it->matches(sp))
return true;
}
return false;
}
void add(df::building_stockpilest *sp)
{
auto pile = PersistentStockpileInfo(sp, PERSISTENCE_KEY);
if (pile.isValid())
{
monitored_stockpiles.push_back(PersistentStockpileInfo(pile));
monitored_stockpiles.back().save();
}
}
void remove(df::building_stockpilest *sp)
{
for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++)
{
if (it->matches(sp))
{
it->remove();
monitored_stockpiles.erase(it);
break;
}
}
}
void doCycle()
{
for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end();)
{
if (!it->isValid())
it = monitored_stockpiles.erase(it);
else
++it;
}
mark_all_in_stockpiles(monitored_stockpiles);
}
void reset()
{
monitored_stockpiles.clear();
std::vector<PersistentDataItem> items;
DFHack::World::GetPersistentData(&items, PERSISTENCE_KEY);
for (auto i = items.begin(); i != items.end(); i++)
{
auto pile = PersistentStockpileInfo(*i, PERSISTENCE_KEY);
if (pile.load())
monitored_stockpiles.push_back(PersistentStockpileInfo(pile));
else
pile.remove();
}
}
private:
vector<PersistentStockpileInfo> monitored_stockpiles;
};
static StockpileMonitor monitor;
#define DELTA_TICKS 620
DFhackCExport command_result plugin_onupdate ( color_ostream &out )
{
if(!Maps::IsValid())
return CR_OK;
if (DFHack::World::ReadPauseState())
return CR_OK;
if (world->frame_counter % DELTA_TICKS != 0)
return CR_OK;
monitor.doCycle();
return CR_OK;
}
struct dump_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
bool handleInput(set<df::interface_key> *input)
{
if (Gui::inRenameBuilding())
return false;
building_stockpilest *sp = get_selected_stockpile();
if (!sp)
return false;
if (input->count(interface_key::CUSTOM_SHIFT_D))
{
if (monitor.isMonitored(sp))
monitor.remove(sp);
else
monitor.add(sp);
}
return false;
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
if (!handleInput(input))
INTERPOSE_NEXT(feed)(input);
}
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
building_stockpilest *sp = get_selected_stockpile();
if (!sp)
return;
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = dims.menu_x1 + 1;
int x = left_margin;
int y = dims.y2 - 7;
int links = 0;
links += sp->links.give_to_pile.size();
links += sp->links.take_from_pile.size();
links += sp->links.give_to_workshop.size();
links += sp->links.take_from_workshop.size();
bool state = monitor.isMonitored(sp);
if (links + 12 >= y) {
y = dims.y2;
OutputString(COLOR_WHITE, x, y, "Auto: ");
OutputString(COLOR_LIGHTRED, x, y, "D");
OutputString(state? COLOR_LIGHTGREEN: COLOR_GREY, x, y, "ump ");
} else {
OutputToggleString(x, y, "Auto dump", "D", state, true, left_margin, COLOR_WHITE, COLOR_LIGHTRED);
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(dump_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(dump_hook, render);
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event)
{
case DFHack::SC_MAP_LOADED:
monitor.reset();
break;
case DFHack::SC_MAP_UNLOADED:
break;
default:
break;
}
return CR_OK;
}
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
{
if (enable != is_enabled)
{
if (!INTERPOSE_HOOK(dump_hook, feed).apply(enable) ||
!INTERPOSE_HOOK(dump_hook, render).apply(enable))
return CR_FAILURE;
is_enabled = enable;
}
return CR_OK;
}
// Stockpile interface END
*/
command_result df_autodump(color_ostream &out, vector <string> & parameters);
command_result df_autodump_destroy_here(color_ostream &out, vector <string> & parameters);
command_result df_autodump_destroy_item(color_ostream &out, vector <string> & parameters);

@ -1,799 +0,0 @@
#include "Debug.h"
#include "LuaTools.h"
#include "PluginManager.h"
#include "modules/Buildings.h"
#include "modules/Items.h"
#include "modules/World.h"
#include "modules/Persistence.h"
#include "modules/Gui.h"
#include "df/world.h"
#include "df/building_stockpilest.h"
#include "df/item_quality.h"
#include <map>
#include <unordered_map>
using std::map;
using std::string;
using std::unordered_map;
using std::vector;
using namespace DFHack;
using namespace df::enums;
DFHACK_PLUGIN("automelt");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(world);
namespace DFHack
{
DBG_DECLARE(automelt, status, DebugCategory::LINFO);
DBG_DECLARE(automelt, cycle, DebugCategory::LINFO);
DBG_DECLARE(automelt, perf, DebugCategory::LINFO);
}
static const string CONFIG_KEY = string(plugin_name) + "/config";
static const string STOCKPILE_CONFIG_KEY_PREFIX = string(plugin_name) + "/stockpile/";
static PersistentDataItem config;
static unordered_map<int32_t, PersistentDataItem> watched_stockpiles;
enum StockpileConfigValues
{
STOCKPILE_CONFIG_ID = 0,
STOCKPILE_CONFIG_MONITORED = 1,
};
static int get_config_val(PersistentDataItem &c, int index)
{
if (!c.isValid())
return -1;
return c.ival(index);
}
static bool get_config_bool(PersistentDataItem &c, int index)
{
return get_config_val(c, index) == 1;
}
static void set_config_val(PersistentDataItem &c, int index, int value)
{
if (c.isValid())
c.ival(index) = value;
}
static void set_config_bool(PersistentDataItem &c, int index, bool value)
{
set_config_val(c, index, value ? 1 : 0);
}
static PersistentDataItem &ensure_stockpile_config(color_ostream &out, int id)
{
DEBUG(cycle,out).print("ensuring stockpile config id=%d\n", id);
if (watched_stockpiles.count(id)){
DEBUG(cycle,out).print("stockpile exists in watched_indices\n");
return watched_stockpiles[id];
}
string keyname = STOCKPILE_CONFIG_KEY_PREFIX + int_to_string(id);
DEBUG(status,out).print("creating new persistent key for stockpile %d\n", id);
watched_stockpiles.emplace(id, World::GetPersistentData(keyname, NULL));
return watched_stockpiles[id];
}
static void remove_stockpile_config(color_ostream &out, int id)
{
if (!watched_stockpiles.count(id))
return;
DEBUG(status, out).print("removing persistent key for stockpile %d\n", id);
World::DeletePersistentData(watched_stockpiles[id]);
watched_stockpiles.erase(id);
}
static bool isStockpile(df::building * bld) {
return bld && bld->getType() == df::building_type::Stockpile;
}
static void validate_stockpile_configs(color_ostream &out)
{
for (auto &c : watched_stockpiles) {
int id = get_config_val(c.second, STOCKPILE_CONFIG_ID);
auto bld = df::building::find(id);
if (!isStockpile(bld))
remove_stockpile_config(out, id);
}
}
static const int32_t CYCLE_TICKS = 1200;
static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
static command_result do_command(color_ostream &out, vector<string> &parameters);
static int32_t do_cycle(color_ostream &out);
DFhackCExport command_result plugin_init(color_ostream &out, vector<PluginCommand> &commands)
{
DEBUG(status, out).print("initializing %s\n", plugin_name);
// provide a configuration interface for the plugin
commands.push_back(PluginCommand(
plugin_name,
"Auto melt items in designated stockpiles.",
do_command));
return CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
{
if (enable != is_enabled)
{
is_enabled = enable;
DEBUG(status, out).print("%s from the API; persisting\n", is_enabled ? "enabled" : "disabled");
}
else
{
DEBUG(status, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled");
}
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream &out)
{
DEBUG(status, out).print("shutting down %s\n", plugin_name);
return CR_OK;
}
DFhackCExport command_result plugin_load_data(color_ostream &out)
{
cycle_timestamp = 0;
config = World::GetPersistentData(CONFIG_KEY);
if (!config.isValid())
{
DEBUG(status, out).print("no config found in this save; initializing\n");
config = World::AddPersistentData(CONFIG_KEY);
}
DEBUG(status, out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false");
vector<PersistentDataItem> loaded_persist_data;
World::GetPersistentData(&loaded_persist_data, STOCKPILE_CONFIG_KEY_PREFIX, true);
watched_stockpiles.clear();
const size_t num_watched_stockpiles = loaded_persist_data.size();
for (size_t idx = 0; idx < num_watched_stockpiles; ++idx)
{
auto &c = loaded_persist_data[idx];
watched_stockpiles.emplace(get_config_val(c, STOCKPILE_CONFIG_ID), c);
}
validate_stockpile_configs(out);
return CR_OK;
}
DFhackCExport command_result plugin_onupdate(color_ostream &out)
{
if (!Core::getInstance().isWorldLoaded())
return CR_OK;
if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS)
{
int32_t marked = do_cycle(out);
if (0 < marked)
out.print("automelt: marked %d item(s) for melting\n", marked);
}
return CR_OK;
}
static bool call_automelt_lua(color_ostream *out, const char *fn_name,
int nargs = 0, int nres = 0,
Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
DEBUG(status).print("calling automelt lua function: '%s'\n", fn_name);
CoreSuspender guard;
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!out)
out = &Core::getInstance().getConsole();
return Lua::CallLuaModuleFunction(*out, L, "plugins.automelt", fn_name,
nargs, nres,
std::forward<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
}
static command_result do_command(color_ostream &out, vector<string> &parameters) {
CoreSuspender suspend;
if (!Core::getInstance().isWorldLoaded()) {
out.printerr("Cannot run %s without a loaded world.\n", plugin_name);
return CR_FAILURE;
}
bool show_help = false;
if (!call_automelt_lua(&out, "parse_commandline", parameters.size(), 1,
[&](lua_State *L) {
for (const string &param : parameters)
Lua::Push(L, param);
},
[&](lua_State *L) {
show_help = !lua_toboolean(L, -1);
})) {
return CR_FAILURE;
}
return show_help ? CR_WRONG_USAGE : CR_OK;
}
static inline bool is_metal_item(df::item *item)
{
if (!item)
return false;
MaterialInfo mat(item);
return (mat.getCraftClass() == craft_material_class::Metal);
}
struct BadFlagsCanMelt {
uint32_t whole;
BadFlagsCanMelt() {
df::item_flags flags;
#define F(x) flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(artifact);
F(spider_web); F(owned); F(in_job);
#undef F
whole = flags.whole;
}
};
struct BadFlagsMarkItem {
uint32_t whole;
BadFlagsMarkItem() {
df::item_flags flags;
#define F(x) flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(artifact);
F(spider_web); F(owned);
#undef F
whole = flags.whole;
}
};
// Copied from Kelly Martin's code
static inline bool can_melt(df::item *item)
{
static const BadFlagsCanMelt bad_flags;
if (!is_metal_item(item))
return false;
if (item->flags.whole & bad_flags.whole)
return false;
df::item_type t = item->getType();
if (t == df::enums::item_type::BAR)
return false;
for (auto &g : item->general_refs)
{
switch (g->getType())
{
case general_ref_type::CONTAINS_ITEM:
case general_ref_type::UNIT_HOLDER:
case general_ref_type::CONTAINS_UNIT:
return false;
case general_ref_type::CONTAINED_IN_ITEM:
{
df::item *c = g->getItem();
for (auto &gg : c->general_refs)
{
if (gg->getType() == general_ref_type::UNIT_HOLDER)
return false;
}
}
break;
default:
break;
}
}
if (item->getQuality() >= item_quality::Masterful)
return false;
return true;
}
static inline bool is_set_to_melt(df::item *item)
{
return item->flags.bits.melt;
}
static int mark_item(color_ostream &out, df::item *item, BadFlagsMarkItem bad_flags, int32_t stockpile_id,
int32_t &premarked_item_count, int32_t &item_count, map<int32_t, bool> &tracked_item_map, bool should_melt)
{
DEBUG(perf,out).print("%s running mark_item: should_melt=%d\n", plugin_name, should_melt);
if (DBG_NAME(perf).isEnabled(DebugCategory::LDEBUG)) {
string name = "";
item->getItemDescription(&name, 0);
DEBUG(perf,out).print("item %s %d\n", name.c_str(), item->id);
}
if (item->flags.whole & bad_flags.whole){
DEBUG(perf,out).print("rejected flag check\n");
return 0;
}
if (item->isAssignedToThisStockpile(stockpile_id))
{
DEBUG(perf,out).print("assignedToStockpile\n");
size_t marked_count = 0;
vector<df::item *> contents;
Items::getContainedItems(item, &contents);
for (auto child = contents.begin(); child != contents.end(); child++)
{
DEBUG(perf,out).print("inside child loop\n");
marked_count += mark_item(out, *child, bad_flags, stockpile_id, premarked_item_count, item_count, tracked_item_map, should_melt);
}
return marked_count;
}
DEBUG(perf,out).print("past recurse loop\n");
if (is_set_to_melt(item)) {
DEBUG(perf,out).print("already set to melt\n");
tracked_item_map.emplace(item->id, true);
premarked_item_count++;
DEBUG(perf,out).print("premarked_item_count=%d\n", premarked_item_count);
item_count++;
return 0;
}
if (!can_melt(item)) {
DEBUG(perf,out).print("cannot melt\n");
return 0;
}
DEBUG(perf,out).print("increment item count\n");
item_count++;
//Only melt if told to, else count
if (should_melt) {
DEBUG(perf,out).print("should_melt hit\n");
insert_into_vector(world->items.other[items_other_id::ANY_MELT_DESIGNATED], &df::item::id, item);
item->flags.bits.melt = 1;
tracked_item_map.emplace(item->id, true);
return 1;
} else {
return 0;
}
}
static int32_t mark_all_in_stockpile(color_ostream &out, PersistentDataItem & stockpile, int32_t &premarked_item_count, int32_t &item_count, map<int32_t, bool> &tracked_item_map, bool should_melt)
{
DEBUG(perf,out).print("%s running mark_all_in_stockpile\nshould_melt=%d\n", plugin_name, should_melt);
static const BadFlagsMarkItem bad_flags;
int32_t marked_count = 0;
if(!stockpile.isValid()) {
return 0;
}
int spid = get_config_val(stockpile, STOCKPILE_CONFIG_ID);
auto found = df::building::find(spid);
if (!isStockpile(found))
return 0;
df::building_stockpilest * pile_cast = virtual_cast<df::building_stockpilest>(found);
if (!pile_cast)
return 0;
Buildings::StockpileIterator stored;
DEBUG(perf,out).print("starting item iter. loop\n");
for (stored.begin(pile_cast); !stored.done(); ++stored) {
DEBUG(perf,out).print("calling mark_item\n");
marked_count += mark_item(out, *stored, bad_flags, spid, premarked_item_count, item_count, tracked_item_map, should_melt);
DEBUG(perf,out).print("end mark_item call\npremarked_item_count=%d\n", premarked_item_count);
}
DEBUG(perf,out).print("end item iter. loop\n");
DEBUG(perf,out).print("exit mark_all_in_stockpile\nmarked_count %d\npremarked_count %d\n", marked_count, premarked_item_count);
return marked_count;
}
static int32_t scan_stockpiles(color_ostream &out, bool should_melt, map<int32_t, int32_t> &item_count_piles, map<int32_t, int32_t> &premarked_item_count_piles,
map<int32_t, int32_t> &marked_item_count_piles, map<int32_t, bool> &tracked_item_map) {
DEBUG(perf,out).print("running scan_stockpiles\n");
int32_t newly_marked = 0;
item_count_piles.clear();
premarked_item_count_piles.clear();
marked_item_count_piles.clear();
//Parse all the watched piles
for (auto &c : watched_stockpiles) {
int id = get_config_val(c.second, STOCKPILE_CONFIG_ID);
//Check monitor status
bool monitored = get_config_bool(c.second, STOCKPILE_CONFIG_MONITORED);
if (!monitored) continue;
DEBUG(perf,out).print("%s past monitor check\nmonitored=%d\n", plugin_name, monitored);
int32_t item_count = 0;
int32_t premarked_count = 0;
int32_t marked = mark_all_in_stockpile(out, c.second, premarked_count, item_count, tracked_item_map, should_melt);
DEBUG(perf,out).print("post mark_all_in_stockpile premarked_count=%d\n", premarked_count);
item_count_piles.emplace(id, item_count);
premarked_item_count_piles.emplace(id, premarked_count);
marked_item_count_piles.emplace(id, marked);
newly_marked += marked;
}
DEBUG(perf,out).print("exit scan_stockpiles\n");
return newly_marked;
}
static int32_t scan_all_melt_designated(color_ostream &out, map<int32_t, bool> &tracked_item_map) {
DEBUG(perf,out).print("running scan_all_melt_designated\n");
int32_t marked_item_count = 0;
//loop over all items marked as melt-designated
for (auto item : world->items.other[items_other_id::ANY_MELT_DESIGNATED]) {
//item has already been marked/counted as inside a stockpile. Don't double-count.
if (tracked_item_map.count(item->id)) {
continue;
}
marked_item_count++;
}
DEBUG(perf,out).print("exiting scan_all_melt_designated\n");
return marked_item_count;
}
static int32_t scan_count_all(color_ostream &out, bool should_melt, int32_t &marked_item_count_total, int32_t &marked_total_count_all_piles, int32_t &marked_item_count_global,
int32_t &total_items_all_piles, map<int32_t, int32_t> &item_count_piles, map<int32_t, int32_t> &premarked_item_count_piles, map<int32_t, int32_t> &marked_item_count_piles) {
//Wraps both scan_stockpiles and scan_all_melt_designated
//Returns count of items in piles freshly marked
int32_t newly_marked_items_piles = 0;
map<int32_t, bool> tracked_item_map_piles;
newly_marked_items_piles = scan_stockpiles(out, should_melt, item_count_piles, premarked_item_count_piles, marked_item_count_piles, tracked_item_map_piles);
marked_item_count_global = scan_all_melt_designated(out, tracked_item_map_piles);
for (auto &i : watched_stockpiles) {
int id = get_config_val(i.second, STOCKPILE_CONFIG_ID);
total_items_all_piles+= item_count_piles[id];
marked_total_count_all_piles += premarked_item_count_piles[id];
}
marked_item_count_total = marked_item_count_global + marked_total_count_all_piles;
return newly_marked_items_piles;
}
static int32_t do_cycle(color_ostream &out) {
DEBUG(cycle,out).print("running %s cycle\n", plugin_name);
cycle_timestamp = world->frame_counter;
validate_stockpile_configs(out);
int32_t marked_item_count_total = 0;
int32_t marked_item_count_global = 0;
int32_t newly_marked_items_piles = 0;
int32_t total_items_all_piles = 0;
int32_t marked_total_count_all_piles = 0;
map<int32_t, int32_t> item_count_piles, premarked_item_count_piles, marked_item_count_piles;
newly_marked_items_piles = scan_count_all(out, true, marked_item_count_total, marked_total_count_all_piles, marked_item_count_global,
total_items_all_piles, item_count_piles, premarked_item_count_piles, marked_item_count_piles);
DEBUG(perf,out).print("exit %s do_cycle\n", plugin_name);
return newly_marked_items_piles;
}
static int getSelectedStockpile(color_ostream &out) {
df::building *bld = Gui::getSelectedBuilding(out, true);
if (!isStockpile(bld)) {
DEBUG(status,out).print("Selected building is not stockpile\n");
return -1;
}
return bld->id;
}
static PersistentDataItem *getSelectedStockpileConfig(color_ostream &out) {
int32_t bldg_id = getSelectedStockpile(out);
if (bldg_id == -1) {
return NULL;
}
validate_stockpile_configs(out);
PersistentDataItem *c = NULL;
if (watched_stockpiles.count(bldg_id)) {
c = &(watched_stockpiles[bldg_id]);
return c;
}
DEBUG(status,out).print("No existing config\n");
return NULL;
}
static void push_stockpile_config(lua_State *L, int id, bool monitored) {
map<string, int32_t> stockpile_config;
stockpile_config.emplace("id", id);
stockpile_config.emplace("monitored", monitored);
Lua::Push(L, stockpile_config);
}
static void push_stockpile_config(lua_State *L, PersistentDataItem &c) {
push_stockpile_config(L, get_config_val(c, STOCKPILE_CONFIG_ID),
get_config_bool(c, STOCKPILE_CONFIG_MONITORED));
}
static void emplace_bulk_stockpile_config(lua_State *L, int id, bool monitored, map<int32_t, map<string, int32_t>> &stockpiles) {
map<string, int32_t> stockpile_config;
stockpile_config.emplace("id", id);
stockpile_config.emplace("monitored", monitored);
stockpiles.emplace(id, stockpile_config);
}
static void emplace_bulk_stockpile_config(lua_State *L, PersistentDataItem &c, map<int32_t, map<string, int32_t>> &stockpiles) {
int32_t id = get_config_val(c, STOCKPILE_CONFIG_ID);
bool monitored = get_config_bool(c, STOCKPILE_CONFIG_MONITORED);
emplace_bulk_stockpile_config(L, id, monitored, stockpiles);
}
static void automelt_designate(color_ostream &out) {
DEBUG(status, out).print("entering automelt designate\n");
out.print("designated %d item(s) for melting\n", do_cycle(out));
}
static void automelt_printStatus(color_ostream &out) {
DEBUG(status,out).print("entering automelt_printStatus\n");
validate_stockpile_configs(out);
out.print("automelt is %s\n\n", is_enabled ? "enabled" : "disabled");
int32_t marked_item_count_total = 0;
int32_t marked_item_count_global = 0;
int32_t total_items_all_piles = 0;
int32_t marked_total_count_all_piles = 0;
map<int32_t, int32_t> item_count_piles, premarked_item_count_piles, marked_item_count_piles;
scan_count_all(out, false, marked_item_count_total, marked_total_count_all_piles, marked_item_count_global,
total_items_all_piles, item_count_piles, premarked_item_count_piles, marked_item_count_piles);
out.print("summary:\n");
out.print(" total items in monitored piles: %d\n", total_items_all_piles);
out.print(" marked items in monitored piles: %d\n", marked_total_count_all_piles);
out.print("marked items global (excludes those in monitored piles): %d\n", marked_item_count_global);
out.print(" marked items global (monitored piles + others): %d\n", marked_item_count_total);
int name_width = 11;
for (auto &stockpile : world->buildings.other.STOCKPILE) {
if (!isStockpile(stockpile)) continue;
if (stockpile->name.empty()) {
string stock_name = "Stockpile #" + int_to_string(stockpile->stockpile_number);
name_width = std::max(name_width, (int)(stock_name.size()));
} else {
name_width = std::max(name_width, (int)stockpile->name.size());
}
}
name_width = -name_width;
const char *fmt = "%*s %9s %5s %6s\n";
out.print(fmt, name_width, "name", "monitored", "items", "marked");
out.print(fmt, name_width, "----", "---------", "-----", "------");
for (auto &stockpile : world->buildings.other.STOCKPILE) {
if (!isStockpile(stockpile)) continue;
bool monitored = false;
int32_t item_count = 0;
int32_t marked_item_count = 0;
if (watched_stockpiles.count(stockpile->id)) {
auto &c = watched_stockpiles[stockpile->id];
monitored = get_config_bool(c, STOCKPILE_CONFIG_MONITORED);
int id = get_config_val(c, STOCKPILE_CONFIG_ID);
item_count = item_count_piles[id];
marked_item_count = premarked_item_count_piles[id];
}
if (stockpile->name.empty()) {
string stock_name = "Stockpile #" + int_to_string(stockpile->stockpile_number);
out.print(fmt, name_width, stock_name.c_str(), monitored ? "[x]": "[ ]",
int_to_string(item_count).c_str(), int_to_string(marked_item_count).c_str());
} else {
out.print(fmt, name_width, stockpile->name.c_str(), monitored ? "[x]": "[ ]",
int_to_string(item_count).c_str(), int_to_string(marked_item_count).c_str());
}
}
DEBUG(status,out).print("exiting automelt_printStatus\n");
}
static void automelt_setStockpileConfig(color_ostream &out, int id, bool monitored) {
DEBUG(status,out).print("entering automelt_setStockpileConfig for id=%d and monitored=%d\n", id, monitored);
validate_stockpile_configs(out);
auto bldg = df::building::find(id);
bool isInvalidStockpile = !isStockpile(bldg);
bool hasNoData = !monitored;
if (isInvalidStockpile || hasNoData) {
DEBUG(cycle,out).print("calling remove_stockpile_config with id=%d monitored=%d\n", id, monitored);
remove_stockpile_config(out, id);
return;
}
PersistentDataItem &c = ensure_stockpile_config(out, id);
set_config_val(c, STOCKPILE_CONFIG_ID, id);
set_config_bool(c, STOCKPILE_CONFIG_MONITORED, monitored);
//If we're marking something as monitored, go ahead and designate contents.
if (monitored) {
automelt_designate(out);
}
}
static int automelt_getStockpileConfig(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
DEBUG(status, *out).print("entering automelt_getStockpileConfig\n");
validate_stockpile_configs(*out);
int id;
if (lua_isnumber(L, -1)) {
bool found = false;
id = lua_tointeger(L, -1);
for (auto &stockpile : world->buildings.other.STOCKPILE) {
if (!isStockpile(stockpile)) continue;
if (id == stockpile->stockpile_number){
id = stockpile->id;
found = true;
break;
}
}
if (!found)
return 0;
} else {
const char * name = lua_tostring(L, -1);
if (!name)
return 0;
string nameStr = name;
bool found = false;
for (auto &stockpile : world->buildings.other.STOCKPILE) {
if (!isStockpile(stockpile)) continue;
if (nameStr == stockpile->name) {
id = stockpile->id;
found = true;
break;
} else {
string stock_name = "Stockpile #" + int_to_string(stockpile->stockpile_number);
if (stock_name == nameStr) {
id = stockpile->id;
found = true;
break;
}
}
}
if (!found)
return 0;
}
if (watched_stockpiles.count(id)) {
push_stockpile_config(L, watched_stockpiles[id]);
} else {
push_stockpile_config(L, id, false);
}
return 1;
}
static int automelt_getSelectedStockpileConfig(lua_State *L){
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
DEBUG(status, *out).print("entering automelt_getSelectedStockpileConfig\n");
validate_stockpile_configs(*out);
int32_t stock_id = getSelectedStockpile(*out);
PersistentDataItem *c = getSelectedStockpileConfig(*out);
//If we have a valid config entry, use that. Else assume all false.
if (c) {
push_stockpile_config(L, *c);
} else {
push_stockpile_config(L, stock_id, false);
}
return 1;
}
static int automelt_getItemCountsAndStockpileConfigs(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
DEBUG(status,*out).print("entering automelt_getItemCountsAndStockpileConfigs\n");
validate_stockpile_configs(*out);
int32_t marked_item_count_total = 0;
int32_t marked_item_count_global = 0;
int32_t total_items_all_piles = 0;
int32_t marked_total_count_all_piles = 0;
map<int32_t, int32_t> item_count_piles, premarked_item_count_piles, marked_item_count_piles;
scan_count_all(*out, false, marked_item_count_total, marked_total_count_all_piles, marked_item_count_global,
total_items_all_piles, item_count_piles, premarked_item_count_piles, marked_item_count_piles);
map<string, int32_t> summary;
summary.emplace("total_items", total_items_all_piles);
summary.emplace("premarked_items", marked_total_count_all_piles);
summary.emplace("marked_item_count_global", marked_item_count_global);
summary.emplace("marked_item_count_total", marked_item_count_total);
Lua::Push(L, summary);
Lua::Push(L, item_count_piles);
Lua::Push(L, marked_item_count_piles);
Lua::Push(L, premarked_item_count_piles);
map<int32_t, map<string, int32_t>> stockpile_config_map;
for (auto pile : world->buildings.other.STOCKPILE) {
if (!isStockpile(pile))
continue;
int id = pile->id;
if (watched_stockpiles.count(id)) {
emplace_bulk_stockpile_config(L, watched_stockpiles[id], stockpile_config_map);
} else {
emplace_bulk_stockpile_config(L, id, false, stockpile_config_map);
}
}
Lua::Push(L, stockpile_config_map);
DEBUG(perf, *out).print("exit automelt_getItemCountsAndStockpileConfigs\n");
return 5;
}
DFHACK_PLUGIN_LUA_FUNCTIONS{
DFHACK_LUA_FUNCTION(automelt_printStatus),
DFHACK_LUA_FUNCTION(automelt_designate),
DFHACK_LUA_FUNCTION(automelt_setStockpileConfig),
DFHACK_LUA_END};
DFHACK_PLUGIN_LUA_COMMANDS{
DFHACK_LUA_COMMAND(automelt_getStockpileConfig),
DFHACK_LUA_COMMAND(automelt_getItemCountsAndStockpileConfigs),
DFHACK_LUA_COMMAND(automelt_getSelectedStockpileConfig),
DFHACK_LUA_END};

@ -1,467 +0,0 @@
#include "uicommon.h"
#include "modules/Gui.h"
#include "df/world.h"
#include "df/world_raws.h"
#include "df/building_def.h"
#include "df/viewscreen_dwarfmodest.h"
#include "df/viewscreen_tradegoodsst.h"
#include "df/building_stockpilest.h"
#include "modules/Buildings.h"
#include "modules/Items.h"
#include "df/building_tradedepotst.h"
#include "df/general_ref_building_holderst.h"
#include "df/job.h"
#include "df/job_item_ref.h"
#include "modules/Job.h"
#include "df/plotinfost.h"
#include "df/mandate.h"
#include "modules/Maps.h"
using df::building_stockpilest;
DFHACK_PLUGIN("autotrade");
REQUIRE_GLOBAL(gps);
REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(cursor);
REQUIRE_GLOBAL(plotinfo);
static const string PERSISTENCE_KEY = "autotrade/stockpiles";
/*
* Depot Access
*/
class TradeDepotInfo
{
public:
TradeDepotInfo() : depot(0)
{
}
bool findDepot()
{
if (isValid())
return true;
reset();
for(auto bld_it = world->buildings.all.begin(); bld_it != world->buildings.all.end(); bld_it++)
{
auto bld = *bld_it;
if (!isUsableDepot(bld))
continue;
depot = bld;
id = depot->id;
break;
}
return depot;
}
bool assignItem(df::item *item)
{
auto href = df::allocate<df::general_ref_building_holderst>();
if (!href)
return false;
auto job = new df::job();
df::coord tpos(depot->centerx, depot->centery, depot->z);
job->pos = tpos;
job->job_type = job_type::BringItemToDepot;
// job <-> item link
if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled))
{
delete job;
delete href;
return false;
}
// job <-> building link
href->building_id = id;
depot->jobs.push_back(job);
job->general_refs.push_back(href);
// add to job list
Job::linkIntoWorld(job);
return true;
}
void reset()
{
depot = 0;
}
private:
int32_t id;
df::building *depot;
bool isUsableDepot(df::building* bld)
{
if (bld->getType() != building_type::TradeDepot)
return false;
if (bld->getBuildStage() < bld->getMaxBuildStage())
return false;
if (bld->jobs.size() == 1 && bld->jobs[0]->job_type == job_type::DestroyBuilding)
return false;
return true;
}
bool isValid()
{
if (!depot)
return false;
auto found = df::building::find(id);
return found && found == depot && isUsableDepot(found);
}
};
static TradeDepotInfo depot_info;
/*
* Item Manipulation
*/
static bool is_valid_item(df::item *item)
{
// Similar to Items::canTrade() with a few checks changed
for (size_t i = 0; i < item->general_refs.size(); i++)
{
df::general_ref *ref = item->general_refs[i];
switch (ref->getType())
{
case general_ref_type::CONTAINED_IN_ITEM:
return false;
case general_ref_type::UNIT_HOLDER:
return false;
case general_ref_type::BUILDING_HOLDER:
return false;
default:
break;
}
}
for (size_t i = 0; i < item->specific_refs.size(); i++)
{
df::specific_ref *ref = item->specific_refs[i];
if (ref->type == specific_ref_type::JOB)
{
// Ignore any items assigned to a job
return false;
}
}
if (!Items::checkMandates(item))
return false;
return true;
}
static void mark_all_in_stockpiles(vector<PersistentStockpileInfo> &stockpiles)
{
if (!depot_info.findDepot())
return;
// Precompute a bitmask with the bad flags
df::item_flags bad_flags;
bad_flags.whole = 0;
#define F(x) bad_flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(artifact);
F(spider_web); F(owned); F(in_job);
#undef F
size_t marked_count = 0;
size_t error_count = 0;
for (auto it = stockpiles.begin(); it != stockpiles.end(); it++)
{
if (!it->isValid())
continue;
Buildings::StockpileIterator stored;
for (stored.begin(it->getStockpile()); !stored.done(); ++stored)
{
df::item *item = *stored;
if (item->flags.whole & bad_flags.whole)
continue;
if (!is_valid_item(item))
continue;
// In case of container, check contained items for mandates
bool mandates_ok = true;
vector<df::item*> contained_items;
Items::getContainedItems(item, &contained_items);
for (df::item *cit : contained_items)
{
if (!Items::checkMandates(cit))
{
mandates_ok = false;
break;
}
}
if (!mandates_ok)
continue;
if (depot_info.assignItem(item))
{
++marked_count;
}
else
{
if (++error_count < 5)
{
Gui::showZoomAnnouncement(df::announcement_type::CANCEL_JOB, item->pos,
"Cannot trade item from stockpile " + int_to_string(it->getId()), COLOR_RED, true);
}
}
}
}
if (marked_count)
Gui::showAnnouncement("Marked " + int_to_string(marked_count) + " items for trade", COLOR_GREEN, false);
if (error_count >= 5)
{
Gui::showAnnouncement(int_to_string(error_count) + " items were not marked", COLOR_RED, true);
}
}
/*
* Stockpile Monitoring
*/
class StockpileMonitor
{
public:
bool isMonitored(df::building_stockpilest *sp)
{
for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++)
{
if (it->matches(sp))
return true;
}
return false;
}
void add(df::building_stockpilest *sp)
{
auto pile = PersistentStockpileInfo(sp, PERSISTENCE_KEY);
if (pile.isValid())
{
monitored_stockpiles.push_back(pile);
monitored_stockpiles.back().save();
}
}
void remove(df::building_stockpilest *sp)
{
for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++)
{
if (it->matches(sp))
{
it->remove();
monitored_stockpiles.erase(it);
break;
}
}
}
void doCycle()
{
if (!can_trade())
return;
for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end();)
{
if (!it->isValid())
{
it = monitored_stockpiles.erase(it);
continue;
}
++it;
}
mark_all_in_stockpiles(monitored_stockpiles);
}
void reset()
{
monitored_stockpiles.clear();
std::vector<PersistentDataItem> items;
DFHack::World::GetPersistentData(&items, PERSISTENCE_KEY);
for (auto i = items.begin(); i != items.end(); i++)
{
auto pile = PersistentStockpileInfo(*i, PERSISTENCE_KEY);
if (pile.load())
monitored_stockpiles.push_back(pile);
else
pile.remove();
}
}
private:
vector<PersistentStockpileInfo> monitored_stockpiles;
};
static StockpileMonitor monitor;
#define DELTA_TICKS 600
DFhackCExport command_result plugin_onupdate ( color_ostream &out )
{
if(!Maps::IsValid())
return CR_OK;
if (DFHack::World::ReadPauseState())
return CR_OK;
if (world->frame_counter % DELTA_TICKS != 0)
return CR_OK;
monitor.doCycle();
return CR_OK;
}
/*
* Interface
*/
struct trade_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
bool handleInput(set<df::interface_key> *input)
{
if (Gui::inRenameBuilding())
return false;
building_stockpilest *sp = get_selected_stockpile();
if (!sp)
return false;
if (input->count(interface_key::CUSTOM_SHIFT_T))
{
if (monitor.isMonitored(sp))
monitor.remove(sp);
else
monitor.add(sp);
}
return false;
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
if (!handleInput(input))
INTERPOSE_NEXT(feed)(input);
}
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
building_stockpilest *sp = get_selected_stockpile();
if (!sp)
return;
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = dims.menu_x1 + 1;
int x = left_margin;
int y = dims.y2 - 5;
int links = 0;
links += sp->links.give_to_pile.size();
links += sp->links.take_from_pile.size();
links += sp->links.give_to_workshop.size();
links += sp->links.take_from_workshop.size();
bool state = monitor.isMonitored(sp);
if (links + 12 >= y) {
y = dims.y2;
OutputString(COLOR_WHITE, x, y, "Auto: ");
x += 11;
OutputString(COLOR_LIGHTRED, x, y, "T");
OutputString(state? COLOR_LIGHTGREEN: COLOR_GREY, x, y, "rade ");
} else {
OutputToggleString(x, y, "Auto trade", "T", state, true, left_margin, COLOR_WHITE, COLOR_LIGHTRED);
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, render);
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event)
{
case DFHack::SC_MAP_LOADED:
depot_info.reset();
monitor.reset();
break;
case DFHack::SC_MAP_UNLOADED:
break;
default:
break;
}
return CR_OK;
}
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
{
if (enable != is_enabled)
{
depot_info.reset();
monitor.reset();
if (!INTERPOSE_HOOK(trade_hook, feed).apply(enable) ||
!INTERPOSE_HOOK(trade_hook, render).apply(enable))
return CR_FAILURE;
is_enabled = enable;
}
return CR_OK;
}
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}

@ -0,0 +1,778 @@
#include "Debug.h"
#include "LuaTools.h"
#include "PluginManager.h"
#include "modules/Buildings.h"
#include "modules/Job.h"
#include "modules/Persistence.h"
#include "modules/Units.h"
#include "modules/World.h"
#include "df/building.h"
#include "df/building_stockpilest.h"
#include "df/building_tradedepotst.h"
#include "df/caravan_state.h"
#include "df/general_ref_building_holderst.h"
#include "df/plotinfost.h"
#include "df/training_assignment.h"
#include "df/world.h"
using std::string;
using std::unordered_map;
using std::vector;
using namespace DFHack;
DFHACK_PLUGIN("logistics");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(plotinfo);
REQUIRE_GLOBAL(world);
namespace DFHack {
DBG_DECLARE(logistics, status, DebugCategory::LINFO);
DBG_DECLARE(logistics, cycle, DebugCategory::LINFO);
}
static const string CONFIG_KEY_PREFIX = string(plugin_name) + "/";
static unordered_map<int32_t, PersistentDataItem> watched_stockpiles;
enum StockpileConfigValues {
STOCKPILE_CONFIG_STOCKPILE_NUMBER = 0,
STOCKPILE_CONFIG_MELT = 1,
STOCKPILE_CONFIG_TRADE = 2,
STOCKPILE_CONFIG_DUMP = 3,
STOCKPILE_CONFIG_TRAIN = 4,
};
static int get_config_val(PersistentDataItem& c, int index) {
if (!c.isValid())
return -1;
return c.ival(index);
}
static bool get_config_bool(PersistentDataItem& c, int index) {
return get_config_val(c, index) == 1;
}
static void set_config_val(PersistentDataItem& c, int index, int value) {
if (c.isValid())
c.ival(index) = value;
}
static void set_config_bool(PersistentDataItem& c, int index, bool value) {
set_config_val(c, index, value ? 1 : 0);
}
static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stockpile_number) {
DEBUG(cycle, out).print("ensuring stockpile config stockpile_number=%d\n", stockpile_number);
if (watched_stockpiles.count(stockpile_number)) {
DEBUG(cycle, out).print("stockpile exists in watched_stockpiles\n");
return watched_stockpiles[stockpile_number];
}
string keyname = CONFIG_KEY_PREFIX + int_to_string(stockpile_number);
DEBUG(status, out).print("creating new persistent key for stockpile %d\n", stockpile_number);
watched_stockpiles.emplace(stockpile_number, World::GetPersistentData(keyname, NULL));
PersistentDataItem& c = watched_stockpiles[stockpile_number];
set_config_val(c, STOCKPILE_CONFIG_STOCKPILE_NUMBER, stockpile_number);
set_config_bool(c, STOCKPILE_CONFIG_MELT, false);
set_config_bool(c, STOCKPILE_CONFIG_TRADE, false);
set_config_bool(c, STOCKPILE_CONFIG_DUMP, false);
set_config_bool(c, STOCKPILE_CONFIG_TRAIN, false);
return c;
}
static void remove_stockpile_config(color_ostream& out, int stockpile_number) {
if (!watched_stockpiles.count(stockpile_number))
return;
DEBUG(status, out).print("removing persistent key for stockpile %d\n", stockpile_number);
World::DeletePersistentData(watched_stockpiles[stockpile_number]);
watched_stockpiles.erase(stockpile_number);
}
static const int32_t CYCLE_TICKS = 600;
static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
static command_result do_command(color_ostream &out, vector<string> &parameters);
static void do_cycle(color_ostream& out, int32_t& melt_count, int32_t& trade_count, int32_t& dump_count, int32_t& train_count);
DFhackCExport command_result plugin_init(color_ostream &out, vector<PluginCommand> &commands) {
DEBUG(status, out).print("initializing %s\n", plugin_name);
commands.push_back(PluginCommand(
plugin_name,
"Automatically mark and route items in monitored stockpiles.",
do_command));
return CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
is_enabled = enable;
DEBUG(status, out).print("now %s\n", is_enabled ? "enabled" : "disabled");
return CR_OK;
}
static df::building_stockpilest* find_stockpile(int32_t stockpile_number) {
return binsearch_in_vector(world->buildings.other.STOCKPILE,
&df::building_stockpilest::stockpile_number, stockpile_number);
}
static void validate_stockpile_configs(color_ostream& out,
unordered_map<df::building_stockpilest *, PersistentDataItem> &cache) {
for (auto& entry : watched_stockpiles) {
int stockpile_number = entry.first;
PersistentDataItem &c = entry.second;
auto bld = find_stockpile(stockpile_number);
if (!bld || (
!get_config_bool(c, STOCKPILE_CONFIG_MELT) &&
!get_config_bool(c, STOCKPILE_CONFIG_TRADE) &&
!get_config_bool(c, STOCKPILE_CONFIG_DUMP) &&
!get_config_bool(c, STOCKPILE_CONFIG_TRAIN))) {
remove_stockpile_config(out, stockpile_number);
continue;
}
cache.emplace(bld, c);
}
}
// remove this function once saves from 50.08 are no longer compatible
static void migrate_old_keys(color_ostream &out) {
vector<PersistentDataItem> old_data;
World::GetPersistentData(&old_data, "automelt/stockpile/", true);
const size_t num_old_keys = old_data.size();
for (size_t idx = 0; idx < num_old_keys; ++idx) {
auto& old_c = old_data[idx];
int32_t bld_id = get_config_val(old_c, 0);
bool melt_was_on = get_config_bool(old_c, 1);
World::DeletePersistentData(old_c);
auto bld = df::building::find(bld_id);
if (!bld || bld->getType() != df::building_type::Stockpile ||
watched_stockpiles.count(static_cast<df::building_stockpilest *>(bld)->stockpile_number))
continue;
auto &c = ensure_stockpile_config(out, static_cast<df::building_stockpilest *>(bld)->stockpile_number);
set_config_bool(c, STOCKPILE_CONFIG_MELT, melt_was_on);
}
}
DFhackCExport command_result plugin_load_data(color_ostream &out) {
cycle_timestamp = 0;
vector<PersistentDataItem> loaded_persist_data;
World::GetPersistentData(&loaded_persist_data, CONFIG_KEY_PREFIX, true);
watched_stockpiles.clear();
const size_t num_watched_stockpiles = loaded_persist_data.size();
for (size_t idx = 0; idx < num_watched_stockpiles; ++idx) {
auto& c = loaded_persist_data[idx];
watched_stockpiles.emplace(get_config_val(c, STOCKPILE_CONFIG_STOCKPILE_NUMBER), c);
}
migrate_old_keys(out);
return CR_OK;
}
DFhackCExport command_result plugin_onupdate(color_ostream &out) {
if (!is_enabled || !Core::getInstance().isWorldLoaded())
return CR_OK;
if (world->frame_counter - cycle_timestamp >= CYCLE_TICKS) {
int32_t melt_count = 0, trade_count = 0, dump_count = 0, train_count = 0;
do_cycle(out, melt_count, trade_count, dump_count, train_count);
if (0 < melt_count)
out.print("logistics: marked %d item(s) for melting\n", melt_count);
if (0 < trade_count)
out.print("logistics: marked %d item(s) for trading\n", trade_count);
if (0 < dump_count)
out.print("logistics: marked %d item(s) for dumping\n", dump_count);
if (0 < train_count)
out.print("logistics: marked %d animal(s) for training\n", dump_count);
}
return CR_OK;
}
static bool call_logistics_lua(color_ostream* out, const char* fn_name,
int nargs = 0, int nres = 0,
Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
DEBUG(status).print("calling logistics lua function: '%s'\n", fn_name);
CoreSuspender guard;
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!out)
out = &Core::getInstance().getConsole();
return Lua::CallLuaModuleFunction(*out, L, "plugins.logistics", fn_name,
nargs, nres,
std::forward<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
}
static command_result do_command(color_ostream &out, vector<string> &parameters) {
CoreSuspender suspend;
bool show_help = false;
if (!call_logistics_lua(&out, "parse_commandline", 1, 1,
[&](lua_State *L) {
Lua::PushVector(L, parameters);
},
[&](lua_State *L) {
show_help = !lua_toboolean(L, -1);
})) {
return CR_FAILURE;
}
return show_help ? CR_WRONG_USAGE : CR_OK;
}
/////////////////////////////////////////////////////
// cycle
//
typedef unordered_map<int32_t, size_t> StatMap;
struct ProcessorStats {
size_t newly_designated = 0;
StatMap designated_counts, can_designate_counts;
};
class StockProcessor {
public:
const string name;
const int32_t stockpile_number;
const bool enabled;
ProcessorStats &stats;
protected:
StockProcessor(const string &name, int32_t stockpile_number, bool enabled, ProcessorStats &stats)
: name(name), stockpile_number(stockpile_number), enabled(enabled), stats(stats) { }
public:
virtual bool is_designated(color_ostream &out, df::item *item) = 0;
virtual bool can_designate(color_ostream &out, df::item *item) = 0;
virtual bool designate(color_ostream &out, df::item *item) = 0;
};
class MeltStockProcessor : public StockProcessor {
public:
MeltStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats &stats)
: StockProcessor("melt", stockpile_number, enabled, stats) { }
bool is_designated(color_ostream &out, df::item *item) override {
return item->flags.bits.melt;
}
bool can_designate(color_ostream &out, df::item *item) override {
MaterialInfo mat(item);
if (mat.getCraftClass() != df::craft_material_class::Metal)
return false;
if (item->getType() == df::item_type::BAR)
return false;
for (auto &g : item->general_refs) {
switch (g->getType()) {
case df::general_ref_type::CONTAINS_ITEM:
case df::general_ref_type::UNIT_HOLDER:
case df::general_ref_type::CONTAINS_UNIT:
return false;
case df::general_ref_type::CONTAINED_IN_ITEM:
{
df::item *c = g->getItem();
for (auto &gg : c->general_refs) {
if (gg->getType() == df::general_ref_type::UNIT_HOLDER)
return false;
}
break;
}
default:
break;
}
}
if (item->getQuality() >= df::item_quality::Masterful)
return false;
return true;
}
bool designate(color_ostream &out, df::item *item) override {
insert_into_vector(world->items.other.ANY_MELT_DESIGNATED, &df::item::id, item);
item->flags.bits.melt = 1;
return true;
}
};
class TradeStockProcessor: public StockProcessor {
public:
TradeStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats& stats)
: StockProcessor("trade", stockpile_number, enabled && get_active_trade_depot(), stats),
depot(get_active_trade_depot()) { }
bool is_designated(color_ostream& out, df::item* item) override {
auto ref = Items::getSpecificRef(item, df::specific_ref_type::JOB);
return ref && ref->data.job &&
ref->data.job->job_type == df::job_type::BringItemToDepot;
}
bool can_designate(color_ostream& out, df::item* item) override {
return Items::canTradeWithContents(item);
}
bool designate(color_ostream& out, df::item* item) override {
if (!depot)
return false;
auto href = df::allocate<df::general_ref_building_holderst>();
if (!href)
return false;
auto job = new df::job();
job->job_type = df::job_type::BringItemToDepot;
job->pos = df::coord(depot->centerx, depot->centery, depot->z);
// job <-> item link
if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) {
delete job;
delete href;
return false;
}
// job <-> building link
href->building_id = depot->id;
depot->jobs.push_back(job);
job->general_refs.push_back(href);
// add to job list
Job::linkIntoWorld(job);
return true;
}
private:
df::building_tradedepotst * const depot;
static df::building_tradedepotst * get_active_trade_depot() {
// at least one caravan must be approaching or ready to trade
if (!plotinfo->caravans.size())
return NULL;
bool found = false;
for (auto caravan : plotinfo->caravans) {
auto trade_state = caravan->trade_state;
auto time_remaining = caravan->time_remaining;
if ((trade_state == df::caravan_state::T_trade_state::Approaching ||
trade_state == df::caravan_state::T_trade_state::AtDepot) && time_remaining != 0) {
found = true;
break;
}
}
if (!found)
return NULL;
// at least one trade depot must be ready to receive goods
for (auto bld : world->buildings.other.TRADE_DEPOT) {
if (bld->getBuildStage() < bld->getMaxBuildStage())
continue;
if (bld->jobs.size() == 1 &&
bld->jobs[0]->job_type == df::job_type::DestroyBuilding)
continue;
return bld;
}
return NULL;
}
};
class DumpStockProcessor: public StockProcessor {
public:
DumpStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats& stats)
: StockProcessor("dump", stockpile_number, enabled, stats) { }
bool is_designated(color_ostream& out, df::item* item) override {
return item->flags.bits.dump;
}
bool can_designate(color_ostream& out, df::item* item) override {
return true;
}
bool designate(color_ostream& out, df::item* item) override {
item->flags.bits.dump = true;
return true;
}
};
class TrainStockProcessor : public StockProcessor {
public:
TrainStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats& stats)
: StockProcessor("train", stockpile_number, enabled, stats) {}
bool is_designated(color_ostream& out, df::item* item) override {
auto unit = get_caged_unit(item);
return unit && has_training_assignment(unit);
}
bool can_designate(color_ostream& out, df::item* item) override {
auto unit = get_caged_unit(item);
return unit && Units::isTamable(unit) && !Units::isTame(unit)
&& !has_training_assignment(unit);
}
bool designate(color_ostream& out, df::item* item) override {
auto unit = get_caged_unit(item);
if (!unit)
return false;
df::training_assignment *assignment = new df::training_assignment();
assignment->animal_id = unit->id;
assignment->trainer_id = -1;
assignment->flags.bits.any_trainer = true;
insert_into_vector(df::global::plotinfo->equipment.training_assignments,
&df::training_assignment::animal_id, assignment);
return true;
}
private:
static df::unit* get_caged_unit(df::item* item) {
if (item->getType() != df::item_type::CAGE)
return NULL;
auto gref = Items::getGeneralRef(item, df::general_ref_type::CONTAINS_UNIT);
if (!gref)
return NULL;
return gref->getUnit();
}
static bool has_training_assignment(df::unit* unit) {
return binsearch_index(df::global::plotinfo->equipment.training_assignments,
&df::training_assignment::animal_id, unit->id) > -1;
}
};
static const struct BadFlags {
const uint32_t whole;
BadFlags() : whole(get_bad_flags()) { }
private:
uint32_t get_bad_flags() {
df::item_flags flags;
#define F(x) flags.bits.x = true;
F(forbid); F(garbage_collect); F(hostile); F(on_fire);
F(rotten); F(trader); F(in_building); F(construction);
F(artifact); F(spider_web); F(owned); F(in_job);
#undef F
return flags.whole;
}
} bad_flags;
static void scan_item(color_ostream &out, df::item *item, StockProcessor &processor) {
DEBUG(cycle,out).print("scan_item [%s] item_id=%d\n", processor.name.c_str(), item->id);
if (DBG_NAME(cycle).isEnabled(DebugCategory::LTRACE)) {
string name = "";
item->getItemDescription(&name, 0);
TRACE(cycle,out).print("item: %s\n", name.c_str());
}
if (item->flags.whole & bad_flags.whole) {
TRACE(cycle,out).print("rejected flag check\n");
return;
}
if (processor.is_designated(out, item)) {
TRACE(cycle,out).print("already designated\n");
++processor.stats.designated_counts[processor.stockpile_number];
return;
}
if (!processor.can_designate(out, item)) {
TRACE(cycle,out).print("cannot designate\n");
return;
}
if (!processor.enabled) {
++processor.stats.can_designate_counts[processor.stockpile_number];
return;
}
processor.designate(out, item);
++processor.stats.newly_designated;
++processor.stats.designated_counts[processor.stockpile_number];
}
static void scan_stockpile(color_ostream &out, df::building_stockpilest *bld,
MeltStockProcessor &melt_stock_processor,
TradeStockProcessor &trade_stock_processor,
DumpStockProcessor &dump_stock_processor,
TrainStockProcessor &train_stock_processor) {
auto id = bld->id;
Buildings::StockpileIterator items;
for (items.begin(bld); !items.done(); ++items) {
df::item *item = *items;
scan_item(out, item, trade_stock_processor);
if (0 == (item->flags.whole & bad_flags.whole) &&
item->isAssignedToThisStockpile(id)) {
TRACE(cycle,out).print("assignedToStockpile\n");
vector<df::item *> contents;
Items::getContainedItems(item, &contents);
for (df::item *contained_item : contents) {
scan_item(out, contained_item, melt_stock_processor);
scan_item(out, contained_item, dump_stock_processor);
}
continue;
}
scan_item(out, item, melt_stock_processor);
scan_item(out, item, dump_stock_processor);
scan_item(out, item, train_stock_processor);
}
}
static void do_cycle(color_ostream& out, int32_t& melt_count, int32_t& trade_count, int32_t& dump_count, int32_t& train_count) {
DEBUG(cycle,out).print("running %s cycle\n", plugin_name);
cycle_timestamp = world->frame_counter;
ProcessorStats melt_stats, trade_stats, dump_stats, train_stats;
unordered_map<df::building_stockpilest *, PersistentDataItem> cache;
validate_stockpile_configs(out, cache);
for (auto &entry : cache) {
df::building_stockpilest *bld = entry.first;
PersistentDataItem &c = entry.second;
int32_t stockpile_number = bld->stockpile_number;
bool melt = get_config_bool(c, STOCKPILE_CONFIG_MELT);
bool trade = get_config_bool(c, STOCKPILE_CONFIG_TRADE);
bool dump = get_config_bool(c, STOCKPILE_CONFIG_DUMP);
bool train = get_config_bool(c, STOCKPILE_CONFIG_TRAIN);
MeltStockProcessor melt_stock_processor(stockpile_number, melt, melt_stats);
TradeStockProcessor trade_stock_processor(stockpile_number, trade, trade_stats);
DumpStockProcessor dump_stock_processor(stockpile_number, dump, dump_stats);
TrainStockProcessor train_stock_processor(stockpile_number, train, train_stats);
scan_stockpile(out, bld, melt_stock_processor,
trade_stock_processor, dump_stock_processor, train_stock_processor);
}
melt_count = melt_stats.newly_designated;
trade_count = trade_stats.newly_designated;
dump_count = dump_stats.newly_designated;
TRACE(cycle,out).print("exit %s do_cycle\n", plugin_name);
}
/////////////////////////////////////////////////////
// Lua API
//
static int logistics_getStockpileData(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
DEBUG(status,*out).print("entering logistics_getStockpileData\n");
unordered_map<df::building_stockpilest *, PersistentDataItem> cache;
validate_stockpile_configs(*out, cache);
ProcessorStats melt_stats, trade_stats, dump_stats, train_stats;
for (auto bld : df::global::world->buildings.other.STOCKPILE) {
int32_t stockpile_number = bld->stockpile_number;
MeltStockProcessor melt_stock_processor(stockpile_number, false, melt_stats);
TradeStockProcessor trade_stock_processor(stockpile_number, false, trade_stats);
DumpStockProcessor dump_stock_processor(stockpile_number, false, dump_stats);
TrainStockProcessor train_stock_processor(stockpile_number, false, train_stats);
scan_stockpile(*out, bld, melt_stock_processor,
trade_stock_processor, dump_stock_processor, train_stock_processor);
}
unordered_map<string, StatMap> stats;
stats.emplace("melt_designated", melt_stats.designated_counts);
stats.emplace("melt_can_designate", melt_stats.can_designate_counts);
stats.emplace("trade_designated", trade_stats.designated_counts);
stats.emplace("trade_can_designate", trade_stats.can_designate_counts);
stats.emplace("dump_designated", dump_stats.designated_counts);
stats.emplace("dump_can_designate", dump_stats.can_designate_counts);
stats.emplace("train_designated", train_stats.designated_counts);
stats.emplace("train_can_designate", train_stats.can_designate_counts);
Lua::Push(L, stats);
unordered_map<int32_t, unordered_map<string, string>> configs;
for (auto &entry : cache) {
df::building_stockpilest *bld = entry.first;
PersistentDataItem &c = entry.second;
bool melt = get_config_bool(c, STOCKPILE_CONFIG_MELT);
bool trade = get_config_bool(c, STOCKPILE_CONFIG_TRADE);
bool dump = get_config_bool(c, STOCKPILE_CONFIG_DUMP);
bool train = get_config_bool(c, STOCKPILE_CONFIG_TRAIN);
unordered_map<string, string> config;
config.emplace("melt", melt ? "true" : "false");
config.emplace("trade", trade ? "true" : "false");
config.emplace("dump", dump ? "true" : "false");
config.emplace("train", train ? "true" : "false");
configs.emplace(bld->stockpile_number, config);
}
Lua::Push(L, configs);
TRACE(cycle, *out).print("exit logistics_getStockpileData\n");
return 2;
}
static void logistics_cycle(color_ostream &out) {
DEBUG(status, out).print("entering logistics_cycle\n");
int32_t melt_count = 0, trade_count = 0, dump_count = 0, train_count = 0;
do_cycle(out, melt_count, trade_count, dump_count, train_count);
out.print("logistics: marked %d item(s) for melting\n", melt_count);
out.print("logistics: marked %d item(s) for trading\n", trade_count);
out.print("logistics: marked %d item(s) for dumping\n", dump_count);
out.print("logistics: marked %d animal(s) for train\n", train_count);
}
static void find_stockpiles(lua_State *L, int idx,
vector<df::building_stockpilest*> &sps) {
if (lua_isnumber(L, idx)) {
sps.emplace_back(find_stockpile(lua_tointeger(L, -1)));
return;
}
const char * pname = lua_tostring(L, -1);
if (!pname || !*pname)
return;
string name(pname);
for (auto sp : world->buildings.other.STOCKPILE) {
if (name == sp->name)
sps.emplace_back(sp);
}
}
static unordered_map<string, int> get_stockpile_config(int32_t stockpile_number) {
unordered_map<string, int> stockpile_config;
stockpile_config.emplace("stockpile_number", stockpile_number);
if (watched_stockpiles.count(stockpile_number)) {
PersistentDataItem &c = watched_stockpiles[stockpile_number];
stockpile_config.emplace("melt", get_config_bool(c, STOCKPILE_CONFIG_MELT));
stockpile_config.emplace("trade", get_config_bool(c, STOCKPILE_CONFIG_TRADE));
stockpile_config.emplace("dump", get_config_bool(c, STOCKPILE_CONFIG_DUMP));
stockpile_config.emplace("train", get_config_bool(c, STOCKPILE_CONFIG_TRAIN));
} else {
stockpile_config.emplace("melt", false);
stockpile_config.emplace("trade", false);
stockpile_config.emplace("dump", false);
stockpile_config.emplace("train", false);
}
return stockpile_config;
}
static int logistics_getStockpileConfigs(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
DEBUG(status, *out).print("entering logistics_getStockpileConfigs\n");
unordered_map<df::building_stockpilest*, PersistentDataItem> cache;
validate_stockpile_configs(*out, cache);
vector<df::building_stockpilest*> sps;
find_stockpiles(L, -1, sps);
if (sps.empty())
return 0;
vector<unordered_map<string, int>> configs;
for (auto sp : sps)
configs.emplace_back(get_stockpile_config(sp->stockpile_number));
Lua::PushVector(L, configs);
return 1;
}
static void logistics_setStockpileConfig(color_ostream& out, int stockpile_number, bool melt, bool trade, bool dump, bool train) {
DEBUG(status, out).print("entering logistics_setStockpileConfig stockpile_number=%d, melt=%d, trade=%d, dump=%d, train=%d\n",
stockpile_number, melt, trade, dump, train);
if (!find_stockpile(stockpile_number)) {
out.printerr("invalid stockpile number: %d\n", stockpile_number);
return;
}
auto &c = ensure_stockpile_config(out, stockpile_number);
set_config_bool(c, STOCKPILE_CONFIG_MELT, melt);
set_config_bool(c, STOCKPILE_CONFIG_TRADE, trade);
set_config_bool(c, STOCKPILE_CONFIG_DUMP, dump);
set_config_bool(c, STOCKPILE_CONFIG_TRAIN, train);
}
static int logistics_clearStockpileConfig(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
DEBUG(status, *out).print("entering logistics_clearStockpileConfig\n");
vector<df::building_stockpilest*> sps;
find_stockpiles(L, -1, sps);
if (sps.empty())
return 0;
for (auto sp : sps)
remove_stockpile_config(*out, sp->stockpile_number);
return 0;
}
static void logistics_clearAllStockpileConfigs(color_ostream &out) {
DEBUG(status, out).print("entering logistics_clearAllStockpileConfigs\n");
for (auto &entry : watched_stockpiles)
World::DeletePersistentData(entry.second);
watched_stockpiles.clear();
}
static int logistics_getGlobalCounts(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
DEBUG(status,*out).print("entering logistics_getGlobalCounts\n");
size_t num_melt = df::global::world->items.other.ANY_MELT_DESIGNATED.size();
size_t num_trade = 0;
for (auto link = world->jobs.list.next; link; link = link->next) {
df::job *job = link->item;
if (job->job_type == df::job_type::BringItemToDepot)
++num_trade;
}
size_t num_dump = 0;
for (auto item : world->items.other.IN_PLAY) {
if (item->flags.bits.dump)
++num_dump;
}
size_t num_train = 0;
// TODO
unordered_map<string, size_t> results;
results.emplace("total_melt", num_melt);
results.emplace("total_trade", num_trade);
results.emplace("total_dump", num_dump);
results.emplace("total_train", num_train);
Lua::Push(L, results);
TRACE(cycle, *out).print("exit logistics_getGlobalCounts\n");
return 1;
}
DFHACK_PLUGIN_LUA_FUNCTIONS{
DFHACK_LUA_FUNCTION(logistics_cycle),
DFHACK_LUA_FUNCTION(logistics_setStockpileConfig),
DFHACK_LUA_FUNCTION(logistics_clearAllStockpileConfigs),
DFHACK_LUA_END};
DFHACK_PLUGIN_LUA_COMMANDS{
DFHACK_LUA_COMMAND(logistics_getStockpileData),
DFHACK_LUA_COMMAND(logistics_getStockpileConfigs),
DFHACK_LUA_COMMAND(logistics_clearStockpileConfig),
DFHACK_LUA_COMMAND(logistics_getGlobalCounts),
DFHACK_LUA_END};

@ -1,87 +0,0 @@
local _ENV = mkmodule('plugins.automelt')
local argparse = require('argparse')
local function process_args(opts, args)
if args[1] == 'help' then
opts.help = true
return
end
return argparse.processArgsGetopt(args, {
{'h', 'help', handler=function() opts.help = true end},
})
end
local function do_set_stockpile_config(var_name, val, stockpiles)
for _,bspec in ipairs(argparse.stringList(stockpiles)) do
local config = automelt_getStockpileConfig(bspec)
if not config then
dfhack.printerr('invalid stockpile: '..tostring(bspec))
else
config[var_name] = val
automelt_setStockpileConfig(config.id, config.monitor, config.melt)
end
end
end
function parse_commandline(...)
local args, opts = {...}, {}
local positionals = process_args(opts, args)
if opts.help then
return false
end
local command = positionals[1]
if not command or command == 'status' then
automelt_printStatus()
elseif command == 'designate' then
automelt_designate()
elseif command == 'monitor' then
do_set_stockpile_config('monitor', true, args[2])
elseif command == 'nomonitor' or command == 'unmonitor' then
do_set_stockpile_config('monitor', false, args[2])
else
return false
end
return true
end
-- used by gui/automelt
function setStockpileConfig(config)
automelt_setStockpileConfig(config.id, config.monitored)
end
function getItemCountsAndStockpileConfigs()
local fmt = 'Stockpile #%-5s'
local data = {automelt_getItemCountsAndStockpileConfigs()}
local ret = {}
ret.summary = table.remove(data, 1)
ret.item_counts = table.remove(data, 1)
ret.marked_item_counts = table.remove(data, 1)
ret.premarked_item_counts = table.remove(data, 1)
local unparsed_stockpile_configs = table.remove(data, 1)
ret.stockpile_configs = {}
for idx,c in pairs(unparsed_stockpile_configs) do
if not c.id or c.id == -1 then
c.name = "ERROR"
c.monitored = false
else
c.name = df.building.find(c.id).name
if not c.name or c.name == '' then
c.name = (fmt):format(tostring(df.building.find(c.id).stockpile_number))
end
c.monitored = c.monitored ~= 0
end
table.insert(ret.stockpile_configs, c)
end
return ret
end
return _ENV

@ -0,0 +1,156 @@
local _ENV = mkmodule('plugins.logistics')
local argparse = require('argparse')
local utils = require('utils')
local function make_stat(name, stockpile_number, stats, configs)
return {
enabled=configs[stockpile_number] and configs[stockpile_number][name] == 'true',
designated=stats[name..'_designated'][stockpile_number] or 0,
can_designate=stats[name..'_can_designate'][stockpile_number] or 0,
}
end
function getStockpileData()
local stats, configs = logistics_getStockpileData()
local data = {}
for _,bld in ipairs(df.global.world.buildings.other.STOCKPILE) do
local stockpile_number, name = bld.stockpile_number, bld.name
local sort_key = tostring(name):lower()
if #name == 0 then
name = ('Stockpile #%d'):format(bld.stockpile_number)
sort_key = ('stockpile #%09d'):format(bld.stockpile_number)
end
table.insert(data, {
stockpile_number=stockpile_number,
name=name,
sort_key=sort_key,
melt=make_stat('melt', stockpile_number, stats, configs),
trade=make_stat('trade', stockpile_number, stats, configs),
dump=make_stat('dump', stockpile_number, stats, configs),
train=make_stat('train', stockpile_number, stats, configs),
})
end
table.sort(data, function(a, b) return a.sort_key < b.sort_key end)
return data
end
local function print_stockpile_data(data)
local name_len = 4
for _,sp in ipairs(data) do
name_len = math.min(40, math.max(name_len, #sp.name))
end
print('Designated/designatable items in stockpiles:')
print()
local fmt = '%6s %-' .. name_len .. 's %4s %10s %5s %11s %4s %10s %5s %11s';
print(fmt:format('number', 'name', 'melt', 'melt items', 'trade', 'trade items', 'dump', 'dump items', 'train', 'train items'))
local function uline(len) return ('-'):rep(len) end
print(fmt:format(uline(6), uline(name_len), uline(4), uline(10), uline(5), uline(11), uline(4), uline(10), uline(5), uline(11)))
local function get_enab(stats) return ('[%s]'):format(stats.enabled and 'x' or ' ') end
local function get_dstat(stats) return ('%d/%d'):format(stats.designated, stats.designated + stats.can_designate) end
for _,sp in ipairs(data) do
print(fmt:format(sp.stockpile_number, sp.name, get_enab(sp.melt), get_dstat(sp.melt), get_enab(sp.trade), get_dstat(sp.trade), get_enab(sp.dump), get_dstat(sp.dump), get_enab(sp.train), get_dstat(sp.train)))
end
end
local function print_status()
print(('logistics is %sactively monitoring stockpiles and marking items')
:format(isEnabled() and '' or 'not '))
if df.global.gamemode ~= df.game_mode.DWARF or not dfhack.isMapLoaded() then
return
end
local data = getStockpileData()
print()
if not data[1] then
print 'No stockpiles defined -- go make some!'
else
print_stockpile_data(data)
end
local global_stats = logistics_getGlobalCounts()
print()
print(('Total items marked for melting: %5d'):format(global_stats.total_melt))
print(('Total items marked for trading: %5d'):format(global_stats.total_trade))
print(('Total items marked for dumping: %5d'):format(global_stats.total_dump))
print(('Total animals marked for training: %5d'):format(global_stats.total_train))
end
local function for_stockpiles(opts, fn)
if not opts.sp then
local selected_sp = dfhack.gui.getSelectedStockpile()
if not selected_sp then qerror('please specify or select a stockpile') end
fn(selected_sp.stockpile_number)
return
end
for _,sp in ipairs(argparse.stringList(opts.sp)) do
fn(sp)
end
end
local function do_add_stockpile_config(features, opts)
for_stockpiles(opts, function(sp)
local configs = logistics_getStockpileConfigs(tonumber(sp) or sp)
if not configs then
dfhack.printerr('invalid stockpile: '..sp)
else
for _,config in ipairs(configs) do
logistics_setStockpileConfig(config.stockpile_number,
features.melt or config.melt == 1,
features.trade or config.trade == 1,
features.dump or config.dump == 1,
features.train or config.train == 1)
end
end
end)
end
local function do_clear_stockpile_config(all, opts)
if all then
logistics_clearAllStockpileConfigs()
return
end
for_stockpiles(opts, function(sp)
logistics_clearStockpileConfig(tonumber(sp) or sp)
end)
end
local function process_args(opts, args)
if args[1] == 'help' then
opts.help = true
return
end
return argparse.processArgsGetopt(args, {
{'h', 'help', handler=function() opts.help = true end},
{'s', 'stockpile', hasArg=true, handler=function(arg) opts.sp = arg end},
})
end
function parse_commandline(args)
local opts = {}
local positionals = process_args(opts, args)
if opts.help or not positionals then
return false
end
local command = table.remove(positionals, 1)
if not command or command == 'status' then
print_status()
elseif command == 'now' then
logistics_cycle()
elseif command == 'add' then
do_add_stockpile_config(utils.invert(positionals), opts)
elseif command == 'clear' then
do_clear_stockpile_config(utils.invert(positionals).all, opts)
else
return false
end
return true
end
return _ENV

@ -1,9 +1,19 @@
local _ENV = mkmodule('plugins.stockpiles')
local argparse = require('argparse')
local gui = require('gui')
local logistics = require('plugins.logistics')
local overlay = require('plugins.overlay')
local widgets = require('gui.widgets')
local STOCKPILES_DIR = "dfhack-config/stockpiles";
local STOCKPILES_LIBRARY_DIR = "hack/data/stockpiles";
local STOCKPILES_DIR = 'dfhack-config/stockpiles'
local STOCKPILES_LIBRARY_DIR = 'hack/data/stockpiles'
local BAD_FILENAME_REGEX = '[^%w._]'
--------------------
-- plugin logic
--------------------
local function get_sp_name(name, num)
if #name > 0 then return name end
@ -19,7 +29,7 @@ local function print_status()
print(STATUS_FMT:format('ID', 'Name'))
print(STATUS_FMT:format('------', '----------'))
end
for _,sp in ipairs(sps) do
for _, sp in ipairs(sps) do
print(STATUS_FMT:format(sp.id, get_sp_name(sp.name, sp.stockpile_number)))
end
end
@ -31,16 +41,14 @@ local function list_dir(path, prefix, filters)
return
end
local normalized_filters = {}
for _,filter in ipairs(filters or {}) do
table.insert(normalized_filters, filter:lower())
end
for _,v in ipairs(paths) do
for _, filter in ipairs(filters or {}) do table.insert(normalized_filters, filter:lower()) end
for _, v in ipairs(paths) do
local normalized_path = prefix .. v.path:lower()
if v.isdir or not normalized_path:endswith('.dfstock') then goto continue end
normalized_path = normalized_path:sub(1, -9)
if #normalized_filters > 0 then
local matched = false
for _,filter in ipairs(normalized_filters) do
for _, filter in ipairs(normalized_filters) do
if normalized_path:find(filter, 1, true) then
matched = true
break
@ -59,43 +67,32 @@ local function list_settings_files(filters)
end
local function assert_safe_name(name)
if not name or #name == 0 then
qerror('name missing or empty')
end
if name:find('[^%w._]') then
if not name or #name == 0 then qerror('name missing or empty') end
if name:find(BAD_FILENAME_REGEX) then
qerror('name can only contain numbers, letters, periods, and underscores')
end
end
local function get_sp_id(opts)
if opts.id then return opts.id end
local sp = dfhack.gui.getSelectedStockpile()
local sp = dfhack.gui.getSelectedStockpile(true)
if sp then return sp.id end
return nil
end
local included_elements = {
containers=1,
general=2,
categories=4,
types=8,
}
local included_elements = {containers=1, general=2, categories=4, types=8}
function export_stockpile(name, opts)
assert_safe_name(name)
name = STOCKPILES_DIR .. '/' .. name
local includedElements = 0
for _,inc in ipairs(opts.includes) do
if included_elements[inc] then
includedElements = includedElements | included_elements[inc]
end
for _, inc in ipairs(opts.includes) do
includedElements = includedElements | included_elements[inc]
end
if includedElements == 0 then
for _,v in pairs(included_elements) do
includedElements = includedElements | v
end
for _, v in pairs(included_elements) do includedElements = includedElements | v end
end
stockpiles_export(name, get_sp_id(opts), includedElements)
@ -124,14 +121,10 @@ function import_route(name, route_id, stop_id, mode, filters)
stockpiles_route_import(name, route_id, stop_id, mode, table.concat(filters or {}, ','))
end
local valid_includes = {general=true, categories=true, types=true}
local function parse_include(arg)
local includes = argparse.stringList(arg, 'include')
for _,v in ipairs(includes) do
if not valid_includes[v] then
qerror(('invalid included element: "%s"'):format(v))
end
for _, v in ipairs(includes) do
if not included_elements[v] then qerror(('invalid included element: "%s"'):format(v)) end
end
return includes
end
@ -139,9 +132,7 @@ end
local valid_modes = {set=true, enable=true, disable=true}
local function parse_mode(arg)
if not valid_modes[arg] then
qerror(('invalid mode: "%s"'):format(arg))
end
if not valid_modes[arg] then qerror(('invalid mode: "%s"'):format(arg)) end
return arg
end
@ -156,25 +147,49 @@ local function process_args(opts, args)
opts.filters = {}
return argparse.processArgsGetopt(args, {
{'f', 'filter', hasArg=true,
handler=function(arg) opts.filters = argparse.stringList(arg) end},
{'h', 'help', handler=function() opts.help = true end},
{'i', 'include', hasArg=true,
handler=function(arg) opts.includes = parse_include(arg) end},
{'m', 'mode', hasArg=true,
handler=function(arg) opts.mode = parse_mode(arg) end},
{'s', 'stockpile', hasArg=true,
handler=function(arg) opts.id = argparse.nonnegativeInt(arg, 'stockpile') end},
})
{
'h',
'help',
handler=function()
opts.help = true
end,
}, {
'm',
'mode',
hasArg=true,
handler=function(arg)
opts.mode = parse_mode(arg)
end,
}, {
'f',
'filter',
hasArg=true,
handler=function(arg)
opts.filters = argparse.stringList(arg)
end,
}, {
'i',
'include',
hasArg=true,
handler=function(arg)
opts.includes = parse_include(arg)
end,
}, {
's',
'stockpile',
hasArg=true,
handler=function(arg)
opts.id = argparse.nonnegativeInt(arg, 'stockpile')
end,
},
})
end
function parse_commandline(args)
local opts = {}
local positionals = process_args(opts, args)
if opts.help or not positionals then
return false
end
if opts.help or not positionals then return false end
local command = table.remove(positionals, 1)
if not command or command == 'status' then
@ -192,4 +207,290 @@ function parse_commandline(args)
return true
end
--------------------
-- dialogs
--------------------
StockpilesExport = defclass(StockpilesExport, widgets.Window)
StockpilesExport.ATTRS{frame_title='Export stockpile settings', frame={w=33, h=15}, resizable=true}
function StockpilesExport:init()
self:addviews{
widgets.EditField{
view_id='edit',
frame={t=0, l=0, r=0},
label_text='name: ',
on_char=function(ch)
return not ch:match(BAD_FILENAME_REGEX)
end,
}, widgets.Label{frame={t=2, l=0}, text='Include which elements?'},
widgets.ToggleHotkeyLabel{frame={t=4, l=0}, label='General settings', initial_option=false},
widgets.ToggleHotkeyLabel{
frame={t=5, l=0},
label='Container settings',
initial_option=false,
}, widgets.ToggleHotkeyLabel{frame={t=6, l=0}, label='Categories', initial_option=true},
widgets.ToggleHotkeyLabel{frame={t=7, l=0}, label='Subtypes', initial_option=true},
widgets.HotkeyLabel{
frame={t=10, l=0},
label='export',
key='SELECT',
enabled=function()
return #self.subviews.edit.text > 0
end,
on_activate=self:callback('on_submit'),
},
}
end
function StockpilesExport:on_submit(text)
self:dismiss()
end
StockpilesExportScreen = defclass(StockpilesExportScreen, gui.ZScreenModal)
StockpilesExportScreen.ATTRS{focus_path='stockpiles/export'}
function StockpilesExportScreen:init()
self:addviews{StockpilesExport{}}
end
function StockpilesExportScreen:onDismiss()
export_view = nil
end
local function do_export()
export_view = export_view and export_view:raise() or StockpilesExportScreen{}:show()
end
--------------------
-- MinimizeButton
--------------------
MinimizeButton = defclass(MinimizeButton, widgets.Widget)
MinimizeButton.ATTRS{
label_unminimized='minimize',
label_minimized='restore',
label_pos='left',
symbol_minimize=string.char(31),
symbol_restore=string.char(30),
get_minimized_fn=DEFAULT_NIL,
on_click=DEFAULT_NIL,
}
function MinimizeButton:init()
self.hovered = false
ensure_key(self, 'frame').h = 1
local is_hovered = function()
return self.hovered
end
local is_not_hovered = function()
return not self.hovered
end
local get_action_symbol = function()
return self.get_minimized_fn() and self.symbol_minimize or self.symbol_restore
end
local get_label = function()
local label = self.get_minimized_fn() and self.label_minimized or self.label_unminimized
return (' %s '):format(label)
end
local hovered_text = {'[', {text=get_action_symbol}, ']'}
table.insert(hovered_text, self.label_pos == 'left' and 1 or #hovered_text + 1,
{text=get_label, hpen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_WHITE}})
self:addviews{
widgets.Label{
view_id='unhovered_label',
frame={t=0, w=3, h=1},
text={'[', {text=get_action_symbol}, ']'},
text_pen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_LIGHTRED},
text_hpen=dfhack.pen.parse{fg=COLOR_WHITE, bg=COLOR_RED},
on_click=function()
self.on_click()
self:updateLayout()
end,
visible=is_not_hovered,
}, widgets.Label{
view_id='hovered_label',
frame={t=0, h=1},
text=hovered_text,
auto_width=true,
text_pen=dfhack.pen.parse{fg=COLOR_BLACK, bg=COLOR_LIGHTRED},
text_hpen=dfhack.pen.parse{fg=COLOR_WHITE, bg=COLOR_RED},
on_click=function()
self.on_click()
self:updateLayout()
end,
visible=is_hovered,
},
}
if self.label_pos == 'left' then
self.subviews.unhovered_label.frame.r = 0
self.subviews.hovered_label.frame.r = 0
else
self.subviews.unhovered_label.frame.l = 0
self.subviews.hovered_label.frame.l = 0
end
end
function MinimizeButton:onRenderFrame()
local prev_hovered = self.hovered
if self.hovered then
self.hovered = self.subviews.hovered_label:getMousePos()
else
self.hovered = self.subviews.unhovered_label:getMousePos()
end
if self.hovered ~= prev_hovered then
self:updateLayout()
df.global.gps.force_full_display_count = 1
end
end
--------------------
-- StockpilesOverlay
--------------------
StockpilesOverlay = defclass(StockpilesOverlay, overlay.OverlayWidget)
StockpilesOverlay.ATTRS{
default_pos={x=24, y=-6},
default_enabled=true,
viewscreens='dwarfmode/Some/Stockpile',
frame={w=65, h=4},
}
function StockpilesOverlay:init()
self.minimized = false
local main_panel = widgets.Panel{
view_id='main',
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
visible=function()
return not self.minimized
end,
subviews={
-- widgets.HotkeyLabel{
-- frame={t=0, l=0},
-- label='import settings',
-- auto_width=true,
-- key='CUSTOM_CTRL_I',
-- on_activate=do_import,
-- }, widgets.HotkeyLabel{
-- frame={t=1, l=0},
-- label='export settings',
-- auto_width=true,
-- key='CUSTOM_CTRL_E',
-- on_activate=do_export,
-- },
widgets.Panel{
frame={t=0, l=0},
subviews={
widgets.Label{
frame={t=0, l=0, h=1},
auto_height=false,
text={'Designate items/animals brought to this stockpile for:'},
text_pen=COLOR_DARKGREY,
}, widgets.ToggleHotkeyLabel{
view_id='melt',
frame={t=1, l=0},
auto_width=true,
key='CUSTOM_CTRL_M',
option_gap=-1,
options={{label='Melting', value=true, pen=COLOR_RED},
{label='Melting', value=false}},
initial_option=false,
on_change=self:callback('toggleLogisticsFeature', 'melt'),
}, widgets.ToggleHotkeyLabel{
view_id='trade',
frame={t=1, l=16},
auto_width=true,
key='CUSTOM_CTRL_T',
option_gap=-1,
options={{label='Trading', value=true, pen=COLOR_YELLOW},
{label='Trading', value=false}},
initial_option=false,
on_change=self:callback('toggleLogisticsFeature', 'trade'),
}, widgets.ToggleHotkeyLabel{
view_id='dump',
frame={t=1, l=32},
auto_width=true,
key='CUSTOM_CTRL_U',
option_gap=-1,
options={{label='Dumping', value=true, pen=COLOR_LIGHTMAGENTA},
{label='Dumping', value=false}},
initial_option=false,
on_change=self:callback('toggleLogisticsFeature', 'dump'),
}, widgets.ToggleHotkeyLabel{
view_id='train',
frame={t=1, l=48},
auto_width=true,
key='CUSTOM_CTRL_A',
option_gap=-1,
options={{label='Training', value=true, pen=COLOR_LIGHTBLUE},
{label='Training', value=false}},
initial_option=false,
on_change=self:callback('toggleLogisticsFeature', 'train'),
},
},
},
},
}
self:addviews{
main_panel, MinimizeButton{
frame={t=0, r=1},
get_minimized_fn=function()
return self.minimized
end,
on_click=self:callback('toggleMinimized'),
},
}
end
function StockpilesOverlay:overlay_onupdate()
-- periodically pick up changes made from other interfaces
self.cur_stockpile = nil
end
function StockpilesOverlay:onRenderFrame()
local sp = dfhack.gui.getSelectedStockpile(true)
if sp and self.cur_stockpile ~= sp then
local config = logistics.logistics_getStockpileConfigs(sp.stockpile_number)[1]
self.subviews.melt:setOption(config.melt == 1)
self.subviews.trade:setOption(config.trade == 1)
self.subviews.dump:setOption(config.dump == 1)
self.subviews.train:setOption(config.train == 1)
self.cur_stockpile = sp
end
end
function StockpilesOverlay:toggleLogisticsFeature(feature)
self.cur_stockpile = nil
local sp = dfhack.gui.getSelectedStockpile(true)
if not sp then return end
local config = logistics.logistics_getStockpileConfigs(sp.stockpile_number)[1]
-- logical xor
logistics.logistics_setStockpileConfig(config.stockpile_number,
(feature == 'melt') ~= (config.melt == 1), (feature == 'trade') ~= (config.trade == 1),
(feature == 'dump') ~= (config.dump == 1), (feature == 'train') ~= (config.train == 1))
end
function StockpilesOverlay:toggleMinimized()
self.minimized = not self.minimized
self.cur_stockpile = nil
end
function StockpilesOverlay:onInput(keys)
if keys.CUSTOM_ALT_M then
self:toggleMinimized()
return true
end
return StockpilesOverlay.super.onInput(self, keys)
end
OVERLAY_WIDGETS = {overlay=StockpilesOverlay}
return _ENV

@ -11,8 +11,9 @@ using namespace DFHack;
using namespace df::enums;
using df::global::world;
namespace DFHack {
DBG_EXTERN(stockpiles, log);
namespace DFHack
{
DBG_EXTERN(stockpiles, log);
}
/**
@ -20,8 +21,8 @@ namespace DFHack {
* and their index in the stockpile_settings structures.
*/
void OrganicMatLookup::food_mat_by_idx(organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type food_idx, FoodMat& food_mat) {
DEBUG(log).print("food_lookup: food_idx(%zd)\n", food_idx);
void OrganicMatLookup::food_mat_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type food_idx, FoodMat& food_mat) {
DEBUG(log, out).print("food_lookup: food_idx(%zd)\n", food_idx);
df::world_raws& raws = world->raws;
df::special_mat_table table = raws.mat_table;
int32_t main_idx = table.organic_indexes[mat_category][food_idx];
@ -31,16 +32,16 @@ void OrganicMatLookup::food_mat_by_idx(organic_mat_category::organic_mat_categor
mat_category == organic_mat_category::Eggs) {
food_mat.creature = raws.creatures.all[type];
food_mat.caste = food_mat.creature->caste[main_idx];
DEBUG(log).print("special creature type(%d) caste(%d)\n", type, main_idx);
DEBUG(log, out).print("special creature type(%d) caste(%d)\n", type, main_idx);
}
else {
food_mat.material.decode(type, main_idx);
DEBUG(log).print("type(%d) index(%d) token(%s)\n", type, main_idx, food_mat.material.getToken().c_str());
DEBUG(log, out).print("type(%d) index(%d) token(%s)\n", type, main_idx, food_mat.material.getToken().c_str());
}
}
std::string OrganicMatLookup::food_token_by_idx(organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type idx) {
std::string OrganicMatLookup::food_token_by_idx(color_ostream& out, organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type idx) {
FoodMat food_mat;
food_mat_by_idx(mat_category, idx, food_mat);
food_mat_by_idx(out, mat_category, idx, food_mat);
if (food_mat.material.isValid()) {
return food_mat.material.getToken();
}
@ -71,32 +72,32 @@ void OrganicMatLookup::food_build_map() {
index_built = true;
}
int16_t OrganicMatLookup::food_idx_by_token(organic_mat_category::organic_mat_category mat_category, const std::string& token) {
int16_t OrganicMatLookup::food_idx_by_token(color_ostream& out, organic_mat_category::organic_mat_category mat_category, const std::string& token) {
df::world_raws& raws = world->raws;
df::special_mat_table table = raws.mat_table;
DEBUG(log).print("food_idx_by_token:\n");
DEBUG(log, out).print("food_idx_by_token:\n");
if (mat_category == organic_mat_category::Fish ||
mat_category == organic_mat_category::UnpreparedFish ||
mat_category == organic_mat_category::Eggs) {
std::vector<std::string> tokens;
split_string(&tokens, token, ":");
if (tokens.size() != 2) {
WARN(log).print("creature invalid CREATURE:CASTE token: %s\n", token.c_str());
WARN(log, out).print("creature invalid CREATURE:CASTE token: %s\n", token.c_str());
return -1;
}
int16_t creature_idx = find_creature(tokens[0]);
if (creature_idx < 0) {
WARN(log).print("creature invalid token %s\n", tokens[0].c_str());
WARN(log, out).print("creature invalid token %s\n", tokens[0].c_str());
return -1;
}
int16_t food_idx = linear_index(table.organic_types[mat_category], creature_idx);
if (tokens[1] == "MALE")
food_idx += 1;
if (table.organic_types[mat_category][food_idx] == creature_idx) {
DEBUG(log).print("creature %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx);
DEBUG(log, out).print("creature %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx);
return food_idx;
}
WARN(log).print("creature caste not found: %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx);
WARN(log, out).print("creature caste not found: %s caste %s creature_idx(%d) food_idx(%d)\n", token.c_str(), tokens[1].c_str(), creature_idx, food_idx);
return -1;
}
@ -107,12 +108,12 @@ int16_t OrganicMatLookup::food_idx_by_token(organic_mat_category::organic_mat_ca
int32_t index = mat_info.index;
auto it = food_index[mat_category].find(std::make_pair(type, index));
if (it != food_index[mat_category].end()) {
DEBUG(log).print("matinfo: %s type(%d) idx(%d) food_idx(%zd)\n", token.c_str(), mat_info.type, mat_info.index, it->second);
DEBUG(log, out).print("matinfo: %s type(%d) idx(%d) food_idx(%zd)\n", token.c_str(), mat_info.type, mat_info.index, it->second);
return it->second;
}
WARN(log).print("matinfo: %s type(%d) idx(%d) food_idx not found :(\n", token.c_str(), mat_info.type, mat_info.index);
WARN(log, out).print("matinfo: %s type(%d) idx(%d) food_idx not found :(\n", token.c_str(), mat_info.type, mat_info.index);
return -1;
}

@ -28,13 +28,13 @@ public:
FoodMat(): material(-1), creature(0), caste(0) { }
};
static void food_mat_by_idx(df::enums::organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type food_idx, FoodMat& food_mat);
static std::string food_token_by_idx(df::enums::organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type idx);
static void food_mat_by_idx(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type food_idx, FoodMat& food_mat);
static std::string food_token_by_idx(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, std::vector<int16_t>::size_type idx);
static size_t food_max_size(df::enums::organic_mat_category::organic_mat_category mat_category);
static void food_build_map();
static int16_t food_idx_by_token(df::enums::organic_mat_category::organic_mat_category mat_category, const std::string& token);
static int16_t food_idx_by_token(DFHack::color_ostream& out, df::enums::organic_mat_category::organic_mat_category mat_category, const std::string& token);
static DFHack::MaterialInfo food_mat_by_token(const std::string& token);

File diff suppressed because it is too large Load Diff

@ -71,74 +71,74 @@ public:
* Since we depend on protobuf-lite, not the full lib, we copy this function from
* protobuf message.cc
*/
bool serialize_to_ostream(std::ostream* output, uint32_t includedElements);
bool serialize_to_ostream(DFHack::color_ostream& out, std::ostream* output, uint32_t includedElements);
/**
* Will serialize stockpile settings to a file (overwrites existing files)
* @return success/failure
*/
bool serialize_to_file(const std::string& file, uint32_t includedElements);
bool serialize_to_file(DFHack::color_ostream& out, const std::string& file, uint32_t includedElements);
/**
* Again, copied from message.cc
*/
bool parse_from_istream(std::istream* input, DeserializeMode mode, const std::vector<std::string>& filters);
bool parse_from_istream(DFHack::color_ostream &out, std::istream* input, DeserializeMode mode, const std::vector<std::string>& filters);
/**
* Read stockpile settings from file
*/
bool unserialize_from_file(const std::string& file, DeserializeMode mode, const std::vector<std::string>& filters);
bool unserialize_from_file(DFHack::color_ostream &out, const std::string& file, DeserializeMode mode, const std::vector<std::string>& filters);
protected:
dfstockpiles::StockpileSettings mBuffer;
// read memory structures and serialize to protobuf
virtual void write(uint32_t includedElements);
virtual void write(DFHack::color_ostream& out, uint32_t includedElements);
// parse serialized data into ui indices
virtual void read(DeserializeMode mode, const std::vector<std::string>& filters);
virtual void read(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
virtual void write_general();
virtual void read_general(DeserializeMode mode);
virtual void write_general(DFHack::color_ostream& out);
virtual void read_general(DFHack::color_ostream& out, DeserializeMode mode);
private:
df::stockpile_settings *mSettings;
bool write_ammo(dfstockpiles::StockpileSettings::AmmoSet* ammo);
void read_ammo(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_animals(dfstockpiles::StockpileSettings::AnimalsSet* animals);
void read_animals(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_armor(dfstockpiles::StockpileSettings::ArmorSet* armor);
void read_armor(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_bars_blocks(dfstockpiles::StockpileSettings::BarsBlocksSet* bars_blocks);
void read_bars_blocks(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_cloth(dfstockpiles::StockpileSettings::ClothSet* cloth);
void read_cloth(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_coins(dfstockpiles::StockpileSettings::CoinSet* coins);
void read_coins(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_finished_goods(dfstockpiles::StockpileSettings::FinishedGoodsSet* finished_goods);
void read_finished_goods(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_ammo(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::AmmoSet* ammo);
void read_ammo(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_animals(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::AnimalsSet* animals);
void read_animals(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_armor(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::ArmorSet* armor);
void read_armor(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_bars_blocks(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::BarsBlocksSet* bars_blocks);
void read_bars_blocks(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_cloth(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::ClothSet* cloth);
void read_cloth(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_coins(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::CoinSet* coins);
void read_coins(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_finished_goods(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::FinishedGoodsSet* finished_goods);
void read_finished_goods(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
food_pair food_map(df::enums::organic_mat_category::organic_mat_category cat);
bool write_food(dfstockpiles::StockpileSettings::FoodSet* food);
void read_food(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_furniture(dfstockpiles::StockpileSettings::FurnitureSet* furniture);
void read_furniture(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_gems(dfstockpiles::StockpileSettings::GemsSet* gems);
void read_gems(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_leather(dfstockpiles::StockpileSettings::LeatherSet* leather);
void read_leather(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_corpses(dfstockpiles::StockpileSettings::CorpsesSet* corpses);
void read_corpses(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_refuse(dfstockpiles::StockpileSettings::RefuseSet* refuse);
void read_refuse(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_sheet(dfstockpiles::StockpileSettings::SheetSet* sheet);
void read_sheet(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_stone(dfstockpiles::StockpileSettings::StoneSet* stone);
void read_stone(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_weapons(dfstockpiles::StockpileSettings::WeaponsSet* weapons);
void read_weapons(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_wood(dfstockpiles::StockpileSettings::WoodSet* wood);
void read_wood(DeserializeMode mode, const std::vector<std::string>& filters);
bool write_food(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::FoodSet* food);
void read_food(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_furniture(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::FurnitureSet* furniture);
void read_furniture(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_gems(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::GemsSet* gems);
void read_gems(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_leather(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::LeatherSet* leather);
void read_leather(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_corpses(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::CorpsesSet* corpses);
void read_corpses(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_refuse(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::RefuseSet* refuse);
void read_refuse(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_sheet(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::SheetSet* sheet);
void read_sheet(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_stone(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::StoneSet* stone);
void read_stone(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_weapons(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::WeaponsSet* weapons);
void read_weapons(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
bool write_wood(DFHack::color_ostream& out, dfstockpiles::StockpileSettings::WoodSet* wood);
void read_wood(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
};
/**
@ -155,17 +155,17 @@ public:
protected:
// read memory structures and serialize to protobuf
virtual void write(uint32_t includedElements);
virtual void write(DFHack::color_ostream& out, uint32_t includedElements);
// parse serialized data into ui indices
virtual void read(DeserializeMode mode, const std::vector<std::string>& filters);
virtual void read(DFHack::color_ostream& out, DeserializeMode mode, const std::vector<std::string>& filters);
virtual void write_general();
virtual void read_general(DeserializeMode mode);
virtual void write_general(DFHack::color_ostream& out);
virtual void read_general(DFHack::color_ostream& out, DeserializeMode mode);
private:
df::building_stockpilest* mPile;
void write_containers();
void read_containers(DeserializeMode mode);
void write_containers(DFHack::color_ostream& out);
void read_containers(DFHack::color_ostream& out, DeserializeMode mode);
};

@ -1,5 +1,6 @@
#pragma once
#include "LuaTools.h"
#include "MiscUtils.h"
#include "df/world.h"
@ -9,6 +10,11 @@
// Utility Functions {{{
// A set of convenience functions for doing common lookups
bool call_stockpiles_lua(DFHack::color_ostream* out, const char* fn_name,
int nargs = 0, int nres = 0,
DFHack::Lua::LuaLambda&& args_lambda = DFHack::Lua::DEFAULT_LUA_LAMBDA,
DFHack::Lua::LuaLambda&& res_lambda = DFHack::Lua::DEFAULT_LUA_LAMBDA);
/**
* Retrieve creature raw from index
*/

@ -11,9 +11,6 @@
#include "df/hauling_route.h"
#include "df/hauling_stop.h"
#include <string>
#include <vector>
using std::string;
using std::vector;
@ -23,27 +20,27 @@ DFHACK_PLUGIN("stockpiles");
REQUIRE_GLOBAL(world);
namespace DFHack {
DBG_DECLARE(stockpiles, log, DebugCategory::LINFO);
namespace DFHack
{
DBG_DECLARE(stockpiles, log, DebugCategory::LINFO);
}
static command_result do_command(color_ostream &out, vector<string> &parameters);
static command_result do_command(color_ostream& out, vector<string>& parameters);
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
DEBUG(log,out).print("initializing %s\n", plugin_name);
DFhackCExport command_result plugin_init(color_ostream &out, vector<PluginCommand> &commands) {
DEBUG(log, out).print("initializing %s\n", plugin_name);
commands.push_back(PluginCommand(
plugin_name,
"Import, export, or modify stockpile settings and features.",
"Import, export, or modify stockpile settings.",
do_command));
return CR_OK;
}
static bool call_stockpiles_lua(color_ostream *out, const char *fn_name,
int nargs = 0, int nres = 0,
Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
bool call_stockpiles_lua(color_ostream* out, const char* fn_name,
int nargs, int nres, Lua::LuaLambda&& args_lambda, Lua::LuaLambda&& res_lambda) {
DEBUG(log).print("calling stockpiles lua function: '%s'\n", fn_name);
CoreSuspender guard;
@ -97,7 +94,7 @@ static bool stockpiles_export(color_ostream& out, string fname, int id, uint32_t
try {
StockpileSerializer cereal(sp);
if (!cereal.serialize_to_file(fname, includedElements)) {
if (!cereal.serialize_to_file(out, fname, includedElements)) {
out.printerr("could not save to '%s'\n", fname.c_str());
return false;
}
@ -136,7 +133,7 @@ static bool stockpiles_import(color_ostream& out, string fname, int id, string m
try {
StockpileSerializer cereal(sp);
if (!cereal.unserialize_from_file(fname, mode, filters)) {
if (!cereal.unserialize_from_file(out, fname, mode, filters)) {
out.printerr("deserialization failed: '%s'\n", fname.c_str());
return false;
}
@ -181,7 +178,7 @@ static bool stockpiles_route_import(color_ostream& out, string fname, int route_
try {
StockpileSettingsSerializer cereal(&stop->settings);
if (!cereal.unserialize_from_file(fname, mode, filters)) {
if (!cereal.unserialize_from_file(out, fname, mode, filters)) {
out.printerr("deserialization failed: '%s'\n", fname.c_str());
return false;
}