unify and update automelt/autotrade/autodump

develop
Myk Taylor 2023-04-18 08:22:52 -07:00
parent c06f54df8f
commit 5ffbb4d0ef
No known key found for this signature in database
19 changed files with 973 additions and 1702 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

@ -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,71 @@
logistics
=========
.. dfhack-tool::
:summary: Automatically mark and route items in monitored stockpiles.
:tags: fort productivity items stockpiles
:no-command:
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, and/or autodump features twice every in-game day, and will mark valid
items in those stockpiles for melting, trading, and/or dumping, respectively.
Note that items will only be marked for trading if a caravan is approaching or
is already at the trade depot.
Please see `gui/logistics` for the interactive status and configuration dialog.
Usage
-----
::
enable logistics
logistics [status]
logistics now
logistics add [melt] [trade] [dump] [<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.

@ -5,8 +5,8 @@ stockpiles
:summary: Import, export, or modify stockpile settings and features.
: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
@ -94,6 +94,7 @@ file are:
:types: The elements below the categories, which include the sub-categories, the
specific item types, and any toggles the category might have (like Prepared
meals for the Food category).
:features: DFHack `logistics` features.
.. _stockpiles-library:

@ -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,676 @@
#include "Debug.h"
#include "LuaTools.h"
#include "PluginManager.h"
#include "modules/Buildings.h"
#include "modules/Job.h"
#include "modules/Persistence.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/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,
};
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);
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);
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))) {
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;
do_cycle(out, melt_count, trade_count, dump_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);
}
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) { }
virtual bool is_designated(color_ostream &out, df::item *item) override {
return item->flags.bits.melt;
}
virtual 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;
}
virtual 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()) { }
virtual 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;
}
virtual bool can_designate(color_ostream& out, df::item* item) override {
return Items::canTradeWithContents(item);
}
virtual 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) { }
virtual bool is_designated(color_ostream& out, df::item* item) override {
return item->flags.bits.dump;
}
virtual bool can_designate(color_ostream& out, df::item* item) override {
return true;
}
virtual bool designate(color_ostream& out, df::item* item) override {
item->flags.bits.dump = true;
return true;
}
};
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) {
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);
}
}
static void do_cycle(color_ostream &out, int32_t &melt_count, int32_t &trade_count, int32_t &dump_count) {
DEBUG(cycle,out).print("running %s cycle\n", plugin_name);
cycle_timestamp = world->frame_counter;
ProcessorStats melt_stats, trade_stats, dump_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);
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);
scan_stockpile(out, bld, melt_stock_processor,
trade_stock_processor, dump_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;
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);
scan_stockpile(*out, bld, melt_stock_processor,
trade_stock_processor, dump_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);
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);
unordered_map<string, string> config;
config.emplace("melt", melt ? "true" : "false");
config.emplace("trade", trade ? "true" : "false");
config.emplace("dump", dump ? "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;
do_cycle(out, melt_count, trade_count, dump_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);
}
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));
} else {
stockpile_config.emplace("melt", false);
stockpile_config.emplace("trade", false);
stockpile_config.emplace("dump", 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_getStockpileConfig\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) {
DEBUG(status, out).print("entering logistics_setStockpileConfig stockpile_number=%d, melt=%d, trade=%d, dump=%d\n",
stockpile_number, melt, trade, dump);
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);
}
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();
}
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_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,146 @@
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 = name
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),
})
end
table.sort(data, function(a, b) return a.sort_key < b.sort_key end)
return data
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!'
return
end
local name_len = 12
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 %11s %11s %11s';
print(fmt:format('number', 'name', 'melt', 'trade', 'dump'))
local function uline(len) return ('-'):rep(len) end
print(fmt:format(uline(6), uline(name_len), uline(11), uline(11), 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_enab(sp.trade), get_enab(sp.dump)))
print(fmt:format('', '', get_dstat(sp.melt), get_dstat(sp.trade), get_dstat(sp.dump)))
end
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,
features.trade or config.trade,
features.dump or config.dump)
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

@ -79,6 +79,7 @@ local included_elements = {
general=2,
categories=4,
types=8,
features=16,
}
function export_stockpile(name, opts)
@ -87,9 +88,7 @@ function export_stockpile(name, opts)
local includedElements = 0
for _,inc in ipairs(opts.includes) do
if included_elements[inc] then
includedElements = includedElements | included_elements[inc]
end
includedElements = includedElements | included_elements[inc]
end
if includedElements == 0 then
@ -124,12 +123,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
if not included_elements[v] then
qerror(('invalid included element: "%s"'):format(v))
end
end

@ -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);
}
/**

@ -42,8 +42,9 @@ using df::global::world;
using std::placeholders::_1;
namespace DFHack {
DBG_EXTERN(stockpiles, log);
namespace DFHack
{
DBG_EXTERN(stockpiles, log);
}
static struct OtherMatsFurniture {
@ -905,6 +906,28 @@ void StockpileSerializer::read_general(DeserializeMode mode) {
mPile->use_links_only);
}
void StockpileSerializer::write_features() {
DEBUG(log).print("writing feature settings\n");
mBuffer.set_melt(mPile->use_links_only);
mBuffer.set_trade(mPile->settings.allow_inorganic);
mBuffer.set_dump(mPile->settings.allow_organic);
}
void StockpileSerializer::read_features(DeserializeMode mode) {
read_elem<int32_t, bool>("use_links_only", mode,
std::bind(&StockpileSettings::has_use_links_only, mBuffer),
std::bind(&StockpileSettings::use_links_only, mBuffer),
mPile->use_links_only);
read_elem<bool, bool>("allow_inorganic", mode,
std::bind(&StockpileSettings::has_allow_inorganic, mBuffer),
std::bind(&StockpileSettings::allow_inorganic, mBuffer),
mPile->settings.allow_inorganic);
read_elem<bool, bool>("allow_organic", mode,
std::bind(&StockpileSettings::has_allow_organic, mBuffer),
std::bind(&StockpileSettings::allow_organic, mBuffer),
mPile->settings.allow_organic);
}
static bool ammo_mat_is_allowed(const MaterialInfo& mi) {
return mi.isValid() && mi.material && mi.material->flags.is_set(material_flags::IS_METAL);
}

@ -168,4 +168,6 @@ private:
void write_containers();
void read_containers(DeserializeMode mode);
void write_features();
void read_features(DeserializeMode mode);
};

@ -183,6 +183,11 @@ message StockpileSettings {
optional WeaponsSet weapons = 16;
optional WoodSet wood = 15;
// DFHack features
optional bool melt = 27;
optional bool trade = 28;
optional bool dump = 29;
// deprecated
optional bool corpses = 24; // not marked as deprecated since we still read it
optional OreSet ore = 7 [deprecated=true];

@ -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,14 +20,15 @@ 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,