Merge remote-tracking branch 'falconne/develop' into develop

develop
Alexander Gavrilov 2014-05-11 13:22:13 +04:00
commit a3d7b54b9c
11 changed files with 3112 additions and 585 deletions

@ -27,6 +27,13 @@ DFHack future
- digfort: improved csv parsing, add start() comment handling
- exterminate: allow specifying a caste (exterminate gob:male)
- createitem: in adventure mode it now defaults to the controlled unit as maker.
- autotrade: adds "(Un)mark All" options to both panes of trade screen.
- mousequery: several usability improvements.
- mousequery: show live overlay (in menu area) of what's on the tile under the mouse cursor.
- search: workshop profile search added.
- dwarfmonitor: add screen to summarise preferences of fortress dwarfs.
- getplants: add autochop function to automate woodcutting.
- stocks: added more filtering and display options.
Siege engine plugin:
- engine quality and distance to target now affect accuracy

@ -1938,6 +1938,29 @@ another savegame you can use the command list_export:
autobutcher.bat
autochop
---------
Automatically manage tree cutting designation to keep available logs withing given
quotas.
Open the dashboard by running:
::
getplants autochop
The plugin must be activated (with 'a') before it can be used. You can then set logging quotas
and restrict designations to specific burrows (with 'Enter') if desired. The plugin's activity
cycle runs once every in game day.
If you add
::
enable getplants
to your dfhack.init there will be a hotkey to open the dashboard from the chop designation
menu.
autolabor
---------
Automatically manage dwarf labors.

@ -735,7 +735,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
MaterialDescriptor material = get_material_in_list(ui_build_selector->sel_index);
if (material.valid)
{
if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT))
if (input->count(interface_key::SELECT) || input->count(interface_key::SELECT_ALL))
{
if (get_last_moved_material().matches(material))
last_used_moved = false; //Keep selected material on top
@ -749,7 +749,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
gen_material.push_back(get_material_in_list(curr_index));
box_select_materials.clear();
// Populate material list with selected material
populate_box_materials(gen_material, ((input->count(interface_key::SEC_SELECT) && ui_build_selector->is_grouped) ? -1 : 1));
populate_box_materials(gen_material, ((input->count(interface_key::SELECT_ALL) && ui_build_selector->is_grouped) ? -1 : 1));
input->clear(); // Let the apply_box_selection routine allocate the construction
input->insert(interface_key::LEAVESCREEN);
@ -1162,6 +1162,15 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
case SELECT_SECOND:
OutputString(COLOR_GREEN, x, y, "Choose second corner", true, left_margin);
int32_t curr_x, curr_y, curr_z;
Gui::getCursorCoords(curr_x, curr_y, curr_z);
int dX = abs(box_first.x - curr_x) + 1;
int dY = abs(box_first.y - curr_y) + 1;
stringstream label;
label << "Selection: " << dX << "x" << dY;
OutputString(COLOR_WHITE, x, ++y, label.str(), true, left_margin);
int cx = box_first.x;
int cy = box_first.y;
OutputString(COLOR_BROWN, cx, cy, "X");

@ -6,6 +6,7 @@
#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/Items.h"
#include "df/building_tradedepotst.h"
@ -14,10 +15,8 @@
#include "df/job_item_ref.h"
#include "modules/Job.h"
#include "df/ui.h"
#include "df/caravan_state.h"
#include "df/mandate.h"
#include "modules/Maps.h"
#include "modules/World.h"
using df::global::world;
using df::global::cursor;
@ -25,127 +24,9 @@ using df::global::ui;
using df::building_stockpilest;
DFHACK_PLUGIN("autotrade");
#define PLUGIN_VERSION 0.2
/*
* Stockpile Access
*/
static building_stockpilest *get_selected_stockpile()
{
if (!Gui::dwarfmode_hotkey(Core::getTopViewscreen()) ||
ui->main.mode != ui_sidebar_mode::QueryBuilding)
{
return nullptr;
}
return virtual_cast<building_stockpilest>(world->selected_building);
}
static bool can_trade()
{
if (df::global::ui->caravans.size() == 0)
return false;
for (auto it = df::global::ui->caravans.begin(); it != df::global::ui->caravans.end(); it++)
{
auto caravan = *it;
auto trade_state = caravan->trade_state;
auto time_remaining = caravan->time_remaining;
if ((trade_state != 1 && trade_state != 2) || time_remaining == 0)
return false;
}
return true;
}
class StockpileInfo {
public:
StockpileInfo(df::building_stockpilest *sp_) : sp(sp_)
{
readBuilding();
}
StockpileInfo(PersistentDataItem &config)
{
this->config = config;
id = config.ival(1);
}
bool inStockpile(df::item *i)
{
df::item *container = Items::getContainer(i);
if (container)
return inStockpile(container);
if (i->pos.z != z) return false;
if (i->pos.x < x1 || i->pos.x >= x2 ||
i->pos.y < y1 || i->pos.y >= y2) return false;
int e = (i->pos.x - x1) + (i->pos.y - y1) * sp->room.width;
return sp->room.extents[e] == 1;
}
bool isValid()
{
auto found = df::building::find(id);
return found && found == sp && found->getType() == building_type::Stockpile;
}
bool load()
{
auto found = df::building::find(id);
if (!found || found->getType() != building_type::Stockpile)
return false;
sp = virtual_cast<df::building_stockpilest>(found);
if (!sp)
return false;
readBuilding();
return true;
}
int32_t getId()
{
return id;
}
bool matches(df::building_stockpilest* sp)
{
return this->sp == sp;
}
void save()
{
config = DFHack::World::AddPersistentData("autotrade/stockpiles");
config.ival(1) = id;
}
void remove()
{
DFHack::World::DeletePersistentData(config);
}
private:
PersistentDataItem config;
df::building_stockpilest* sp;
int x1, x2, y1, y2, z;
int32_t id;
void readBuilding()
{
id = sp->id;
z = sp->z;
x1 = sp->room.x;
x2 = sp->room.x + sp->room.width;
y1 = sp->room.y;
y2 = sp->room.y + sp->room.height;
}
};
#define PLUGIN_VERSION 0.4
static const string PERSISTENCE_KEY = "autotrade/stockpiles";
/*
* Depot Access
@ -316,15 +197,10 @@ static bool is_valid_item(df::item *item)
return true;
}
static void mark_all_in_stockpiles(vector<StockpileInfo> &stockpiles, bool announce)
static void mark_all_in_stockpiles(vector<PersistentStockpileInfo> &stockpiles)
{
if (!depot_info.findDepot())
{
if (announce)
Gui::showAnnouncement("Cannot trade, no valid depot available", COLOR_RED, true);
return;
}
std::vector<df::item*> &items = world->items.other[items_other_id::IN_PLAY];
@ -389,8 +265,6 @@ static void mark_all_in_stockpiles(vector<StockpileInfo> &stockpiles, bool annou
if (marked_count)
Gui::showAnnouncement("Marked " + int_to_string(marked_count) + " items for trade", COLOR_GREEN, false);
else if (announce)
Gui::showAnnouncement("No more items to mark", COLOR_RED, true);
if (error_count >= 5)
{
@ -419,10 +293,10 @@ public:
void add(df::building_stockpilest *sp)
{
auto pile = StockpileInfo(sp);
auto pile = PersistentStockpileInfo(sp, PERSISTENCE_KEY);
if (pile.isValid())
{
monitored_stockpiles.push_back(StockpileInfo(sp));
monitored_stockpiles.push_back(pile);
monitored_stockpiles.back().save();
}
}
@ -456,20 +330,20 @@ public:
++it;
}
mark_all_in_stockpiles(monitored_stockpiles, false);
mark_all_in_stockpiles(monitored_stockpiles);
}
void reset()
{
monitored_stockpiles.clear();
std::vector<PersistentDataItem> items;
DFHack::World::GetPersistentData(&items, "autotrade/stockpiles");
DFHack::World::GetPersistentData(&items, PERSISTENCE_KEY);
for (auto i = items.begin(); i != items.end(); i++)
{
auto pile = StockpileInfo(*i);
auto pile = PersistentStockpileInfo(*i, PERSISTENCE_KEY);
if (pile.load())
monitored_stockpiles.push_back(StockpileInfo(pile));
monitored_stockpiles.push_back(pile);
else
pile.remove();
}
@ -477,7 +351,7 @@ public:
private:
vector<StockpileInfo> monitored_stockpiles;
vector<PersistentStockpileInfo> monitored_stockpiles;
};
static StockpileMonitor monitor;
@ -519,18 +393,7 @@ struct trade_hook : public df::viewscreen_dwarfmodest
if (!sp)
return false;
if (input->count(interface_key::CUSTOM_M))
{
if (!can_trade())
return false;
vector<StockpileInfo> wrapper;
wrapper.push_back(StockpileInfo(sp));
mark_all_in_stockpiles(wrapper, true);
return true;
}
else if (input->count(interface_key::CUSTOM_U))
if (input->count(interface_key::CUSTOM_SHIFT_T))
{
if (monitor.isMonitored(sp))
monitor.remove(sp);
@ -558,18 +421,81 @@ struct trade_hook : public df::viewscreen_dwarfmodest
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = dims.menu_x1 + 1;
int x = left_margin;
int y = 23;
if (can_trade())
OutputHotkeyString(x, y, "Mark all for trade", "m", true, left_margin);
OutputToggleString(x, y, "Auto trade", "u", monitor.isMonitored(sp), true, left_margin);
int y = 24;
OutputToggleString(x, y, "Auto trade", "Shift-T", monitor.isMonitored(sp), true, left_margin);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, render);
struct tradeview_hook : public df::viewscreen_tradegoodsst
{
typedef df::viewscreen_tradegoodsst interpose_base;
bool handleInput(set<df::interface_key> *input)
{
if (input->count(interface_key::CUSTOM_M))
{
for (int i = 0; i < trader_selected.size(); i++)
{
trader_selected[i] = 1;
}
}
else if (input->count(interface_key::CUSTOM_U))
{
for (int i = 0; i < trader_selected.size(); i++)
{
trader_selected[i] = 0;
}
}
else if (input->count(interface_key::CUSTOM_SHIFT_M))
{
for (int i = 0; i < broker_selected.size(); i++)
{
broker_selected[i] = 1;
}
}
else if (input->count(interface_key::CUSTOM_SHIFT_U))
{
for (int i = 0; i < broker_selected.size(); i++)
{
broker_selected[i] = 0;
}
}
else
{
return false;
}
return true;
}
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)();
int x = 2;
int y = 27;
OutputHotkeyString(x, y, "Mark all", "m", true, 2);
OutputHotkeyString(x, y, "Unmark all", "u");
x = 42;
y = 27;
OutputHotkeyString(x, y, "Mark all", "Shift-m", true, 42);
OutputHotkeyString(x, y, "Unmark all", "Shift-u");
}
};
IMPLEMENT_VMETHOD_INTERPOSE(tradeview_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(tradeview_hook, render);
static command_result autotrade_cmd(color_ostream &out, vector <string> & parameters)
{
if (!parameters.empty())
@ -612,7 +538,9 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
monitor.reset();
if (!INTERPOSE_HOOK(trade_hook, feed).apply(enable) ||
!INTERPOSE_HOOK(trade_hook, render).apply(enable))
!INTERPOSE_HOOK(trade_hook, render).apply(enable) ||
!INTERPOSE_HOOK(tradeview_hook, feed).apply(enable) ||
!INTERPOSE_HOOK(tradeview_hook, render).apply(enable))
return CR_FAILURE;
is_enabled = enable;

@ -38,7 +38,7 @@ using df::global::ui_build_selector;
using df::global::world;
DFHACK_PLUGIN("buildingplan");
#define PLUGIN_VERSION 0.9
#define PLUGIN_VERSION 0.12
struct MaterialDescriptor
{
@ -58,16 +58,6 @@ struct MaterialDescriptor
}
};
struct coord32_t
{
int32_t x, y, z;
df::coord get_coord16() const
{
return df::coord(x, y, z);
}
};
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
@ -78,6 +68,7 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
#define SIDEBAR_WIDTH 30
static bool show_debugging = false;
static bool show_help = false;
static void debug(const string &msg)
{
@ -943,7 +934,7 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest
if (isInPlannedBuildingPlacementMode())
{
auto type = ui_build_selector->building_type;
if (input->count(interface_key::CUSTOM_P))
if (input->count(interface_key::CUSTOM_SHIFT_P))
{
planmode_enabled[type] = !planmode_enabled[type];
if (!planmode_enabled[type])
@ -954,6 +945,14 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest
}
return true;
}
else if (input->count(interface_key::CUSTOM_P) ||
input->count(interface_key::CUSTOM_F) ||
input->count(interface_key::CUSTOM_Q) ||
input->count(interface_key::CUSTOM_D) ||
input->count(interface_key::CUSTOM_N))
{
show_help = true;
}
if (is_planmode_enabled(type))
{
@ -983,7 +982,7 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest
return true;
}
else if (input->count(interface_key::CUSTOM_F))
else if (input->count(interface_key::CUSTOM_SHIFT_F))
{
if (!planner.inQuickFortMode())
{
@ -994,15 +993,15 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest
planner.disableQuickfortMode();
}
}
else if (input->count(interface_key::CUSTOM_M))
else if (input->count(interface_key::CUSTOM_SHIFT_M))
{
Screen::show(new ViewscreenChooseMaterial(planner.getDefaultItemFilterForType(type)));
}
else if (input->count(interface_key::CUSTOM_Q))
else if (input->count(interface_key::CUSTOM_SHIFT_Q))
{
planner.cycleDefaultQuality(type);
}
else if (input->count(interface_key::CUSTOM_D))
else if (input->count(interface_key::CUSTOM_SHIFT_D))
{
planner.getDefaultItemFilterForType(type)->decorated_only =
!planner.getDefaultItemFilterForType(type)->decorated_only;
@ -1080,22 +1079,25 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest
{
int y = 23;
OutputToggleString(x, y, "Planning Mode", "p", is_planmode_enabled(type), true, left_margin);
if (show_help)
{
OutputString(COLOR_BROWN, x, y, "Note: ");
OutputString(COLOR_WHITE, x, y, "Use Shift-Keys here", true, left_margin);
}
OutputToggleString(x, y, "Planning Mode", "P", is_planmode_enabled(type), true, left_margin);
if (is_planmode_enabled(type))
{
OutputToggleString(x, y, "Quickfort Mode", "f", planner.inQuickFortMode(), true, left_margin);
OutputToggleString(x, y, "Quickfort Mode", "F", planner.inQuickFortMode(), true, left_margin);
auto filter = planner.getDefaultItemFilterForType(type);
OutputHotkeyString(x, y, "Min Quality: ", "q");
OutputHotkeyString(x, y, "Min Quality: ", "Q");
OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin);
OutputHotkeyString(x, y, "Decorated Only: ", "d");
OutputString(COLOR_BROWN, x, y,
(filter->decorated_only) ? "Yes" : "No", true, left_margin);
OutputToggleString(x, y, "Decorated Only: ", "D", filter->decorated_only, true, left_margin);
OutputHotkeyString(x, y, "Material Filter:", "m", true, left_margin);
OutputHotkeyString(x, y, "Material Filter:", "M", true, left_margin);
auto filter_descriptions = filter->getMaterialFilterAsVector();
for (auto it = filter_descriptions.begin(); it != filter_descriptions.end(); ++it)
OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin);
@ -1131,6 +1133,7 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest
else
{
planner.in_dummmy_screen = false;
show_help = false;
}
}
};

@ -17,6 +17,29 @@
#include "modules/Maps.h"
#include "df/activity_event.h"
#include "df/activity_entry.h"
#include "df/unit_preference.h"
#include "df/unit_soul.h"
#include "df/item_type.h"
#include "df/itemdef_weaponst.h"
#include "df/itemdef_trapcompst.h"
#include "df/itemdef_toyst.h"
#include "df/itemdef_toolst.h"
#include "df/itemdef_instrumentst.h"
#include "df/itemdef_armorst.h"
#include "df/itemdef_ammost.h"
#include "df/itemdef_siegeammost.h"
#include "df/itemdef_glovesst.h"
#include "df/itemdef_shoesst.h"
#include "df/itemdef_shieldst.h"
#include "df/itemdef_helmst.h"
#include "df/itemdef_pantsst.h"
#include "df/itemdef_foodst.h"
#include "df/trapcomp_flags.h"
#include "df/creature_raw.h"
#include "df/world_raws.h"
#include "df/descriptor_shape.h"
#include "df/descriptor_color.h"
using std::deque;
@ -25,7 +48,7 @@ using df::global::ui;
typedef int16_t activity_type;
#define PLUGIN_VERSION 0.5
#define PLUGIN_VERSION 0.8
#define DAY_TICKS 1200
#define DELTA_TICKS 100
@ -48,6 +71,36 @@ static map<df::unit *, deque<activity_type>> work_history;
static int misery[] = { 0, 0, 0, 0, 0, 0, 0 };
static bool misery_upto_date = false;
static color_value monitor_colors[] =
{
COLOR_LIGHTRED,
COLOR_RED,
COLOR_YELLOW,
COLOR_WHITE,
COLOR_CYAN,
COLOR_LIGHTBLUE,
COLOR_LIGHTGREEN
};
static int get_happiness_cat(df::unit *unit)
{
int happy = unit->status.happiness;
if (happy == 0) // miserable
return 0;
else if (happy <= 25) // very unhappy
return 1;
else if (happy <= 50) // unhappy
return 2;
else if (happy <= 75) // fine
return 3;
else if (happy <= 125) // quite content
return 4;
else if (happy <= 150) // happy
return 5;
else // ecstatic
return 6;
}
static int get_max_history()
{
return ticks_per_day * max_history_days;
@ -129,6 +182,7 @@ static string getActivityLabel(const activity_type activity)
return label;
}
class ViewscreenDwarfStats : public dfhack_viewscreen
{
public:
@ -981,6 +1035,520 @@ private:
}
};
struct preference_map
{
df::unit_preference pref;
vector<df::unit *> dwarves;
string label;
string getItemLabel()
{
df::world_raws::T_itemdefs &defs = df::global::world->raws.itemdefs;
label = ENUM_ATTR_STR(item_type, caption, pref.item_type);
switch (pref.item_type)
{
case (df::item_type::WEAPON):
label = defs.weapons[pref.item_subtype]->name_plural;
break;
case (df::item_type::TRAPCOMP):
label = defs.trapcomps[pref.item_subtype]->name_plural;
break;
case (df::item_type::TOY):
label = defs.toys[pref.item_subtype]->name_plural;
break;
case (df::item_type::TOOL):
label = defs.tools[pref.item_subtype]->name_plural;
break;
case (df::item_type::INSTRUMENT):
label = defs.instruments[pref.item_subtype]->name_plural;
break;
case (df::item_type::ARMOR):
label = defs.armor[pref.item_subtype]->name_plural;
break;
case (df::item_type::AMMO):
label = defs.ammo[pref.item_subtype]->name_plural;
break;
case (df::item_type::SIEGEAMMO):
label = defs.siege_ammo[pref.item_subtype]->name_plural;
break;
case (df::item_type::GLOVES):
label = defs.gloves[pref.item_subtype]->name_plural;
break;
case (df::item_type::SHOES):
label = defs.shoes[pref.item_subtype]->name_plural;
break;
case (df::item_type::SHIELD):
label = defs.shields[pref.item_subtype]->name_plural;
break;
case (df::item_type::HELM):
label = defs.helms[pref.item_subtype]->name_plural;
break;
case (df::item_type::PANTS):
label = defs.pants[pref.item_subtype]->name_plural;
break;
case (df::item_type::FOOD):
label = defs.food[pref.item_subtype]->name;
break;
default:
break;
}
return label;
}
void makeLabel()
{
label = "";
typedef df::unit_preference::T_type T_type;
df::world_raws &raws = world->raws;
switch (pref.type)
{
case (T_type::LikeCreature):
{
label = "Creature :";
auto creature = df::creature_raw::find(pref.creature_id);
if (creature)
label += creature->name[1];
break;
}
case (T_type::HateCreature):
{
label = "Hates :";
auto creature = df::creature_raw::find(pref.creature_id);
if (creature)
label += creature->name[1];
break;
}
case (T_type::LikeItem):
label = "Item :" + getItemLabel();
break;
case (T_type::LikeFood):
{
label = "Food :";
if (pref.matindex < 0 || pref.item_type == item_type::MEAT)
{
auto index = (pref.item_type == item_type::FISH) ? pref.mattype : pref.matindex;
if (index > 0)
{
auto creature = df::creature_raw::find(index);
if (creature)
label += creature->name[0];
}
else
{
label += "Invalid";
}
break;
}
}
case (T_type::LikeMaterial):
{
if (label.length() == 0)
label += "Material :";
MaterialInfo matinfo(pref.mattype, pref.matindex);
if (pref.type == T_type::LikeFood && pref.item_type == item_type::PLANT)
{
label += matinfo.material->prefix;
}
else
label += matinfo.toString();
break;
}
case (T_type::LikePlant):
{
df::plant_raw *p = raws.plants.all[pref.plant_id];
label += "Plant :" + p->name_plural;
break;
}
case (T_type::LikeShape):
label += "Shape :" + raws.language.shapes[pref.shape_id]->name_plural;
break;
case (T_type::LikeTree):
{
df::plant_raw *p = raws.plants.all[pref.plant_id];
label += "Tree :" + p->name_plural;
break;
}
case (T_type::LikeColor):
label += "Color :" + raws.language.colors[pref.color_id]->name;
break;
}
}
};
class ViewscreenPreferences : public dfhack_viewscreen
{
public:
ViewscreenPreferences()
{
preferences_column.multiselect = false;
preferences_column.auto_select = true;
preferences_column.setTitle("Preference");
preferences_column.bottom_margin = 3;
preferences_column.search_margin = 35;
dwarf_column.multiselect = false;
dwarf_column.auto_select = true;
dwarf_column.allow_null = true;
dwarf_column.setTitle("Units with Preference");
dwarf_column.bottom_margin = 3;
dwarf_column.search_margin = 35;
populatePreferencesColumn();
}
void populatePreferencesColumn()
{
selected_column = 0;
auto last_selected_index = preferences_column.highlighted_index;
preferences_column.clear();
preference_totals.clear();
for (auto iter = world->units.active.begin(); iter != world->units.active.end(); iter++)
{
df::unit* unit = *iter;
if (!Units::isCitizen(unit))
continue;
if (DFHack::Units::isDead(unit))
continue;
if (!unit->status.current_soul)
continue;
for (auto it = unit->status.current_soul->preferences.begin();
it != unit->status.current_soul->preferences.end();
it++)
{
auto pref = *it;
if (!pref->active)
continue;
bool foundInStore = false;
for (size_t pref_index = 0; pref_index < preferences_store.size(); pref_index++)
{
if (isMatchingPreference(preferences_store[pref_index].pref, *pref))
{
foundInStore = true;
preferences_store[pref_index].dwarves.push_back(unit);
}
}
if (!foundInStore)
{
size_t pref_index = preferences_store.size();
preferences_store.resize(pref_index + 1);
preferences_store[pref_index].pref = *pref;
preferences_store[pref_index].dwarves.push_back(unit);
}
}
}
for (size_t i = 0; i < preferences_store.size(); i++)
{
preference_totals[i] = preferences_store[i].dwarves.size();
}
vector<pair<size_t, size_t>> rev_vec(preference_totals.begin(), preference_totals.end());
sort(rev_vec.begin(), rev_vec.end(), less_second<size_t, size_t>());
for (auto rev_it = rev_vec.begin(); rev_it != rev_vec.end(); rev_it++)
{
auto pref_index = rev_it->first;
preferences_store[pref_index].makeLabel();
string label = pad_string(int_to_string(rev_it->second), 3);
label += " ";
label += preferences_store[pref_index].label;
ListEntry<size_t> elem(label, pref_index, "", getItemColor(preferences_store[pref_index].pref.type));
preferences_column.add(elem);
}
dwarf_column.left_margin = preferences_column.fixWidth() + 2;
preferences_column.filterDisplay();
preferences_column.setHighlight(last_selected_index);
populateDwarfColumn();
}
bool isMatchingPreference(df::unit_preference &lhs, df::unit_preference &rhs)
{
if (lhs.type != rhs.type)
return false;
typedef df::unit_preference::T_type T_type;
switch (lhs.type)
{
case (T_type::LikeCreature):
if (lhs.creature_id != rhs.creature_id)
return false;
break;
case (T_type::HateCreature):
if (lhs.creature_id != rhs.creature_id)
return false;
break;
case (T_type::LikeFood):
if (lhs.item_type != rhs.item_type)
return false;
if (lhs.mattype != rhs.mattype || lhs.matindex != rhs.matindex)
return false;
break;
case (T_type::LikeItem):
if (lhs.item_type != rhs.item_type || lhs.item_subtype != rhs.item_subtype)
return false;
break;
case (T_type::LikeMaterial):
if (lhs.mattype != rhs.mattype || lhs.matindex != rhs.matindex)
return false;
break;
case (T_type::LikePlant):
if (lhs.plant_id != rhs.plant_id)
return false;
break;
case (T_type::LikeShape):
if (lhs.shape_id != rhs.shape_id)
return false;
break;
case (T_type::LikeTree):
if (lhs.item_type != rhs.item_type)
return false;
break;
case (T_type::LikeColor):
if (lhs.color_id != rhs.color_id)
return false;
break;
default:
return false;
}
return true;
}
UIColor getItemColor(const df::unit_preference::T_type &type) const
{
typedef df::unit_preference::T_type T_type;
switch (type)
{
case (T_type::LikeCreature):
return COLOR_WHITE;
case (T_type::HateCreature):
return COLOR_LIGHTRED;
case (T_type::LikeFood):
return COLOR_GREEN;
case (T_type::LikeItem):
return COLOR_YELLOW;
case (T_type::LikeMaterial):
return COLOR_CYAN;
case (T_type::LikePlant):
return COLOR_BROWN;
case (T_type::LikeShape):
return COLOR_BLUE;
case (T_type::LikeTree):
return COLOR_BROWN;
case (T_type::LikeColor):
return COLOR_BLUE;
default:
return false;
}
return true;
}
void populateDwarfColumn()
{
dwarf_column.clear();
if (preferences_column.getDisplayListSize() > 0)
{
auto selected_preference = preferences_column.getFirstSelectedElem();
for (auto dfit = preferences_store[selected_preference].dwarves.begin();
dfit != preferences_store[selected_preference].dwarves.end();
dfit++)
{
string label = getUnitName(*dfit);
auto happy = get_happiness_cat(*dfit);
UIColor color = monitor_colors[happy];
switch (happy)
{
case 0:
label += " (miserable)";
break;
case 1:
label += " (very unhappy)";
break;
case 2:
label += " (unhappy)";
break;
case 3:
label += " (fine)";
break;
case 4:
label += " (quite content)";
break;
case 5:
label += " (happy)";
break;
case 6:
label += " (ecstatic)";
break;
}
ListEntry<df::unit *> elem(label, *dfit, "", color);
dwarf_column.add(elem);
}
}
dwarf_column.clearSearch();
dwarf_column.setHighlight(0);
}
void feed(set<df::interface_key> *input)
{
bool key_processed = false;
switch (selected_column)
{
case 0:
key_processed = preferences_column.feed(input);
break;
case 1:
key_processed = dwarf_column.feed(input);
break;
}
if (key_processed)
{
if (selected_column == 0 && preferences_column.feed_changed_highlight)
{
populateDwarfColumn();
}
return;
}
if (input->count(interface_key::LEAVESCREEN))
{
input->clear();
Screen::dismiss(this);
return;
}
else if (input->count(interface_key::CUSTOM_SHIFT_Z))
{
df::unit *selected_unit = (selected_column == 1) ? dwarf_column.getFirstSelectedElem() : nullptr;
if (selected_unit)
{
input->clear();
Screen::dismiss(this);
Gui::resetDwarfmodeView(true);
send_key(interface_key::D_VIEWUNIT);
move_cursor(selected_unit->pos);
}
}
else if (input->count(interface_key::CURSOR_LEFT))
{
--selected_column;
validateColumn();
}
else if (input->count(interface_key::CURSOR_RIGHT))
{
++selected_column;
validateColumn();
}
else if (enabler->tracking_on && enabler->mouse_lbut)
{
if (preferences_column.setHighlightByMouse())
{
selected_column = 0;
populateDwarfColumn();
}
else if (dwarf_column.setHighlightByMouse())
selected_column = 1;
enabler->mouse_lbut = enabler->mouse_rbut = 0;
}
}
void render()
{
if (Screen::isDismissed(this))
return;
dfhack_viewscreen::render();
Screen::clear();
Screen::drawBorder(" Dwarf Preferences ");
preferences_column.display(selected_column == 0);
dwarf_column.display(selected_column == 1);
int32_t y = gps->dimy - 3;
int32_t x = 2;
OutputHotkeyString(x, y, "Leave", "Esc");
x += 2;
OutputHotkeyString(x, y, "Zoom Unit", "Shift-Z");
}
std::string getFocusString() { return "dwarfmonitor_preferences"; }
private:
ListColumn<size_t> preferences_column;
ListColumn<df::unit *> dwarf_column;
int selected_column;
map<size_t, size_t> preference_totals;
vector<preference_map> preferences_store;
void validateColumn()
{
set_to_limit(selected_column, 1);
}
void resize(int32_t x, int32_t y)
{
dfhack_viewscreen::resize(x, y);
preferences_column.resize();
dwarf_column.resize();
}
};
static void open_stats_srceen()
{
Screen::show(new ViewscreenFortStats());
@ -1045,21 +1613,7 @@ static void update_dwarf_stats(bool is_paused)
if (monitor_misery)
{
int happy = unit->status.happiness;
if (happy == 0) // miserable
misery[0]++;
else if (happy <= 25) // very unhappy
misery[1]++;
else if (happy <= 50) // unhappy
misery[2]++;
else if (happy <= 75) // fine
misery[3]++;
else if (happy <= 125) // quite content
misery[4]++;
else if (happy <= 150) // happy
misery[5]++;
else // ecstatic
misery[6]++;
misery[get_happiness_cat(unit)]++;
}
if (!monitor_jobs || is_paused)
@ -1094,6 +1648,7 @@ static void update_dwarf_stats(bool is_paused)
}
}
DFhackCExport command_result plugin_onupdate (color_ostream &out)
{
if (!monitor_jobs && !monitor_misery)
@ -1125,17 +1680,6 @@ DFhackCExport command_result plugin_onupdate (color_ostream &out)
return CR_OK;
}
static color_value monitor_colors[] =
{
COLOR_LIGHTRED,
COLOR_RED,
COLOR_YELLOW,
COLOR_WHITE,
COLOR_CYAN,
COLOR_LIGHTBLUE,
COLOR_LIGHTGREEN
};
struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
@ -1265,6 +1809,11 @@ static command_result dwarfmonitor_cmd(color_ostream &out, vector <string> & par
if(Maps::IsValid())
Screen::show(new ViewscreenFortStats());
}
else if (cmd == 'p' || cmd == 'P')
{
if(Maps::IsValid())
Screen::show(new ViewscreenPreferences());
}
else
{
show_help = true;
@ -1307,7 +1856,9 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector <Plugin
"dwarfmonitor disable <mode>\n"
" <mode> as above\n\n"
"dwarfmonitor stats\n"
" Show statistics summary\n\n"
" Show statistics summary\n"
"dwarfmonitor prefs\n"
" Show dwarf preferences summary\n\n"
));
return CR_OK;

@ -1,17 +1,32 @@
// (un)designate matching plants for gathering/cutting
#include "uicommon.h"
#include "Core.h"
#include "Console.h"
#include "Export.h"
#include "PluginManager.h"
#include "DataDefs.h"
#include "TileTypes.h"
#include "df/world.h"
#include "df/map_block.h"
#include "df/tile_dig_designation.h"
#include "df/plant_raw.h"
#include "df/plant.h"
#include "df/ui.h"
#include "df/burrow.h"
#include "df/item_flags.h"
#include "df/item.h"
#include "df/items_other_id.h"
#include "df/viewscreen_dwarfmodest.h"
#include "modules/Screen.h"
#include "modules/Maps.h"
#include "modules/Burrows.h"
#include "modules/World.h"
#include "modules/MapCache.h"
#include "modules/Gui.h"
#include <set>
@ -22,6 +37,601 @@ using namespace DFHack;
using namespace df::enums;
using df::global::world;
using df::global::ui;
#define PLUGIN_VERSION 0.3
DFHACK_PLUGIN("getplants");
static bool autochop_enabled = false;
static int min_logs, max_logs;
static bool wait_for_threshold;
static PersistentDataItem config_autochop;
struct WatchedBurrow
{
int32_t id;
df::burrow *burrow;
WatchedBurrow(df::burrow *burrow) : burrow(burrow)
{
id = burrow->id;
}
};
class WatchedBurrows
{
public:
string getSerialisedIds()
{
validate();
stringstream burrow_ids;
bool append_started = false;
for (auto it = burrows.begin(); it != burrows.end(); it++)
{
if (append_started)
burrow_ids << " ";
burrow_ids << it->id;
append_started = true;
}
return burrow_ids.str();
}
void clear()
{
burrows.clear();
}
void add(const int32_t id)
{
if (!isValidBurrow(id))
return;
WatchedBurrow wb(getBurrow(id));
burrows.push_back(wb);
}
void add(const string burrow_ids)
{
istringstream iss(burrow_ids);
int id;
while (iss >> id)
{
add(id);
}
}
bool isValidPos(const df::coord &plant_pos)
{
validate();
if (!burrows.size())
return true;
for (auto it = burrows.begin(); it != burrows.end(); it++)
{
df::burrow *burrow = it->burrow;
if (Burrows::isAssignedTile(burrow, plant_pos))
return true;
}
return false;
}
bool isBurrowWatched(const df::burrow *burrow)
{
validate();
for (auto it = burrows.begin(); it != burrows.end(); it++)
{
if (it->burrow == burrow)
return true;
}
return false;
}
private:
static bool isValidBurrow(const int32_t id)
{
return getBurrow(id);
}
static df::burrow *getBurrow(const int32_t id)
{
return df::burrow::find(id);
}
void validate()
{
for (auto it = burrows.begin(); it != burrows.end();)
{
if (!isValidBurrow(it->id))
it = burrows.erase(it);
else
++it;
}
}
vector<WatchedBurrow> burrows;
};
static WatchedBurrows watchedBurrows;
static void save_config()
{
config_autochop.val() = watchedBurrows.getSerialisedIds();
config_autochop.ival(0) = autochop_enabled;
config_autochop.ival(1) = min_logs;
config_autochop.ival(2) = max_logs;
config_autochop.ival(3) = wait_for_threshold;
}
static void initialize()
{
watchedBurrows.clear();
autochop_enabled = false;
min_logs = 80;
max_logs = 100;
wait_for_threshold = false;
config_autochop = World::GetPersistentData("autochop/config");
if (config_autochop.isValid())
{
watchedBurrows.add(config_autochop.val());
autochop_enabled = config_autochop.ival(0);
min_logs = config_autochop.ival(1);
max_logs = config_autochop.ival(2);
wait_for_threshold = config_autochop.ival(3);
}
else
{
config_autochop = World::AddPersistentData("autochop/config");
if (config_autochop.isValid())
save_config();
}
}
static int do_chop_designation(bool chop, bool count_only)
{
int count = 0;
for (size_t i = 0; i < world->map.map_blocks.size(); i++)
{
df::map_block *cur = world->map.map_blocks[i];
for (size_t j = 0; j < cur->plants.size(); j++)
{
const df::plant *plant = cur->plants[j];
int x = plant->pos.x % 16;
int y = plant->pos.y % 16;
if (plant->flags.bits.is_shrub)
continue;
if (cur->designation[x][y].bits.hidden)
continue;
df::tiletype_shape shape = tileShape(cur->tiletype[x][y]);
if (shape != tiletype_shape::TREE)
continue;
if (!count_only && !watchedBurrows.isValidPos(plant->pos))
continue;
bool dirty = false;
if (chop && cur->designation[x][y].bits.dig == tile_dig_designation::No)
{
if (count_only)
{
++count;
}
else
{
cur->designation[x][y].bits.dig = tile_dig_designation::Default;
dirty = true;
}
}
if (!chop && cur->designation[x][y].bits.dig == tile_dig_designation::Default)
{
if (count_only)
{
++count;
}
else
{
cur->designation[x][y].bits.dig = tile_dig_designation::No;
dirty = true;
}
}
if (dirty)
{
cur->flags.bits.designated = true;
++count;
}
}
}
return count;
}
static bool is_valid_item(df::item *item)
{
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;
}
}
return true;
}
static int get_log_count()
{
std::vector<df::item*> &items = world->items.other[items_other_id::IN_PLAY];
// Pre-compute 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 valid_count = 0;
for (size_t i = 0; i < items.size(); i++)
{
df::item *item = items[i];
if (item->getType() != item_type::WOOD)
continue;
if (item->flags.whole & bad_flags.whole)
continue;
if (!is_valid_item(item))
continue;
++valid_count;
}
return valid_count;
}
static void set_threshold_check(bool state)
{
wait_for_threshold = state;
save_config();
}
static void do_autochop()
{
int log_count = get_log_count();
if (wait_for_threshold)
{
if (log_count < min_logs)
{
set_threshold_check(false);
do_chop_designation(true, false);
}
}
else
{
if (log_count >= max_logs)
{
set_threshold_check(true);
do_chop_designation(false, false);
}
else
{
do_chop_designation(true, false);
}
}
}
class ViewscreenAutochop : public dfhack_viewscreen
{
public:
ViewscreenAutochop()
{
burrows_column.multiselect = true;
burrows_column.setTitle("Burrows");
burrows_column.bottom_margin = 3;
burrows_column.allow_search = false;
burrows_column.text_clip_at = 30;
populateBurrowsColumn();
message.clear();
}
void populateBurrowsColumn()
{
selected_column = 0;
auto last_selected_index = burrows_column.highlighted_index;
burrows_column.clear();
for (auto iter = ui->burrows.list.begin(); iter != ui->burrows.list.end(); iter++)
{
df::burrow* burrow = *iter;
auto elem = ListEntry<df::burrow *>(burrow->name, burrow);
elem.selected = watchedBurrows.isBurrowWatched(burrow);
burrows_column.add(elem);
}
burrows_column.fixWidth();
burrows_column.filterDisplay();
current_log_count = get_log_count();
marked_tree_count = do_chop_designation(false, true);
}
void change_min_logs(int delta)
{
if (!autochop_enabled)
return;
min_logs += delta;
if (min_logs < 0)
min_logs = 0;
if (min_logs > max_logs)
max_logs = min_logs;
}
void change_max_logs(int delta)
{
if (!autochop_enabled)
return;
max_logs += delta;
if (max_logs < min_logs)
min_logs = max_logs;
}
void feed(set<df::interface_key> *input)
{
bool key_processed = false;
message.clear();
switch (selected_column)
{
case 0:
key_processed = burrows_column.feed(input);
break;
}
if (key_processed)
{
if (input->count(interface_key::SELECT))
updateAutochopBurrows();
return;
}
if (input->count(interface_key::LEAVESCREEN))
{
save_config();
input->clear();
Screen::dismiss(this);
if (autochop_enabled)
do_autochop();
return;
}
else if (input->count(interface_key::CUSTOM_A))
{
autochop_enabled = !autochop_enabled;
}
else if (input->count(interface_key::CUSTOM_D))
{
int count = do_chop_designation(true, false);
message = "Trees marked for chop: " + int_to_string(count);
marked_tree_count = do_chop_designation(false, true);
}
else if (input->count(interface_key::CUSTOM_U))
{
int count = do_chop_designation(false, false);
message = "Trees unmarked: " + int_to_string(count);
marked_tree_count = do_chop_designation(false, true);
}
else if (input->count(interface_key::CUSTOM_H))
{
change_min_logs(-1);
}
else if (input->count(interface_key::CUSTOM_SHIFT_H))
{
change_min_logs(-10);
}
else if (input->count(interface_key::CUSTOM_J))
{
change_min_logs(1);
}
else if (input->count(interface_key::CUSTOM_SHIFT_J))
{
change_min_logs(10);
}
else if (input->count(interface_key::CUSTOM_K))
{
change_max_logs(-1);
}
else if (input->count(interface_key::CUSTOM_SHIFT_K))
{
change_max_logs(-10);
}
else if (input->count(interface_key::CUSTOM_L))
{
change_max_logs(1);
}
else if (input->count(interface_key::CUSTOM_SHIFT_L))
{
change_max_logs(10);
}
else if (enabler->tracking_on && enabler->mouse_lbut)
{
if (burrows_column.setHighlightByMouse())
{
selected_column = 0;
}
enabler->mouse_lbut = enabler->mouse_rbut = 0;
}
}
void render()
{
if (Screen::isDismissed(this))
return;
dfhack_viewscreen::render();
Screen::clear();
Screen::drawBorder(" Autochop ");
burrows_column.display(selected_column == 0);
int32_t y = gps->dimy - 3;
int32_t x = 2;
OutputHotkeyString(x, y, "Leave", "Esc");
x += 3;
OutputString(COLOR_YELLOW, x, y, message);
y = 3;
int32_t left_margin = burrows_column.getMaxItemWidth() + 3;
x = left_margin;
if (burrows_column.getSelectedElems().size() > 0)
{
OutputString(COLOR_GREEN, x, y, "Will chop in selected burrows", true, left_margin);
}
else
{
OutputString(COLOR_YELLOW, x, y, "Will chop from whole map", true, left_margin);
OutputString(COLOR_YELLOW, x, y, "Select from left to chop in specific burrows", true, left_margin);
}
++y;
OutputToggleString(x, y, "Autochop", "a", autochop_enabled, true, left_margin);
OutputHotkeyString(x, y, "Designate Now", "d", true, left_margin);
OutputHotkeyString(x, y, "Undesignate Now", "u", true, left_margin);
OutputHotkeyString(x, y, "Toggle Burrow", "Enter", true, left_margin);
if (autochop_enabled)
{
OutputLabelString(x, y, "Min Logs", "hjHJ", int_to_string(min_logs), true, left_margin);
OutputLabelString(x, y, "Max Logs", "klKL", int_to_string(max_logs), true, left_margin);
}
++y;
OutputString(COLOR_BROWN, x, y, "Current Counts", true, left_margin);
OutputString(COLOR_WHITE, x, y, "Current Logs: ");
OutputString(COLOR_GREEN, x, y, int_to_string(current_log_count), true, left_margin);
OutputString(COLOR_WHITE, x, y, "Marked Trees: ");
OutputString(COLOR_GREEN, x, y, int_to_string(marked_tree_count), true, left_margin);
}
std::string getFocusString() { return "autochop"; }
void updateAutochopBurrows()
{
watchedBurrows.clear();
vector<df::burrow *> v = burrows_column.getSelectedElems();
for_each_<df::burrow *>(v, [] (df::burrow *b) { watchedBurrows.add(b->id); });
}
private:
ListColumn<df::burrow *> burrows_column;
int selected_column;
int current_log_count;
int marked_tree_count;
MapExtras::MapCache mcache;
string message;
void validateColumn()
{
set_to_limit(selected_column, 0);
}
void resize(int32_t x, int32_t y)
{
dfhack_viewscreen::resize(x, y);
burrows_column.resize();
}
};
struct autochop_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
bool isInDesignationMenu()
{
using namespace df::enums::ui_sidebar_mode;
return (ui->main.mode == DesignateChopTrees);
}
void sendKey(const df::interface_key &key)
{
set<df::interface_key> tmp;
tmp.insert(key);
INTERPOSE_NEXT(feed)(&tmp);
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
if (isInDesignationMenu() && input->count(interface_key::CUSTOM_C))
{
sendKey(interface_key::LEAVESCREEN);
Screen::show(new ViewscreenAutochop());
}
else
{
INTERPOSE_NEXT(feed)(input);
}
}
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
auto dims = Gui::getDwarfmodeViewDims();
if (dims.menu_x1 <= 0)
return;
df::ui_sidebar_mode d = ui->main.mode;
if (!isInDesignationMenu())
return;
int left_margin = dims.menu_x1 + 1;
int x = left_margin;
int y = 26;
OutputHotkeyString(x, y, "Autochop Dashboard", "c");
}
};
IMPLEMENT_VMETHOD_INTERPOSE_PRIO(autochop_hook, feed, 100);
IMPLEMENT_VMETHOD_INTERPOSE_PRIO(autochop_hook, render, 100);
command_result df_getplants (color_ostream &out, vector <string> & parameters)
{
@ -45,6 +655,17 @@ command_result df_getplants (color_ostream &out, vector <string> & parameters)
exclude = true;
else if(parameters[i] == "-a")
all = true;
else if(parameters[i] == "debug")
{
save_config();
}
else if(parameters[i] == "autochop")
{
if(Maps::IsValid())
Screen::show(new ViewscreenAutochop());
return CR_OK;
}
else
plantNames.insert(parameters[i]);
}
@ -148,7 +769,48 @@ command_result df_getplants (color_ostream &out, vector <string> & parameters)
return CR_OK;
}
DFHACK_PLUGIN("getplants");
DFhackCExport command_result plugin_onupdate (color_ostream &out)
{
if (!autochop_enabled)
return CR_OK;
if(!Maps::IsValid())
return CR_OK;
static decltype(world->frame_counter) last_frame_count = 0;
if (DFHack::World::ReadPauseState())
return CR_OK;
if (world->frame_counter - last_frame_count < 1200) // Check every day
return CR_OK;
last_frame_count = world->frame_counter;
do_autochop();
return CR_OK;
}
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
{
if (!gps)
return CR_FAILURE;
if (enable != is_enabled)
{
if (!INTERPOSE_HOOK(autochop_hook, feed).apply(enable) ||
!INTERPOSE_HOOK(autochop_hook, render).apply(enable))
return CR_FAILURE;
is_enabled = enable;
initialize();
}
return CR_OK;
}
DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCommand> &commands)
{
@ -164,8 +826,11 @@ DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCom
" -x - Apply selected action to all plants except those specified\n"
" -a - Select every type of plant (obeys -t/-s)\n"
"Specifying both -t and -s will have no effect.\n"
"If no plant IDs are specified, all valid plant IDs will be listed.\n"
"If no plant IDs are specified, all valid plant IDs will be listed.\n\n"
" autochop - Opens the automated chopping control screen\n"
));
initialize();
return CR_OK;
}
@ -173,3 +838,16 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
initialize();
break;
default:
break;
}
return CR_OK;
}

@ -1,244 +1,792 @@
#include <sstream>
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <VTableInterpose.h>
#include "DataDefs.h"
#include "df/building.h"
#include "df/enabler.h"
#include "df/item.h"
#include "df/ui.h"
#include "df/unit.h"
#include "df/viewscreen_dwarfmodest.h"
#include "df/world.h"
#include "df/items_other_id.h"
#include "df/ui_build_selector.h"
#include "df/ui_sidebar_menus.h"
#include "modules/Gui.h"
#include "modules/Screen.h"
using std::set;
using std::string;
using std::ostringstream;
#include "modules/World.h"
#include "modules/Maps.h"
#include "modules/Buildings.h"
#include "modules/Items.h"
#include "modules/Units.h"
#include "modules/Translation.h"
using namespace DFHack;
using namespace df::enums;
#include "uicommon.h"
#include "TileTypes.h"
#include "DataFuncs.h"
using df::global::enabler;
using df::global::gps;
using df::global::world;
using df::global::ui;
using namespace df::enums::ui_sidebar_mode;
DFHACK_PLUGIN("mousequery");
#define PLUGIN_VERSION 0.17
static int32_t last_clicked_x, last_clicked_y, last_clicked_z;
static int32_t last_pos_x, last_pos_y, last_pos_z;
static df::coord last_move_pos;
static size_t max_list_size = 300000; // Avoid iterating over huge lists
static int32_t last_x, last_y, last_z;
static size_t max_list_size = 100000; // Avoid iterating over huge lists
static bool plugin_enabled = true;
static bool rbutton_enabled = true;
static bool tracking_enabled = false;
static bool active_scrolling = false;
static bool box_designation_enabled = false;
static bool live_view = true;
static bool skip_tracking_once = false;
static bool mouse_moved = false;
static int scroll_delay = 100;
static df::coord get_mouse_pos(int32_t &mx, int32_t &my)
{
df::coord pos;
pos.x = -30000;
if (!enabler->tracking_on)
return pos;
if (!Gui::getMousePos(mx, my))
return pos;
int32_t vx, vy, vz;
if (!Gui::getViewCoords(vx, vy, vz))
return pos;
pos.x = vx + mx - 1;
pos.y = vy + my - 1;
pos.z = vz;
return pos;
}
static bool is_valid_pos(const df::coord pos)
{
auto designation = Maps::getTileDesignation(pos);
if (!designation)
return false;
if (designation->bits.hidden)
return false; // Items in parts of the map not yet revealed
return true;
}
static vector<df::unit *> get_units_at(const df::coord pos, bool only_one)
{
vector<df::unit *> list;
auto count = world->units.active.size();
if (count > max_list_size)
return list;
df::unit_flags1 bad_flags;
bad_flags.whole = 0;
bad_flags.bits.dead = true;
bad_flags.bits.hidden_ambusher = true;
bad_flags.bits.hidden_in_ambush = true;
for (size_t i = 0; i < count; i++)
{
df::unit *unit = world->units.active[i];
if(unit->pos.x == pos.x && unit->pos.y == pos.y && unit->pos.z == pos.z &&
!(unit->flags1.whole & bad_flags.whole) &&
unit->profession != profession::THIEF && unit->profession != profession::MASTER_THIEF)
{
list.push_back(unit);
if (only_one)
break;
}
}
return list;
}
static vector<df::item *> get_items_at(const df::coord pos, bool only_one)
{
vector<df::item *> list;
auto count = world->items.other[items_other_id::IN_PLAY].size();
if (count > max_list_size)
return list;
df::item_flags bad_flags;
bad_flags.whole = 0;
bad_flags.bits.in_building = true;
bad_flags.bits.garbage_collect = true;
bad_flags.bits.removed = true;
bad_flags.bits.dead_dwarf = true;
bad_flags.bits.murder = true;
bad_flags.bits.construction = true;
bad_flags.bits.in_inventory = true;
bad_flags.bits.in_chest = true;
for (size_t i = 0; i < count; i++)
{
df::item *item = world->items.other[items_other_id::IN_PLAY][i];
if (item->flags.whole & bad_flags.whole)
continue;
if (pos.z == item->pos.z && pos.x == item->pos.x && pos.y == item->pos.y)
list.push_back(item);
}
return list;
}
static df::interface_key get_default_query_mode(const df::coord pos)
{
if (!is_valid_pos(pos))
return df::interface_key::D_LOOK;
bool fallback_to_building_query = false;
// Check for unit under cursor
auto ulist = get_units_at(pos, true);
if (ulist.size() > 0)
return df::interface_key::D_VIEWUNIT;
// Check for building under cursor
auto bld = Buildings::findAtTile(pos);
if (bld)
{
df::building_type type = bld->getType();
if (type == building_type::Stockpile)
{
fallback_to_building_query = true;
}
else
{
// For containers use item view, for everything else, query view
return (type == building_type::Box || type == building_type::Cabinet ||
type == building_type::Weaponrack || type == building_type::Armorstand)
? df::interface_key::D_BUILDITEM : df::interface_key::D_BUILDJOB;
}
}
// Check for items under cursor
auto ilist = get_items_at(pos, true);
if (ilist.size() > 0)
return df::interface_key::D_LOOK;
return (fallback_to_building_query) ? df::interface_key::D_BUILDJOB : df::interface_key::D_LOOK;
}
struct mousequery_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
void send_key(const df::interface_key &key)
void sendKey(const df::interface_key &key)
{
set<df::interface_key> tmp;
tmp.insert(key);
//INTERPOSE_NEXT(feed)(&tmp);
this->feed(&tmp);
INTERPOSE_NEXT(feed)(&tmp);
}
bool isInDesignationMenu()
{
switch (ui->main.mode)
{
case DesignateMine:
case DesignateRemoveRamps:
case DesignateUpStair:
case DesignateDownStair:
case DesignateUpDownStair:
case DesignateUpRamp:
case DesignateChannel:
case DesignateGatherPlants:
case DesignateRemoveDesignation:
case DesignateSmooth:
case DesignateCarveTrack:
case DesignateEngrave:
case DesignateCarveFortification:
case DesignateChopTrees:
case DesignateToggleEngravings:
case DesignateRemoveConstruction:
case DesignateTrafficHigh:
case DesignateTrafficNormal:
case DesignateTrafficLow:
case DesignateTrafficRestricted:
return true;
case Burrows:
return ui->burrows.in_define_mode;
};
return false;
}
df::interface_key get_default_query_mode(const int32_t &x, const int32_t &y, const int32_t &z)
bool isInTrackableMode()
{
bool fallback_to_building_query = false;
if (isInDesignationMenu())
return box_designation_enabled;
// Check for unit under cursor
size_t count = world->units.all.size();
if (count <= max_list_size)
switch (ui->main.mode)
{
for(size_t i = 0; i < count; i++)
{
df::unit *unit = world->units.all[i];
case DesignateItemsClaim:
case DesignateItemsForbid:
case DesignateItemsMelt:
case DesignateItemsUnmelt:
case DesignateItemsDump:
case DesignateItemsUndump:
case DesignateItemsHide:
case DesignateItemsUnhide:
case DesignateTrafficHigh:
case DesignateTrafficNormal:
case DesignateTrafficLow:
case DesignateTrafficRestricted:
case Stockpiles:
case Squads:
case NotesPoints:
case NotesRoutes:
case Zones:
return true;
case Build:
return inBuildPlacement();
case QueryBuilding:
case BuildingItems:
case ViewUnits:
case LookAround:
return !enabler->mouse_lbut;
default:
return false;
};
}
if(unit->pos.x == x && unit->pos.y == y && unit->pos.z == z)
return df::interface_key::D_VIEWUNIT;
}
}
else
bool isInAreaSelectionMode()
{
bool selectableMode =
isInDesignationMenu() ||
ui->main.mode == Stockpiles ||
ui->main.mode == Zones;
if (selectableMode)
{
fallback_to_building_query = true;
int32_t x, y, z;
return Gui::getDesignationCoords(x, y, z);
}
// Check for building under cursor
count = world->buildings.all.size();
if (count <= max_list_size)
return false;
}
bool handleMouse(const set<df::interface_key> *input)
{
int32_t mx, my;
auto mpos = get_mouse_pos(mx, my);
if (mpos.x == -30000)
return false;
auto dims = Gui::getDwarfmodeViewDims();
if (enabler->mouse_lbut)
{
for(size_t i = 0; i < count; i++)
{
df::building *bld = world->buildings.all[i];
bool cursor_still_here = (last_clicked_x == mpos.x && last_clicked_y == mpos.y && last_clicked_z == mpos.z);
last_clicked_x = mpos.x;
last_clicked_y = mpos.y;
last_clicked_z = mpos.z;
if (z == bld->z &&
x >= bld->x1 && x <= bld->x2 &&
y >= bld->y1 && y <= bld->y2)
{
df::building_type type = bld->getType();
df::interface_key key = interface_key::NONE;
bool designationMode = false;
bool skipRefresh = false;
if (type == building_type::Stockpile)
if (isInTrackableMode())
{
designationMode = true;
key = df::interface_key::SELECT;
}
else
{
switch (ui->main.mode)
{
case QueryBuilding:
if (cursor_still_here)
key = df::interface_key::D_BUILDITEM;
break;
case BuildingItems:
if (cursor_still_here)
key = df::interface_key::D_VIEWUNIT;
break;
case ViewUnits:
if (cursor_still_here)
key = df::interface_key::D_LOOK;
break;
case LookAround:
if (cursor_still_here)
key = df::interface_key::D_BUILDJOB;
break;
case Build:
if (df::global::ui_build_selector)
{
fallback_to_building_query = true;
break; // Check for items in stockpile first
if (df::global::ui_build_selector->stage < 2)
{
designationMode = true;
key = df::interface_key::SELECT;
}
else
{
designationMode = true;
skipRefresh = true;
key = df::interface_key::SELECT_ALL;
}
}
break;
case Default:
break;
// For containers use item view, fir everything else, query view
return (type == building_type::Box || type == building_type::Cabinet ||
type == building_type::Weaponrack || type == building_type::Armorstand)
? df::interface_key::D_BUILDITEM : df::interface_key::D_BUILDJOB;
default:
return false;
}
}
}
else
{
fallback_to_building_query = true;
}
enabler->mouse_lbut = 0;
// Check for items under cursor
count = world->items.all.size();
if (count <= max_list_size)
{
for(size_t i = 0; i < count; i++)
// Can't check limits earlier as we must be sure we are in query or default mode
// (so we can clear the button down flag)
int right_bound = (dims.menu_x1 > 0) ? dims.menu_x1 - 2 : gps->dimx - 2;
if (mx < 1 || mx > right_bound || my < 1 || my > gps->dimy - 2)
return false;
if (ui->main.mode == df::ui_sidebar_mode::Zones ||
ui->main.mode == df::ui_sidebar_mode::Stockpiles)
{
df::item *item = world->items.all[i];
if (z == item->pos.z && x == item->pos.x && y == item->pos.y &&
!item->flags.bits.in_building && !item->flags.bits.hidden &&
!item->flags.bits.in_job && !item->flags.bits.in_chest &&
!item->flags.bits.in_inventory)
int32_t x, y, z;
if (Gui::getDesignationCoords(x, y, z))
{
return df::interface_key::D_LOOK;
auto dX = abs(x - mpos.x);
if (dX > 30)
return false;
auto dY = abs(y - mpos.y);
if (dY > 30)
return false;
}
}
if (!designationMode)
{
while (ui->main.mode != Default)
{
sendKey(df::interface_key::LEAVESCREEN);
}
if (key == interface_key::NONE)
key = get_default_query_mode(mpos);
sendKey(key);
}
if (!skipRefresh)
{
// Force UI refresh
moveCursor(mpos, true);
}
if (designationMode)
sendKey(key);
return true;
}
else if (rbutton_enabled && enabler->mouse_rbut)
{
if (isInDesignationMenu() && !box_designation_enabled)
return false;
// Escape out of query mode
enabler->mouse_rbut_down = 0;
enabler->mouse_rbut = 0;
using namespace df::enums::ui_sidebar_mode;
if ((ui->main.mode == QueryBuilding || ui->main.mode == BuildingItems ||
ui->main.mode == ViewUnits || ui->main.mode == LookAround) ||
(isInTrackableMode() && tracking_enabled))
{
sendKey(df::interface_key::LEAVESCREEN);
}
else
{
int scroll_trigger_x = dims.menu_x1 / 3;
int scroll_trigger_y = gps->dimy / 3;
if (mx < scroll_trigger_x)
sendKey(interface_key::CURSOR_LEFT_FAST);
if (mx > ((dims.menu_x1 > 0) ? dims.menu_x1 : gps->dimx) - scroll_trigger_x)
sendKey(interface_key::CURSOR_RIGHT_FAST);
if (my < scroll_trigger_y)
sendKey(interface_key::CURSOR_UP_FAST);
if (my > gps->dimy - scroll_trigger_y)
sendKey(interface_key::CURSOR_DOWN_FAST);
}
}
else if (input->count(interface_key::CUSTOM_M) && isInDesignationMenu())
{
box_designation_enabled = !box_designation_enabled;
}
else
{
fallback_to_building_query = true;
if (input->count(interface_key::CURSOR_UP) ||
input->count(interface_key::CURSOR_DOWN) ||
input->count(interface_key::CURSOR_LEFT) ||
input->count(interface_key::CURSOR_RIGHT) ||
input->count(interface_key::CURSOR_UPLEFT) ||
input->count(interface_key::CURSOR_UPRIGHT) ||
input->count(interface_key::CURSOR_DOWNLEFT) ||
input->count(interface_key::CURSOR_DOWNRIGHT) ||
input->count(interface_key::CURSOR_UP_FAST) ||
input->count(interface_key::CURSOR_DOWN_FAST) ||
input->count(interface_key::CURSOR_LEFT_FAST) ||
input->count(interface_key::CURSOR_RIGHT_FAST) ||
input->count(interface_key::CURSOR_UPLEFT_FAST) ||
input->count(interface_key::CURSOR_UPRIGHT_FAST) ||
input->count(interface_key::CURSOR_DOWNLEFT_FAST) ||
input->count(interface_key::CURSOR_DOWNRIGHT_FAST) ||
input->count(interface_key::CURSOR_UP_Z) ||
input->count(interface_key::CURSOR_DOWN_Z) ||
input->count(interface_key::CURSOR_UP_Z_AUX) ||
input->count(interface_key::CURSOR_DOWN_Z_AUX))
{
mouse_moved = false;
if (shouldTrack())
skip_tracking_once = true;
}
}
return (fallback_to_building_query) ? df::interface_key::D_BUILDJOB : df::interface_key::D_LOOK;
return false;
}
bool handle_mouse(const set<df::interface_key> *input)
void moveCursor(df::coord &mpos, bool forced)
{
int32_t cx, cy, vz;
if (enabler->tracking_on)
bool should_skip_tracking = skip_tracking_once;
skip_tracking_once = false;
if (!forced)
{
if (mpos.x == last_pos_x && mpos.y == last_pos_y && mpos.z == last_pos_z)
return;
}
last_pos_x = mpos.x;
last_pos_y = mpos.y;
last_pos_z = mpos.z;
if (!forced && should_skip_tracking)
{
if (enabler->mouse_lbut)
return;
}
int32_t x, y, z;
Gui::getCursorCoords(x, y, z);
if (mpos.x == x && mpos.y == y && mpos.z == z)
return;
Gui::setCursorCoords(mpos.x, mpos.y, mpos.z);
sendKey(interface_key::CURSOR_DOWN_Z);
sendKey(interface_key::CURSOR_UP_Z);
}
bool inBuildPlacement()
{
return df::global::ui_build_selector &&
df::global::ui_build_selector->building_type != -1 &&
df::global::ui_build_selector->stage == 1;
}
bool shouldTrack()
{
if (!tracking_enabled)
return false;
return isInTrackableMode();
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
if (!plugin_enabled || !handleMouse(input))
INTERPOSE_NEXT(feed)(input);
}
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
if (!plugin_enabled)
return;
static decltype(enabler->clock) last_t = 0;
auto dims = Gui::getDwarfmodeViewDims();
auto right_margin = (dims.menu_x1 > 0) ? dims.menu_x1 : gps->dimx;
int32_t mx, my;
auto mpos = get_mouse_pos(mx, my);
bool mpos_valid = mpos.x != -30000 && mpos.y != -30000 && mpos.z != -30000;
if (mx < 1 || mx > right_margin - 2 || my < 1 || my > gps->dimy - 2)
mpos_valid = false;
if (mpos_valid)
{
if (mpos.x != last_move_pos.x || mpos.y != last_move_pos.y || mpos.z != last_move_pos.z)
{
int32_t mx, my;
if (Gui::getMousePos(mx, my))
{
int32_t vx, vy;
if (Gui::getViewCoords(vx, vy, vz))
{
cx = vx + mx - 1;
cy = vy + my - 1;
mouse_moved = true;
last_move_pos = mpos;
}
}
using namespace df::enums::ui_sidebar_mode;
df::interface_key key = interface_key::NONE;
bool cursor_still_here = (last_x == cx && last_y == cy && last_z == vz);
switch(ui->main.mode)
{
case QueryBuilding:
if (cursor_still_here)
key = df::interface_key::D_BUILDITEM;
break;
case BuildingItems:
if (cursor_still_here)
key = df::interface_key::D_VIEWUNIT;
break;
case ViewUnits:
if (cursor_still_here)
key = df::interface_key::D_LOOK;
break;
case LookAround:
if (cursor_still_here)
key = df::interface_key::D_BUILDJOB;
break;
case Default:
break;
default:
return false;
}
int left_margin = dims.menu_x1 + 1;
int look_width = dims.menu_x2 - dims.menu_x1 - 1;
int disp_x = left_margin;
enabler->mouse_lbut = 0;
if (isInDesignationMenu())
{
int x = left_margin;
int y = 24;
OutputString(COLOR_BROWN, x, y, "DFHack MouseQuery", true, left_margin);
OutputToggleString(x, y, "Box Select", "m", box_designation_enabled, true, left_margin);
}
// Can't check limits earlier as we must be sure we are in query or default mode we can clear the button flag
// Otherwise the feed gets stuck in a loop
uint8_t menu_width, area_map_width;
Gui::getMenuWidth(menu_width, area_map_width);
int32_t w = gps->dimx;
if (menu_width == 1) w -= 57; //Menu is open doubly wide
else if (menu_width == 2 && area_map_width == 3) w -= 33; //Just the menu is open
else if (menu_width == 2 && area_map_width == 2) w -= 26; //Just the area map is open
//Display selection dimensions
bool showing_dimensions = false;
if (isInAreaSelectionMode())
{
showing_dimensions = true;
int32_t x, y, z;
Gui::getDesignationCoords(x, y, z);
coord32_t curr_pos;
if (mx < 1 || mx > w || my < 1 || my > gps->dimy - 2)
return false;
if (!tracking_enabled && mouse_moved && mpos_valid &&
(!isInDesignationMenu() || box_designation_enabled))
{
curr_pos = mpos;
}
else
{
Gui::getCursorCoords(curr_pos.x, curr_pos.y, curr_pos.z);
}
auto dX = abs(x - curr_pos.x) + 1;
auto dY = abs(y - curr_pos.y) + 1;
auto dZ = abs(z - curr_pos.z) + 1;
int disp_y = gps->dimy - 3;
stringstream label;
label << "Selection: " << dX << "x" << dY << "x" << dZ;
OutputString(COLOR_WHITE, disp_x, disp_y, label.str());
}
else
{
mouse_moved = false;
}
while (ui->main.mode != Default)
{
send_key(df::interface_key::LEAVESCREEN);
}
if (!mpos_valid)
return;
if (key == interface_key::NONE)
key = get_default_query_mode(cx, cy, vz);
int scroll_buffer = 6;
auto delta_t = enabler->clock - last_t;
if (active_scrolling && !isInTrackableMode() && delta_t > scroll_delay)
{
last_t = enabler->clock;
if (mx < scroll_buffer)
{
sendKey(interface_key::CURSOR_LEFT);
return;
}
send_key(key);
if (mx > right_margin - scroll_buffer)
{
sendKey(interface_key::CURSOR_RIGHT);
return;
}
// Force UI refresh
Gui::setCursorCoords(cx, cy, vz);
send_key(interface_key::CURSOR_DOWN_Z);
send_key(interface_key::CURSOR_UP_Z);
last_x = cx;
last_y = cy;
last_z = vz;
if (my < scroll_buffer)
{
sendKey(interface_key::CURSOR_UP);
return;
}
return true;
}
}
if (my > gps->dimy - scroll_buffer)
{
sendKey(interface_key::CURSOR_DOWN);
return;
}
else if (enabler->mouse_rbut)
}
if (!live_view && !isInTrackableMode() && !DFHack::World::ReadPauseState())
return;
if (!tracking_enabled && isInTrackableMode())
{
UIColor color = COLOR_GREEN;
int32_t x, y, z;
if (Gui::getDesignationCoords(x, y, z))
{
// Escape out of query mode
using namespace df::enums::ui_sidebar_mode;
if (ui->main.mode == QueryBuilding || ui->main.mode == BuildingItems ||
ui->main.mode == ViewUnits || ui->main.mode == LookAround)
color = COLOR_WHITE;
if (ui->main.mode == df::ui_sidebar_mode::Zones ||
ui->main.mode == df::ui_sidebar_mode::Stockpiles)
{
while (ui->main.mode != Default)
{
enabler->mouse_rbut = 0;
send_key(df::interface_key::LEAVESCREEN);
}
auto dX = abs(x - mpos.x);
if (dX > 30)
color = COLOR_RED;
auto dY = abs(y - mpos.y);
if (dY > 30)
color = COLOR_RED;
}
}
OutputString(color, mx, my, "X");
return;
}
if (shouldTrack())
{
if (delta_t <= scroll_delay && (mx < scroll_buffer ||
mx > dims.menu_x1 - scroll_buffer ||
my < scroll_buffer ||
my > gps->dimy - scroll_buffer))
{
return;
}
last_t = enabler->clock;
moveCursor(mpos, false);
}
return false;
if (dims.menu_x1 <= 0)
return; // No menu displayed
if (!is_valid_pos(mpos) || isInTrackableMode())
return;
if (showing_dimensions)
return;
// Display live query
auto ulist = get_units_at(mpos, false);
auto bld = Buildings::findAtTile(mpos);
auto ilist = get_items_at(mpos, false);
int look_list = ulist.size() + ((bld) ? 1 : 0) + ilist.size() + 1;
set_to_limit(look_list, 8);
int disp_y = gps->dimy - look_list - 2;
int c = 0;
for (auto it = ulist.begin(); it != ulist.end() && c < 8; it++, c++)
{
string label;
auto name = Units::getVisibleName(*it);
if (name->has_name)
label = Translation::TranslateName(name, false);
if (label.length() > 0)
label += ", ";
label += Units::getProfessionName(*it); // Check animal type too
label = pad_string(label, look_width, false, true);
OutputString(COLOR_WHITE, disp_x, disp_y, label, true, left_margin);
}
for (auto it = ilist.begin(); it != ilist.end() && c < 8; it++, c++)
{
auto label = Items::getDescription(*it, 0, false);
label = pad_string(label, look_width, false, true);
OutputString(COLOR_YELLOW, disp_x, disp_y, label, true, left_margin);
}
if (c > 7)
return;
if (bld)
{
string label;
bld->getName(&label);
label = pad_string(label, look_width, false, true);
OutputString(COLOR_CYAN, disp_x, disp_y, label, true, left_margin);
}
if (c > 7)
return;
auto tt = Maps::getTileType(mpos);
OutputString(COLOR_BLUE, disp_x, disp_y, tileName(*tt), true, left_margin);
}
};
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
IMPLEMENT_VMETHOD_INTERPOSE_PRIO(mousequery_hook, feed, 100);
IMPLEMENT_VMETHOD_INTERPOSE_PRIO(mousequery_hook, render, 100);
static command_result mousequery_cmd(color_ostream &out, vector <string> & parameters)
{
bool show_help = false;
if (parameters.size() < 1)
{
if (!handle_mouse(input))
INTERPOSE_NEXT(feed)(input);
show_help = true;
}
else
{
auto cmd = toLower(parameters[0]);
auto state = (parameters.size() == 2) ? toLower(parameters[1]) : "-1";
if (cmd[0] == 'v')
{
out << "MouseQuery" << endl << "Version: " << PLUGIN_VERSION << endl;
}
else if (cmd[0] == 'p')
{
plugin_enabled = (state == "enable");
}
else if (cmd[0] == 'r')
{
rbutton_enabled = (state == "enable");
}
else if (cmd[0] == 't')
{
tracking_enabled = (state == "enable");
if (!tracking_enabled)
active_scrolling = false;
}
else if (cmd[0] == 'e')
{
active_scrolling = (state == "enable");
if (active_scrolling)
tracking_enabled = true;
}
else if (cmd[0] == 'l')
{
live_view = (state == "enable");
}
else if (cmd[0] == 'd')
{
auto l = atoi(state.c_str());
if (l > 0 || state == "0")
scroll_delay = l;
else
out << "Current delay: " << scroll_delay << endl;
}
else
{
show_help = true;
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(mousequery_hook, feed);
if (show_help)
return CR_WRONG_USAGE;
DFHACK_PLUGIN("mousequery");
return CR_OK;
}
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable)
@ -248,9 +796,12 @@ DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable)
if (is_enabled != enable)
{
last_x = last_y = last_z = -1;
last_clicked_x = last_clicked_y = last_clicked_z = -1;
last_pos_x = last_pos_y = last_pos_z = -1;
last_move_pos.x = last_move_pos.y = last_move_pos.z = -1;
if (!INTERPOSE_HOOK(mousequery_hook, feed).apply(enable))
if (!INTERPOSE_HOOK(mousequery_hook, feed).apply(enable) ||
!INTERPOSE_HOOK(mousequery_hook, render).apply(enable))
return CR_FAILURE;
is_enabled = enable;
@ -261,7 +812,19 @@ DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable)
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
last_x = last_y = last_z = -1;
commands.push_back(
PluginCommand(
"mousequery", "Add mouse functionality to Dwarf Fortress",
mousequery_cmd, false,
"mousequery [plugin|rbutton|track|edge|live] [enabled|disabled]\n"
" plugin: enable/disable the entire plugin\n"
" rbutton: enable/disable right mouse button\n"
" track: enable/disable moving cursor in build and designation mode\n"
" edge: enable/disable active edge scrolling (when on, will also enable tracking)\n"
" live: enable/disable query view when unpaused\n\n"
"mousequery delay <amount>\n"
" Set delay when edge scrolling in tracking mode. Omit amount to display current setting.\n"
));
return CR_OK;
}
@ -270,7 +833,9 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan
{
switch (event) {
case SC_MAP_LOADED:
last_x = last_y = last_z = -1;
last_clicked_x = last_clicked_y = last_clicked_z = -1;
last_pos_x = last_pos_y = last_pos_z = -1;
last_move_pos.x = last_move_pos.y = last_move_pos.z = -1;
break;
default:
break;

@ -11,6 +11,7 @@
#include "df/viewscreen_layer_stockpilest.h"
#include "df/viewscreen_layer_militaryst.h"
#include "df/viewscreen_layer_noblelistst.h"
#include "df/viewscreen_layer_workshop_profilest.h"
#include "df/viewscreen_tradegoodsst.h"
#include "df/viewscreen_unitlistst.h"
#include "df/viewscreen_buildinglistst.h"
@ -423,7 +424,7 @@ protected:
virtual bool can_init(S *screen)
{
auto list = getLayerList(screen);
if (!is_list_valid(screen) || !list->active)
if (!is_list_valid(screen) || !list || !list->active)
return false;
return true;
@ -699,8 +700,8 @@ template <class T, class V, int D> V generic_search_hook<T, V, D> ::module;
#define IMPLEMENT_HOOKS_PRIO(screen, module, prio) \
typedef generic_search_hook<screen, module> module##_hook; \
template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, feed, 100); \
template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, render, 100)
template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, feed, prio); \
template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, render, prio)
//
// END: Generic Search functionality
@ -920,7 +921,7 @@ private:
};
IMPLEMENT_HOOKS(df::viewscreen_storesst, stocks_search);
IMPLEMENT_HOOKS_PRIO(df::viewscreen_storesst, stocks_search, 100);
//
// END: Stocks screen search
@ -1048,7 +1049,9 @@ private:
{
// Block the keys if were searching
if (!search_string.empty())
{
input->clear();
}
return false;
}
@ -1081,6 +1084,9 @@ public:
{
make_text_dim(2, 37, 22);
make_text_dim(42, gps->dimx-2, 22);
int32_t x = 2;
int32_t y = gps->dimy - 3;
OutputString(COLOR_YELLOW, x, y, "Note: Clear search to trade");
}
}
@ -1120,6 +1126,9 @@ public:
{
make_text_dim(2, 37, 22);
make_text_dim(42, gps->dimx-2, 22);
int32_t x = 42;
int32_t y = gps->dimy - 3;
OutputString(COLOR_YELLOW, x, y, "Note: Clear search to trade");
}
}
@ -1432,6 +1441,36 @@ IMPLEMENT_HOOKS(df::viewscreen_layer_noblelistst, nobles_search);
// END: Nobles search list
//
//
// START: Workshop profiles search list
//
typedef layered_search<df::viewscreen_layer_workshop_profilest, df::unit*, 0> profiles_search_base;
class profiles_search : public profiles_search_base
{
public:
string get_element_description(df::unit *element) const
{
return get_unit_description(element);
}
void render() const
{
print_search_option(2, 23);
}
vector<df::unit *> *get_primary_list()
{
return &viewscreen->workers;
}
};
IMPLEMENT_HOOKS(df::viewscreen_layer_workshop_profilest, profiles_search);
//
// END: Workshop profiles search list
//
//
// START: Job list search
@ -1621,6 +1660,7 @@ DFHACK_PLUGIN_IS_ENABLED(is_enabled);
HOOK_ACTION(pets_search_hook) \
HOOK_ACTION(military_search_hook) \
HOOK_ACTION(nobles_search_hook) \
HOOK_ACTION(profiles_search_hook) \
HOOK_ACTION(annoucnement_search_hook) \
HOOK_ACTION(joblist_search_hook) \
HOOK_ACTION(burrow_search_hook) \

File diff suppressed because it is too large Load Diff

@ -1,4 +1,7 @@
#include <algorithm>
#include <cctype>
#include <functional>
#include <locale>
#include <map>
#include <string>
#include <set>
@ -10,9 +13,17 @@
#include <PluginManager.h>
#include <VTableInterpose.h>
#include "modules/Items.h"
#include "modules/Screen.h"
#include "modules/World.h"
#include "df/building_stockpilest.h"
#include "df/caravan_state.h"
#include "df/dfhack_material_category.h"
#include "df/enabler.h"
#include "df/item_quality.h"
#include "df/ui.h"
#include "df/world.h"
using namespace std;
using std::string;
@ -32,11 +43,34 @@ using df::global::gps;
#define nullptr 0L
#endif
#define COLOR_TITLE COLOR_BLUE
#define COLOR_TITLE COLOR_BROWN
#define COLOR_UNSELECTED COLOR_GREY
#define COLOR_SELECTED COLOR_WHITE
#define COLOR_HIGHLIGHTED COLOR_GREEN
struct coord32_t
{
int32_t x, y, z;
coord32_t()
{
x = -30000;
y = -30000;
z = -30000;
}
coord32_t(df::coord& other)
{
x = other.x;
y = other.y;
z = other.z;
}
df::coord get_coord16() const
{
return df::coord(x, y, z);
}
};
template <class T, typename Fn>
static void for_each_(vector<T> &v, Fn func)
@ -80,6 +114,17 @@ void OutputHotkeyString(int &x, int &y, const char *text, const char *hotkey, bo
OutputString(text_color, x, y, display, newline, left_margin);
}
void OutputLabelString(int &x, int &y, const char *text, const char *hotkey, const string &label, bool newline = false,
int left_margin = 0, int8_t text_color = COLOR_WHITE, int8_t hotkey_color = COLOR_LIGHTGREEN)
{
OutputString(hotkey_color, x, y, hotkey);
string display(": ");
display.append(text);
display.append(": ");
OutputString(text_color, x, y, display);
OutputString(hotkey_color, x, y, label, newline, left_margin);
}
void OutputFilterString(int &x, int &y, const char *text, const char *hotkey, bool state, bool newline = false,
int left_margin = 0, int8_t hotkey_color = COLOR_LIGHTGREEN)
{
@ -93,9 +138,9 @@ void OutputToggleString(int &x, int &y, const char *text, const char *hotkey, bo
OutputHotkeyString(x, y, text, hotkey);
OutputString(COLOR_WHITE, x, y, ": ");
if (state)
OutputString(COLOR_GREEN, x, y, "Enabled", newline, left_margin);
OutputString(COLOR_GREEN, x, y, "On", newline, left_margin);
else
OutputString(COLOR_GREY, x, y, "Disabled", newline, left_margin);
OutputString(COLOR_GREY, x, y, "Off", newline, left_margin);
}
const int ascii_to_enum_offset = interface_key::STRING_A048 - '0';
@ -113,6 +158,22 @@ static void set_to_limit(int &value, const int maximum, const int min = 0)
value = maximum;
}
// trim from start
static inline std::string &ltrim(std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), std::not1(std::ptr_fun<int, int>(std::isspace))));
return s;
}
// trim from end
static inline std::string &rtrim(std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end());
return s;
}
// trim from both ends
static inline std::string &trim(std::string &s) {
return ltrim(rtrim(s));
}
inline void paint_text(const UIColor color, const int &x, const int &y, const std::string &text, const UIColor background = 0)
{
@ -145,6 +206,215 @@ static string pad_string(string text, const int size, const bool front = true, c
}
/*
* Utility Functions
*/
static df::building_stockpilest *get_selected_stockpile()
{
if (!Gui::dwarfmode_hotkey(Core::getTopViewscreen()) ||
df::global::ui->main.mode != ui_sidebar_mode::QueryBuilding)
{
return nullptr;
}
return virtual_cast<df::building_stockpilest>(df::global::world->selected_building);
}
static bool can_trade()
{
if (df::global::ui->caravans.size() == 0)
return false;
for (auto it = df::global::ui->caravans.begin(); it != df::global::ui->caravans.end(); it++)
{
auto caravan = *it;
auto trade_state = caravan->trade_state;
auto time_remaining = caravan->time_remaining;
if ((trade_state != 1 && trade_state != 2) || time_remaining == 0)
return false;
}
return true;
}
static bool is_metal_item(df::item *item)
{
MaterialInfo mat(item);
return (mat.getCraftClass() == craft_material_class::Metal);
}
bool is_set_to_melt(df::item* item)
{
return item->flags.bits.melt;
}
// Copied from Kelly Martin's code
bool can_melt(df::item* item)
{
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(in_job);
F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(artifact); F(melt);
#undef F
if (item->flags.whole & bad_flags.whole)
return false;
df::item_type t = item->getType();
if (t == df::enums::item_type::BOX || t == df::enums::item_type::BAR)
return false;
if (!is_metal_item(item)) return false;
for (auto g = item->general_refs.begin(); g != item->general_refs.end(); g++)
{
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.begin(); gg != c->general_refs.end(); gg++)
{
if ((*gg)->getType() == general_ref_type::UNIT_HOLDER)
return false;
}
}
break;
}
}
if (item->getQuality() >= item_quality::Masterful)
return false;
return true;
}
/*
* Stockpile Access
*/
class StockpileInfo {
public:
StockpileInfo() : id(0), sp(nullptr)
{
}
StockpileInfo(df::building_stockpilest *sp_) : sp(sp_)
{
readBuilding();
}
bool inStockpile(df::item *i)
{
df::item *container = Items::getContainer(i);
if (container)
return inStockpile(container);
if (i->pos.z != z) return false;
if (i->pos.x < x1 || i->pos.x >= x2 ||
i->pos.y < y1 || i->pos.y >= y2) return false;
int e = (i->pos.x - x1) + (i->pos.y - y1) * sp->room.width;
return sp->room.extents[e] == 1;
}
bool isValid()
{
if (!id)
return false;
auto found = df::building::find(id);
return found && found == sp && found->getType() == building_type::Stockpile;
}
int32_t getId()
{
return id;
}
bool matches(df::building_stockpilest* sp)
{
return this->sp == sp;
}
protected:
int32_t id;
df::building_stockpilest* sp;
void readBuilding()
{
if (!sp)
return;
id = sp->id;
z = sp->z;
x1 = sp->room.x;
x2 = sp->room.x + sp->room.width;
y1 = sp->room.y;
y2 = sp->room.y + sp->room.height;
}
private:
int x1, x2, y1, y2, z;
};
class PersistentStockpileInfo : public StockpileInfo {
public:
PersistentStockpileInfo(df::building_stockpilest *sp, string persistence_key) :
StockpileInfo(sp), persistence_key(persistence_key)
{
}
PersistentStockpileInfo(PersistentDataItem &config, string persistence_key) :
config(config), persistence_key(persistence_key)
{
id = config.ival(1);
}
bool load()
{
auto found = df::building::find(id);
if (!found || found->getType() != building_type::Stockpile)
return false;
sp = virtual_cast<df::building_stockpilest>(found);
if (!sp)
return false;
readBuilding();
return true;
}
void save()
{
config = DFHack::World::AddPersistentData(persistence_key);
config.ival(1) = id;
}
void remove()
{
DFHack::World::DeletePersistentData(config);
}
private:
PersistentDataItem config;
string persistence_key;
};
/*
* List classes
*/
@ -155,9 +425,10 @@ public:
T elem;
string text, keywords;
bool selected;
UIColor color;
ListEntry(const string text, const T elem, const string keywords = "") :
elem(elem), text(text), selected(false), keywords(keywords)
ListEntry(const string text, const T elem, const string keywords = "", const UIColor color = COLOR_UNSELECTED) :
elem(elem), text(text), selected(false), keywords(keywords), color(color)
{
}
};
@ -173,7 +444,6 @@ public:
bool multiselect;
bool allow_null;
bool auto_select;
bool force_sort;
bool allow_search;
bool feed_changed_highlight;
@ -188,7 +458,6 @@ public:
multiselect = false;
allow_null = true;
auto_select = false;
force_sort = false;
allow_search = true;
feed_changed_highlight = false;
}
@ -198,6 +467,8 @@ public:
list.clear();
display_list.clear();
display_start_offset = 0;
if (highlighted_index != -1)
highlighted_index = 0;
max_item_width = title.length();
resize();
}
@ -231,6 +502,11 @@ public:
it->text = pad_string(it->text, max_item_width, false);
}
return getMaxItemWidth();
}
int getMaxItemWidth()
{
return left_margin + max_item_width;
}
@ -245,7 +521,7 @@ public:
for (int i = display_start_offset; i < display_list.size() && i < last_index_able_to_display; i++)
{
++y;
UIColor fg_color = (display_list[i]->selected) ? COLOR_SELECTED : COLOR_UNSELECTED;
UIColor fg_color = (display_list[i]->selected) ? COLOR_SELECTED : display_list[i]->color;
UIColor bg_color = (is_selected_column && i == highlighted_index) ? COLOR_HIGHLIGHTED : COLOR_BLACK;
string item_label = display_list[i]->text;
@ -324,6 +600,15 @@ public:
}
}
void centerSelection()
{
if (display_list.size() == 0)
return;
display_start_offset = highlighted_index - (display_max_rows / 2);
validateDisplayOffset();
validateHighlight();
}
void validateHighlight()
{
set_to_limit(highlighted_index, display_list.size() - 1);
@ -347,10 +632,15 @@ public:
highlighted_index += highlight_change + offset_shift * display_max_rows;
display_start_offset += offset_shift * display_max_rows;
set_to_limit(display_start_offset, max(0, (int)(display_list.size())-display_max_rows));
validateDisplayOffset();
validateHighlight();
}
void validateDisplayOffset()
{
set_to_limit(display_start_offset, max(0, (int)(display_list.size())-display_max_rows));
}
void setHighlight(const int index)
{
if (!initHighlightChange())
@ -378,6 +668,9 @@ public:
void toggleHighlighted()
{
if (display_list.size() == 0)
return;
if (auto_select)
return;
@ -505,6 +798,7 @@ public:
// Standard character
search_string += last_token - ascii_to_enum_offset;
filterDisplay();
centerSelection();
}
else if (last_token == interface_key::STRING_A000)
{
@ -513,6 +807,7 @@ public:
{
search_string.erase(search_string.length()-1);
filterDisplay();
centerSelection();
}
}
else
@ -547,7 +842,7 @@ public:
return false;
}
void sort()
void sort(bool force_sort = false)
{
if (force_sort || list.size() < 100)
std::sort(list.begin(), list.end(), sort_fn);