Conflicts:
	plugins/CMakeLists.txt
	plugins/stonesense
develop
Petr Mrázek 2013-04-24 17:24:03 +02:00
commit 2e379c4d3f
15 changed files with 7845 additions and 102 deletions

@ -109,7 +109,8 @@ IF(UNIX)
SET(CMAKE_C_FLAGS "-fvisibility=hidden -m32 -march=i686 -mtune=generic")
ELSEIF(MSVC)
# for msvc, tell it to always use 8-byte pointers to member functions to avoid confusion
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg /vmm")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg /vmm /MP")
SET(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /Od")
ENDIF()
# use shared libraries for protobuf

@ -12,6 +12,13 @@ DFHack future
- autoSyndrome: disable by default
- ruby: add df.dfhack_run "somecommand"
- magmasource: rename to source, allow water/magma sources/drains
New plugins:
- buildingplan: Place furniture before it's built
- resume: A plugin to help display and resume suspended constructions conveniently
- dwarfmonitor: Records dwarf activity to measure fort efficiency
- mousequery: Look and poke at the map elements with the mouse.
- autotrade: Automatically send items in marked stockpiles to trade depot, when trading is possible.
- stocks: An improved stocks display screen.
DFHack v0.34.11-r3

@ -42,6 +42,13 @@ keybinding add Ctrl-Shift-B "adv-bodyswap force"
# Context-specific bindings #
#############################
# Stocks plugin
keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show"
# Workflow
keybinding add Ctrl-W@dwarfmode/QueryBuilding/Some "gui/workflow"
keybinding add Ctrl-I "gui/workflow status"
# q->stockpile; p - copy & paste stockpiles
keybinding add Alt-P copystock
@ -93,6 +100,9 @@ keybinding add Alt-A@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workshop-job
keybinding add Alt-W@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workflow
keybinding add Alt-W@overallstatus "gui/workflow status"
# autobutcher front-end
keybinding add Shift-B@pet/List/Unit "gui/autobutcher"
# assign weapon racks to squads so that they can be used
keybinding add P@dwarfmode/QueryBuilding/Some/Weaponrack gui/assign-rack

@ -123,7 +123,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(tweak tweak.cpp)
DFHACK_PLUGIN(feature feature.cpp)
DFHACK_PLUGIN(lair lair.cpp)
DFHACK_PLUGIN(zone zone.cpp)
DFHACK_PLUGIN(zone zone.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(catsplosion catsplosion.cpp)
DFHACK_PLUGIN(regrass regrass.cpp)
DFHACK_PLUGIN(forceequip forceequip.cpp)
@ -148,7 +148,13 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(trueTransformation trueTransformation.cpp)
DFHACK_PLUGIN(infiniteSky infiniteSky.cpp)
DFHACK_PLUGIN(createitem createitem.cpp)
DFHACK_PLUGIN(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote)
DFHACK_PLUGIN(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote)
DFHACK_PLUGIN(buildingplan buildingplan.cpp)
DFHACK_PLUGIN(resume resume.cpp)
DFHACK_PLUGIN(dwarfmonitor dwarfmonitor.cpp)
DFHACK_PLUGIN(mousequery mousequery.cpp)
DFHACK_PLUGIN(autotrade autotrade.cpp)
DFHACK_PLUGIN(stocks stocks.cpp)
endif()

File diff suppressed because it is too large Load Diff

@ -0,0 +1,619 @@
#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/building_stockpilest.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/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;
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;
}
};
/*
* 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 check_mandates(df::item *item)
{
for (auto it = world->mandates.begin(); it != world->mandates.end(); it++)
{
auto mandate = *it;
if (mandate->mode != 0)
continue;
if (item->getType() != mandate->item_type ||
(mandate->item_subtype != -1 && item->getSubtype() != mandate->item_subtype))
continue;
if (mandate->mat_type != -1 && item->getMaterial() != mandate->mat_type)
continue;
if (mandate->mat_index != -1 && item->getMaterialIndex() != mandate->mat_index)
continue;
return false;
}
return true;
}
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;
}
}
if (!check_mandates(item))
return false;
return true;
}
static void mark_all_in_stockpiles(vector<StockpileInfo> &stockpiles, bool announce)
{
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];
// 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 (size_t i = 0; i < items.size(); i++)
{
df::item *item = items[i];
if (item->flags.whole & bad_flags.whole)
continue;
if (!is_valid_item(item))
continue;
for (auto it = stockpiles.begin(); it != stockpiles.end(); it++)
{
if (!it->inStockpile(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 (auto cit = contained_items.begin(); cit != contained_items.end(); cit++)
{
if (!check_mandates(*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);
else if (announce)
Gui::showAnnouncement("No more items to mark", COLOR_RED, true);
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 = StockpileInfo(sp);
if (pile.isValid())
{
monitored_stockpiles.push_back(StockpileInfo(sp));
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, false);
}
void reset()
{
monitored_stockpiles.clear();
std::vector<PersistentDataItem> items;
DFHack::World::GetPersistentData(&items, "autotrade/stockpiles");
for (auto i = items.begin(); i != items.end(); i++)
{
auto pile = StockpileInfo(*i);
if (pile.load())
monitored_stockpiles.push_back(StockpileInfo(pile));
else
pile.remove();
}
}
private:
vector<StockpileInfo> monitored_stockpiles;
};
static StockpileMonitor monitor;
#define DELTA_TICKS 600
DFhackCExport command_result plugin_onupdate ( color_ostream &out )
{
if(!Maps::IsValid())
return CR_OK;
static decltype(world->frame_counter) last_frame_count = 0;
if (DFHack::World::ReadPauseState())
return CR_OK;
if (world->frame_counter - last_frame_count < DELTA_TICKS)
return CR_OK;
last_frame_count = world->frame_counter;
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)
{
building_stockpilest *sp = get_selected_stockpile();
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 (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 = 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);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, feed);
IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, render);
static command_result autotrade_cmd(color_ostream &out, vector <string> & parameters)
{
if (!parameters.empty())
{
if (parameters.size() == 1 && toLower(parameters[0])[0] == 'v')
{
out << "Building Plan" << endl << "Version: " << PLUGIN_VERSION << endl;
}
}
return CR_OK;
}
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;
}
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
if (!gps || !INTERPOSE_HOOK(trade_hook, feed).apply() || !INTERPOSE_HOOK(trade_hook, render).apply())
out.printerr("Could not insert autotrade hooks!\n");
commands.push_back(
PluginCommand(
"autotrade", "Automatically send items in marked stockpiles to trade depot, when trading is possible.",
autotrade_cmd, false, ""));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,12 @@
local _ENV = mkmodule('plugins.zone')
--[[
Native functions:
* autobutcher_isEnabled()
* autowatch_isEnabled()
--]]
return _ENV

@ -0,0 +1,263 @@
#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 "modules/Gui.h"
#include "modules/Screen.h"
using std::set;
using std::string;
using std::ostringstream;
using namespace DFHack;
using namespace df::enums;
using df::global::enabler;
using df::global::gps;
using df::global::world;
using df::global::ui;
static int32_t last_x, last_y, last_z;
static size_t max_list_size = 100000; // Avoid iterating over huge lists
struct mousequery_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
void send_key(const df::interface_key &key)
{
set<df::interface_key> tmp;
tmp.insert(key);
//INTERPOSE_NEXT(feed)(&tmp);
this->feed(&tmp);
}
df::interface_key get_default_query_mode(const int32_t &x, const int32_t &y, const int32_t &z)
{
bool fallback_to_building_query = false;
// Check for unit under cursor
size_t count = world->units.all.size();
if (count <= max_list_size)
{
for(size_t i = 0; i < count; i++)
{
df::unit *unit = world->units.all[i];
if(unit->pos.x == x && unit->pos.y == y && unit->pos.z == z)
return df::interface_key::D_VIEWUNIT;
}
}
else
{
fallback_to_building_query = true;
}
// Check for building under cursor
count = world->buildings.all.size();
if (count <= max_list_size)
{
for(size_t i = 0; i < count; i++)
{
df::building *bld = world->buildings.all[i];
if (z == bld->z &&
x >= bld->x1 && x <= bld->x2 &&
y >= bld->y1 && y <= bld->y2)
{
df::building_type type = bld->getType();
if (type == building_type::Stockpile)
{
fallback_to_building_query = true;
break; // Check for items in stockpile first
}
// 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;
}
}
}
else
{
fallback_to_building_query = true;
}
// Check for items under cursor
count = world->items.all.size();
if (count <= max_list_size)
{
for(size_t i = 0; i < count; i++)
{
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)
{
return df::interface_key::D_LOOK;
}
}
}
else
{
fallback_to_building_query = true;
}
return (fallback_to_building_query) ? df::interface_key::D_BUILDJOB : df::interface_key::D_LOOK;
}
bool handle_mouse(const set<df::interface_key> *input)
{
int32_t cx, cy, vz;
if (enabler->tracking_on)
{
if (enabler->mouse_lbut)
{
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;
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;
}
enabler->mouse_lbut = 0;
// 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
if (mx < 1 || mx > w || my < 1 || my > gps->dimy - 2)
return false;
while (ui->main.mode != Default)
{
send_key(df::interface_key::LEAVESCREEN);
}
if (key == interface_key::NONE)
key = get_default_query_mode(cx, cy, vz);
send_key(key);
// 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;
return true;
}
}
}
else if (enabler->mouse_rbut)
{
// 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)
{
while (ui->main.mode != Default)
{
enabler->mouse_rbut = 0;
send_key(df::interface_key::LEAVESCREEN);
}
}
}
}
return false;
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
if (!handle_mouse(input))
INTERPOSE_NEXT(feed)(input);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(mousequery_hook, feed);
DFHACK_PLUGIN("mousequery");
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
if (!gps || !INTERPOSE_HOOK(mousequery_hook, feed).apply())
out.printerr("Could not insert mousequery hooks!\n");
last_x = last_y = last_z = -1;
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
last_x = last_y = last_z = -1;
break;
default:
break;
}
return CR_OK;
}

@ -0,0 +1,315 @@
#include <string>
#include <vector>
#include <algorithm>
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <VTableInterpose.h>
// DF data structure definition headers
#include "DataDefs.h"
#include "MiscUtils.h"
#include "Types.h"
#include "df/viewscreen_dwarfmodest.h"
#include "df/world.h"
#include "df/building_constructionst.h"
#include "df/building.h"
#include "df/job.h"
#include "df/job_item.h"
#include "modules/Gui.h"
#include "modules/Screen.h"
#include "modules/Buildings.h"
#include "modules/Maps.h"
#include "modules/World.h"
using std::map;
using std::string;
using std::vector;
using namespace DFHack;
using namespace df::enums;
using df::global::gps;
using df::global::ui;
using df::global::world;
DFHACK_PLUGIN("resume");
#define PLUGIN_VERSION 0.2
#ifndef HAVE_NULLPTR
#define nullptr 0L
#endif
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}
template <class T, typename Fn>
static void for_each_(vector<T> &v, Fn func)
{
for_each(v.begin(), v.end(), func);
}
template <class T, class V, typename Fn>
static void transform_(vector<T> &src, vector<V> &dst, Fn func)
{
transform(src.begin(), src.end(), back_inserter(dst), func);
}
void OutputString(int8_t color, int &x, int &y, const std::string &text, bool newline = false, int left_margin = 0)
{
Screen::paintString(Screen::Pen(' ', color, 0), x, y, text);
if (newline)
{
++y;
x = left_margin;
}
else
x += text.length();
}
df::job *get_suspended_job(df::building *bld)
{
if (bld->getBuildStage() != 0)
return nullptr;
if (bld->jobs.size() == 0)
return nullptr;
auto job = bld->jobs[0];
if (job->flags.bits.suspend)
return job;
return nullptr;
}
struct SuspendedBuilding
{
df::building *bld;
df::coord pos;
bool was_resumed;
bool is_planned;
SuspendedBuilding(df::building *bld_) : bld(bld_), was_resumed(false), is_planned(false)
{
pos = df::coord(bld->centerx, bld->centery, bld->z);
}
bool isValid()
{
return bld && Buildings::findAtTile(pos) == bld && get_suspended_job(bld);
}
};
static bool enabled = false;
static bool buildings_scanned = false;
static vector<SuspendedBuilding> suspended_buildings, resumed_buildings;
void scan_for_suspended_buildings()
{
if (buildings_scanned)
return;
for (auto b = world->buildings.all.begin(); b != world->buildings.all.end(); b++)
{
auto bld = *b;
auto job = get_suspended_job(bld);
if (job)
{
SuspendedBuilding sb(bld);
sb.is_planned = job->job_items.size() == 1 && job->job_items[0]->item_type == item_type::NONE;
auto it = find_if(resumed_buildings.begin(), resumed_buildings.end(),
[&] (SuspendedBuilding &rsb) { return rsb.bld == bld; });
sb.was_resumed = it != resumed_buildings.end();
suspended_buildings.push_back(sb);
}
}
buildings_scanned = true;
}
void show_suspended_buildings()
{
int32_t vx, vy, vz;
if (!Gui::getViewCoords(vx, vy, vz))
return;
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = vx + dims.map_x2;
int bottom_margin = vy + dims.y2;
for (auto sb = suspended_buildings.begin(); sb != suspended_buildings.end();)
{
if (!sb->isValid())
{
sb = suspended_buildings.erase(sb);
continue;
}
if (sb->bld->z == vz && sb->bld->centerx >= vx && sb->bld->centerx <= left_margin &&
sb->bld->centery >= vy && sb->bld->centery <= bottom_margin)
{
int x = sb->bld->centerx - vx + 1;
int y = sb->bld->centery - vy + 1;
auto color = COLOR_YELLOW;
if (sb->is_planned)
color = COLOR_GREEN;
else if (sb->was_resumed)
color = COLOR_RED;
OutputString(color, x, y, "X");
}
sb++;
}
}
void clear_scanned()
{
buildings_scanned = false;
suspended_buildings.clear();
}
void resume_suspended_buildings(color_ostream &out)
{
out << "Resuming all buildings." << endl;
for (auto isb = resumed_buildings.begin(); isb != resumed_buildings.end();)
{
if (isb->isValid())
{
isb++;
continue;
}
isb = resumed_buildings.erase(isb);
}
scan_for_suspended_buildings();
for (auto sb = suspended_buildings.begin(); sb != suspended_buildings.end(); sb++)
{
if (sb->is_planned)
continue;
resumed_buildings.push_back(*sb);
sb->bld->jobs[0]->flags.bits.suspend = false;
}
clear_scanned();
out << resumed_buildings.size() << " buildings resumed" << endl;
}
//START Viewscreen Hook
struct resume_hook : public df::viewscreen_dwarfmodest
{
//START UI Methods
typedef df::viewscreen_dwarfmodest interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
if (enabled && DFHack::World::ReadPauseState() && ui->main.mode == ui_sidebar_mode::Default)
{
scan_for_suspended_buildings();
show_suspended_buildings();
}
else
{
clear_scanned();
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(resume_hook, render);
static command_result resume_cmd(color_ostream &out, vector <string> & parameters)
{
bool show_help = false;
if (parameters.empty())
{
show_help = true;
}
else
{
auto cmd = parameters[0][0];
if (cmd == 'v')
{
out << "Resume" << endl << "Version: " << PLUGIN_VERSION << endl;
}
else if (cmd == 's')
{
enabled = true;
out << "Overlay enabled" << endl;
}
else if (cmd == 'h')
{
enabled = false;
out << "Overlay disabled" << endl;
}
else if (cmd == 'a')
{
resume_suspended_buildings(out);
}
else
{
show_help = true;
}
}
if (show_help)
return CR_WRONG_USAGE;
return CR_OK;
}
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
if (!gps || !INTERPOSE_HOOK(resume_hook, render).apply())
out.printerr("Could not insert resume hooks!\n");
commands.push_back(
PluginCommand(
"resume", "A plugin to help display and resume suspended constructions conveniently",
resume_cmd, false,
"resume show\n"
" Show overlay when paused:\n"
" Yellow: Suspended construction\n"
" Red: Suspended after resume attempt, possibly stuck\n"
" Green: Planned building waiting for materials\n"
"resume hide\n"
" Hide overlay\n"
"resume all\n"
" Resume all suspended building constructions\n"
));
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
suspended_buildings.clear();
resumed_buildings.clear();
break;
default:
break;
}
return CR_OK;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,580 @@
#include <algorithm>
#include <map>
#include <string>
#include <set>
#include "Core.h"
#include "MiscUtils.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <VTableInterpose.h>
#include "modules/Screen.h"
#include "df/enabler.h"
using std::string;
using std::vector;
using std::map;
using std::ostringstream;
using std::set;
using namespace DFHack;
using namespace df::enums;
using df::global::enabler;
using df::global::gps;
#ifndef HAVE_NULLPTR
#define nullptr 0L
#endif
#define COLOR_TITLE COLOR_BLUE
#define COLOR_UNSELECTED COLOR_GREY
#define COLOR_SELECTED COLOR_WHITE
#define COLOR_HIGHLIGHTED COLOR_GREEN
template <class T, typename Fn>
static void for_each_(vector<T> &v, Fn func)
{
for_each(v.begin(), v.end(), func);
}
template <class T, class V, typename Fn>
static void for_each_(map<T, V> &v, Fn func)
{
for_each(v.begin(), v.end(), func);
}
template <class T, class V, typename Fn>
static void transform_(vector<T> &src, vector<V> &dst, Fn func)
{
transform(src.begin(), src.end(), back_inserter(dst), func);
}
typedef int8_t UIColor;
void OutputString(UIColor color, int &x, int &y, const std::string &text,
bool newline = false, int left_margin = 0, const UIColor bg_color = 0)
{
Screen::paintString(Screen::Pen(' ', color, bg_color), x, y, text);
if (newline)
{
++y;
x = left_margin;
}
else
x += text.length();
}
void OutputHotkeyString(int &x, int &y, const char *text, const char *hotkey, 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);
OutputString(text_color, x, y, display, 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)
{
OutputString(hotkey_color, x, y, hotkey);
OutputString(COLOR_WHITE, x, y, ": ");
OutputString((state) ? COLOR_WHITE : COLOR_GREY, x, y, text, newline, left_margin);
}
void OutputToggleString(int &x, int &y, const char *text, const char *hotkey, bool state, bool newline = true, int left_margin = 0, int8_t color = COLOR_WHITE)
{
OutputHotkeyString(x, y, text, hotkey);
OutputString(COLOR_WHITE, x, y, ": ");
if (state)
OutputString(COLOR_GREEN, x, y, "Enabled", newline, left_margin);
else
OutputString(COLOR_GREY, x, y, "Disabled", newline, left_margin);
}
const int ascii_to_enum_offset = interface_key::STRING_A048 - '0';
inline string int_to_string(const int n)
{
return static_cast<ostringstream*>( &(ostringstream() << n) )->str();
}
static void set_to_limit(int &value, const int maximum, const int min = 0)
{
if (value < min)
value = min;
else if (value > maximum)
value = maximum;
}
inline void paint_text(const UIColor color, const int &x, const int &y, const std::string &text, const UIColor background = 0)
{
Screen::paintString(Screen::Pen(' ', color, background), x, y, text);
}
static string pad_string(string text, const int size, const bool front = true, const bool trim = false)
{
if (text.length() > size)
{
if (trim && size > 10)
{
text = text.substr(0, size-3);
text.append("...");
}
return text;
}
string aligned(size - text.length(), ' ');
if (front)
{
aligned.append(text);
return aligned;
}
else
{
text.append(aligned);
return text;
}
}
/*
* List classes
*/
template <typename T>
class ListEntry
{
public:
T elem;
string text, keywords;
bool selected;
ListEntry(const string text, const T elem, const string keywords = "") :
elem(elem), text(text), selected(false), keywords(keywords)
{
}
};
template <typename T>
class ListColumn
{
public:
int highlighted_index;
int display_start_offset;
unsigned short text_clip_at;
int32_t bottom_margin, search_margin, left_margin;
bool multiselect;
bool allow_null;
bool auto_select;
bool force_sort;
bool allow_search;
bool feed_changed_highlight;
ListColumn()
{
bottom_margin = 3;
clear();
left_margin = 2;
search_margin = 63;
highlighted_index = 0;
text_clip_at = 0;
multiselect = false;
allow_null = true;
auto_select = false;
force_sort = false;
allow_search = true;
feed_changed_highlight = false;
}
void clear()
{
list.clear();
display_list.clear();
display_start_offset = 0;
max_item_width = title.length();
resize();
}
void resize()
{
display_max_rows = gps->dimy - 4 - bottom_margin;
}
void add(ListEntry<T> &entry)
{
list.push_back(entry);
if (entry.text.length() > max_item_width)
max_item_width = entry.text.length();
}
void add(const string &text, const T &elem)
{
list.push_back(ListEntry<T>(text, elem));
if (text.length() > max_item_width)
max_item_width = text.length();
}
int fixWidth()
{
if (text_clip_at > 0 && max_item_width > text_clip_at)
max_item_width = text_clip_at;
for (auto it = list.begin(); it != list.end(); it++)
{
it->text = pad_string(it->text, max_item_width, false);
}
return left_margin + max_item_width;
}
virtual void display_extras(const T &elem, int32_t &x, int32_t &y) const {}
void display(const bool is_selected_column) const
{
int32_t y = 2;
paint_text(COLOR_TITLE, left_margin, y, title);
int last_index_able_to_display = display_start_offset + display_max_rows;
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 bg_color = (is_selected_column && i == highlighted_index) ? COLOR_HIGHLIGHTED : COLOR_BLACK;
string item_label = display_list[i]->text;
if (text_clip_at > 0 && item_label.length() > text_clip_at)
item_label.resize(text_clip_at);
paint_text(fg_color, left_margin, y, item_label, bg_color);
int x = left_margin + display_list[i]->text.length() + 1;
display_extras(display_list[i]->elem, x, y);
}
if (is_selected_column && allow_search)
{
y = gps->dimy - 3;
int32_t x = search_margin;
OutputHotkeyString(x, y, "Search" ,"S");
OutputString(COLOR_WHITE, x, y, ": ");
OutputString(COLOR_WHITE, x, y, search_string);
OutputString(COLOR_LIGHTGREEN, x, y, "_");
}
}
void filterDisplay()
{
ListEntry<T> *prev_selected = (getDisplayListSize() > 0) ? display_list[highlighted_index] : NULL;
display_list.clear();
search_string = toLower(search_string);
vector<string> search_tokens;
if (!search_string.empty())
split_string(&search_tokens, search_string, " ");
for (size_t i = 0; i < list.size(); i++)
{
ListEntry<T> *entry = &list[i];
bool include_item = true;
if (!search_string.empty())
{
string item_string = toLower(list[i].text);
for (auto si = search_tokens.begin(); si != search_tokens.end(); si++)
{
if (!si->empty() && item_string.find(*si) == string::npos &&
list[i].keywords.find(*si) == string::npos)
{
include_item = false;
break;
}
}
}
if (include_item)
{
display_list.push_back(entry);
if (entry == prev_selected)
highlighted_index = display_list.size() - 1;
}
else if (auto_select)
{
entry->selected = false;
}
}
changeHighlight(0);
feed_changed_highlight = true;
}
void selectDefaultEntry()
{
for (size_t i = 0; i < display_list.size(); i++)
{
if (display_list[i]->selected)
{
highlighted_index = i;
break;
}
}
}
void validateHighlight()
{
set_to_limit(highlighted_index, display_list.size() - 1);
if (highlighted_index < display_start_offset)
display_start_offset = highlighted_index;
else if (highlighted_index >= display_start_offset + display_max_rows)
display_start_offset = highlighted_index - display_max_rows + 1;
if (auto_select || (!allow_null && list.size() == 1))
display_list[highlighted_index]->selected = true;
feed_changed_highlight = true;
}
void changeHighlight(const int highlight_change, const int offset_shift = 0)
{
if (!initHighlightChange())
return;
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));
validateHighlight();
}
void setHighlight(const int index)
{
if (!initHighlightChange())
return;
highlighted_index = index;
validateHighlight();
}
bool initHighlightChange()
{
if (display_list.size() == 0)
return false;
if (auto_select && !multiselect)
{
for (auto it = list.begin(); it != list.end(); it++)
{
it->selected = false;
}
}
return true;
}
void toggleHighlighted()
{
if (auto_select)
return;
ListEntry<T> *entry = display_list[highlighted_index];
if (!multiselect || !allow_null)
{
int selected_count = 0;
for (size_t i = 0; i < list.size(); i++)
{
if (!multiselect && !entry->selected)
list[i].selected = false;
if (!allow_null && list[i].selected)
selected_count++;
}
if (!allow_null && entry->selected && selected_count == 1)
return;
}
entry->selected = !entry->selected;
}
vector<T> getSelectedElems(bool only_one = false)
{
vector<T> results;
for (auto it = list.begin(); it != list.end(); it++)
{
if ((*it).selected)
{
results.push_back(it->elem);
if (only_one)
break;
}
}
return results;
}
T getFirstSelectedElem()
{
vector<T> results = getSelectedElems(true);
if (results.size() == 0)
return nullptr;
else
return results[0];
}
void clearSelection()
{
for_each_(list, [] (ListEntry<T> &e) { e.selected = false; });
}
void selectItem(const T elem)
{
int i = 0;
for (; i < display_list.size(); i++)
{
if (display_list[i]->elem == elem)
{
setHighlight(i);
break;
}
}
}
void clearSearch()
{
search_string.clear();
filterDisplay();
}
size_t getDisplayListSize()
{
return display_list.size();
}
vector<ListEntry<T>*> &getDisplayList()
{
return display_list;
}
size_t getBaseListSize()
{
return list.size();
}
bool feed(set<df::interface_key> *input)
{
feed_changed_highlight = false;
if (input->count(interface_key::CURSOR_UP))
{
changeHighlight(-1);
}
else if (input->count(interface_key::CURSOR_DOWN))
{
changeHighlight(1);
}
else if (input->count(interface_key::STANDARDSCROLL_PAGEUP))
{
changeHighlight(0, -1);
}
else if (input->count(interface_key::STANDARDSCROLL_PAGEDOWN))
{
changeHighlight(0, 1);
}
else if (input->count(interface_key::SELECT) && !auto_select)
{
toggleHighlighted();
}
else if (input->count(interface_key::CUSTOM_SHIFT_S))
{
clearSearch();
}
else if (enabler->tracking_on && gps->mouse_x != -1 && gps->mouse_y != -1 && enabler->mouse_lbut)
{
return setHighlightByMouse();
}
else if (allow_search)
{
// Search query typing mode always on
df::interface_key last_token = *input->rbegin();
if ((last_token >= interface_key::STRING_A096 && last_token <= interface_key::STRING_A123) ||
last_token == interface_key::STRING_A032)
{
// Standard character
search_string += last_token - ascii_to_enum_offset;
filterDisplay();
}
else if (last_token == interface_key::STRING_A000)
{
// Backspace
if (search_string.length() > 0)
{
search_string.erase(search_string.length()-1);
filterDisplay();
}
}
else
{
return false;
}
return true;
}
else
{
return false;
}
return true;
}
bool setHighlightByMouse()
{
if (gps->mouse_y >= 3 && gps->mouse_y < display_max_rows + 3 &&
gps->mouse_x >= left_margin && gps->mouse_x < left_margin + max_item_width)
{
int new_index = display_start_offset + gps->mouse_y - 3;
if (new_index < display_list.size())
setHighlight(new_index);
enabler->mouse_lbut = enabler->mouse_rbut = 0;
return true;
}
return false;
}
void sort()
{
if (force_sort || list.size() < 100)
std::sort(list.begin(), list.end(),
[] (ListEntry<T> const& a, ListEntry<T> const& b) { return a.text.compare(b.text) < 0; });
filterDisplay();
}
void setTitle(const string t)
{
title = t;
if (title.length() > max_item_width)
max_item_width = title.length();
}
size_t getDisplayedListSize()
{
return display_list.size();
}
private:
vector<ListEntry<T>> list;
vector<ListEntry<T>*> display_list;
string search_string;
string title;
int display_max_rows;
int max_item_width;
};

File diff suppressed because it is too large Load Diff

@ -0,0 +1,656 @@
-- A GUI front-end for the autobutcher plugin.
local gui = require 'gui'
local utils = require 'utils'
local widgets = require 'gui.widgets'
local dlg = require 'gui.dialogs'
local plugin = require 'plugins.zone'
WatchList = defclass(WatchList, gui.FramedScreen)
WatchList.ATTRS {
frame_title = 'Autobutcher Watchlist',
frame_inset = 0, -- cover full DF window
frame_background = COLOR_BLACK,
frame_style = gui.BOUNDARY_FRAME,
}
-- width of the race name column in the UI
local racewidth = 25
function nextAutowatchState()
if(plugin.autowatch_isEnabled()) then
return 'Stop '
end
return 'Start'
end
function nextAutobutcherState()
if(plugin.autobutcher_isEnabled()) then
return 'Stop '
end
return 'Start'
end
function getSleepTimer()
return plugin.autobutcher_getSleep()
end
function setSleepTimer(ticks)
plugin.autobutcher_setSleep(ticks)
end
function WatchList:init(args)
local colwidth = 7
self:addviews{
widgets.Panel{
frame = { l = 0, r = 0 },
frame_inset = 1,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
text_pen = COLOR_CYAN,
text = {
{ text = 'Race', width = racewidth }, ' ',
{ text = 'female', width = colwidth }, ' ',
{ text = ' male', width = colwidth }, ' ',
{ text = 'Female', width = colwidth }, ' ',
{ text = ' Male', width = colwidth }, ' ',
{ text = 'watch? ' },
{ text = ' butchering' },
NEWLINE,
{ text = '', width = racewidth }, ' ',
{ text = ' kids', width = colwidth }, ' ',
{ text = ' kids', width = colwidth }, ' ',
{ text = 'adults', width = colwidth }, ' ',
{ text = 'adults', width = colwidth }, ' ',
{ text = ' ' },
{ text = ' ordered' },
}
},
widgets.List{
view_id = 'list',
frame = { t = 3, b = 5 },
not_found_label = 'Watchlist is empty.',
edit_pen = COLOR_LIGHTCYAN,
text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK },
cursor_pen = { fg = COLOR_WHITE, bg = COLOR_GREEN },
--on_select = self:callback('onSelectEntry'),
},
widgets.Label{
view_id = 'bottom_ui',
frame = { b = 0, h = 1 },
text = 'filled by updateBottom()'
}
}
},
}
self:initListChoices()
self:updateBottom()
end
-- change the viewmode for stock data displayed in left section of columns
local viewmodes = { 'total stock', 'protected stock', 'butcherable', 'butchering ordered' }
local viewmode = 1
function WatchList:onToggleView()
if viewmode < #viewmodes then
viewmode = viewmode + 1
else
viewmode = 1
end
self:initListChoices()
self:updateBottom()
end
-- update the bottom part of the UI (after sleep timer changed etc)
function WatchList:updateBottom()
self.subviews.bottom_ui:setText(
{
{ key = 'CUSTOM_SHIFT_V', text = ': View in colums shows: '..viewmodes[viewmode]..' / target max',
on_activate = self:callback('onToggleView') }, NEWLINE,
{ key = 'CUSTOM_F', text = ': f kids',
on_activate = self:callback('onEditFK') }, ', ',
{ key = 'CUSTOM_M', text = ': m kids',
on_activate = self:callback('onEditMK') }, ', ',
{ key = 'CUSTOM_SHIFT_F', text = ': f adults',
on_activate = self:callback('onEditFA') }, ', ',
{ key = 'CUSTOM_SHIFT_M', text = ': m adults',
on_activate = self:callback('onEditMA') }, '. ',
{ key = 'CUSTOM_W', text = ': Toggle watch',
on_activate = self:callback('onToggleWatching') }, '. ',
{ key = 'CUSTOM_X', text = ': Delete',
on_activate = self:callback('onDeleteEntry') }, '. ', NEWLINE,
--{ key = 'CUSTOM_A', text = ': Add race',
-- on_activate = self:callback('onAddRace') }, ', ',
{ key = 'CUSTOM_SHIFT_R', text = ': Set whole row',
on_activate = self:callback('onSetRow') }, '. ',
{ key = 'CUSTOM_B', text = ': Remove butcher orders',
on_activate = self:callback('onUnbutcherRace') }, '. ',
{ key = 'CUSTOM_SHIFT_B', text = ': Butcher race',
on_activate = self:callback('onButcherRace') }, '. ', NEWLINE,
{ key = 'CUSTOM_SHIFT_A', text = ': '..nextAutobutcherState()..' Autobutcher',
on_activate = self:callback('onToggleAutobutcher') }, '. ',
{ key = 'CUSTOM_SHIFT_W', text = ': '..nextAutowatchState()..' Autowatch',
on_activate = self:callback('onToggleAutowatch') }, '. ',
{ key = 'CUSTOM_SHIFT_S', text = ': Sleep ('..getSleepTimer()..' ticks)',
on_activate = self:callback('onEditSleepTimer') }, '. ',
})
end
function stringify(number)
-- cap displayed number to 3 digits
-- after population of 50 per race is reached pets stop breeding anyways
-- so probably this could safely be reduced to 99
local max = 999
if number > max then number = max end
return tostring(number)
end
function WatchList:initListChoices()
local choices = {}
-- first two rows are for "edit all races" and "edit new races"
local settings = plugin.autobutcher_getSettings()
local fk = stringify(settings.fk)
local fa = stringify(settings.fa)
local mk = stringify(settings.mk)
local ma = stringify(settings.ma)
local watched = ''
local colwidth = 7
table.insert (choices, {
text = {
{ text = '!! ALL RACES PLUS NEW', width = racewidth, pad_char = ' ' }, --' ',
{ text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ',
{ text = fk, width = 3, rjustify = false, pad_char = ' ' }, ' ',
{ text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ',
{ text = mk, width = 3, rjustify = false, pad_char = ' ' }, ' ',
{ text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ',
{ text = fa, width = 3, rjustify = false, pad_char = ' ' }, ' ',
{ text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ',
{ text = ma, width = 3, rjustify = false, pad_char = ' ' }, ' ',
{ text = watched, width = 6, rjustify = true }
}
})
table.insert (choices, {
text = {
{ text = '!! ONLY NEW RACES', width = racewidth, pad_char = ' ' }, --' ',
{ text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ',
{ text = fk, width = 3, rjustify = false, pad_char = ' ' }, ' ',
{ text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ',
{ text = mk, width = 3, rjustify = false, pad_char = ' ' }, ' ',
{ text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ',
{ text = fa, width = 3, rjustify = false, pad_char = ' ' }, ' ',
{ text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ',
{ text = ma, width = 3, rjustify = false, pad_char = ' ' }, ' ',
{ text = watched, width = 6, rjustify = true }
}
})
local watchlist = plugin.autobutcher_getWatchList()
for i,entry in ipairs(watchlist) do
fk = stringify(entry.fk)
fa = stringify(entry.fa)
mk = stringify(entry.mk)
ma = stringify(entry.ma)
if viewmode == 1 then
fkc = stringify(entry.fk_total)
fac = stringify(entry.fa_total)
mkc = stringify(entry.mk_total)
mac = stringify(entry.ma_total)
end
if viewmode == 2 then
fkc = stringify(entry.fk_protected)
fac = stringify(entry.fa_protected)
mkc = stringify(entry.mk_protected)
mac = stringify(entry.ma_protected)
end
if viewmode == 3 then
fkc = stringify(entry.fk_butcherable)
fac = stringify(entry.fa_butcherable)
mkc = stringify(entry.mk_butcherable)
mac = stringify(entry.ma_butcherable)
end
if viewmode == 4 then
fkc = stringify(entry.fk_butcherflag)
fac = stringify(entry.fa_butcherflag)
mkc = stringify(entry.mk_butcherflag)
mac = stringify(entry.ma_butcherflag)
end
local butcher_ordered = entry.fk_butcherflag + entry.fa_butcherflag + entry.mk_butcherflag + entry.ma_butcherflag
local bo = ' '
if butcher_ordered > 0 then bo = stringify(butcher_ordered) end
local watched = 'no'
if entry.watched then watched = 'yes' end
local racestr = entry.name
-- highlight entries where the target quota can't be met because too many are protected
bad_pen = COLOR_LIGHTRED
good_pen = NONE -- this is stupid, but it works. sue me
fk_pen = good_pen
fa_pen = good_pen
mk_pen = good_pen
ma_pen = good_pen
if entry.fk_protected > entry.fk then fk_pen = bad_pen end
if entry.fa_protected > entry.fa then fa_pen = bad_pen end
if entry.mk_protected > entry.mk then mk_pen = bad_pen end
if entry.ma_protected > entry.ma then ma_pen = bad_pen end
table.insert (choices, {
text = {
{ text = racestr, width = racewidth, pad_char = ' ' }, --' ',
{ text = fkc, width = 3, rjustify = true, pad_char = ' ' }, '/',
{ text = fk, width = 3, rjustify = false, pad_char = ' ', pen = fk_pen }, ' ',
{ text = mkc, width = 3, rjustify = true, pad_char = ' ' }, '/',
{ text = mk, width = 3, rjustify = false, pad_char = ' ', pen = mk_pen }, ' ',
{ text = fac, width = 3, rjustify = true, pad_char = ' ' }, '/',
{ text = fa, width = 3, rjustify = false, pad_char = ' ', pen = fa_pen }, ' ',
{ text = mac, width = 3, rjustify = true, pad_char = ' ' }, '/',
{ text = ma, width = 3, rjustify = false, pad_char = ' ', pen = ma_pen }, ' ',
{ text = watched, width = 6, rjustify = true, pad_char = ' ' }, ' ',
{ text = bo, width = 8, rjustify = true, pad_char = ' ' }
},
obj = entry,
})
end
local list = self.subviews.list
list:setChoices(choices)
end
function WatchList:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
else
WatchList.super.onInput(self, keys)
end
end
-- check the user input for target population values
function WatchList:checkUserInput(count, text)
if count == nil then
dlg.showMessage('Invalid Number', 'This is not a number: '..text..NEWLINE..'(for zero enter a 0)', COLOR_LIGHTRED)
return false
end
if count < 0 then
dlg.showMessage('Invalid Number', 'Negative numbers make no sense!', COLOR_LIGHTRED)
return false
end
return true
end
-- check the user input for sleep timer
function WatchList:checkUserInputSleep(count, text)
if count == nil then
dlg.showMessage('Invalid Number', 'This is not a number: '..text..NEWLINE..'(for zero enter a 0)', COLOR_LIGHTRED)
return false
end
if count < 1000 then
dlg.showMessage('Invalid Number',
'Minimum allowed timer value is 1000!'..NEWLINE..'Too low values could decrease performance'..NEWLINE..'and are not necessary!',
COLOR_LIGHTRED)
return false
end
return true
end
function WatchList:onEditFK()
local selidx,selobj = self.subviews.list:getSelected()
local settings = plugin.autobutcher_getSettings()
local fk = settings.fk
local mk = settings.mk
local fa = settings.fa
local ma = settings.ma
local race = 'ALL RACES PLUS NEW'
local id = -1
local watched = false
if selidx == 2 then
race = 'ONLY NEW RACES'
end
if selidx > 2 then
local entry = selobj.obj
fk = entry.fk
mk = entry.mk
fa = entry.fa
ma = entry.ma
race = entry.name
id = entry.id
watched = entry.watched
end
dlg.showInputPrompt(
'Race: '..race,
'Enter desired maximum of female kids:',
COLOR_WHITE,
' '..fk,
function(text)
local count = tonumber(text)
if self:checkUserInput(count, text) then
fk = count
if selidx == 1 then
plugin.autobutcher_setDefaultTargetAll( fk, mk, fa, ma )
end
if selidx == 2 then
plugin.autobutcher_setDefaultTargetNew( fk, mk, fa, ma )
end
if selidx > 2 then
plugin.autobutcher_setWatchListRace(id, fk, mk, fa, ma, watched)
end
self:initListChoices()
end
end
)
end
function WatchList:onEditMK()
local selidx,selobj = self.subviews.list:getSelected()
local settings = plugin.autobutcher_getSettings()
local fk = settings.fk
local mk = settings.mk
local fa = settings.fa
local ma = settings.ma
local race = 'ALL RACES PLUS NEW'
local id = -1
local watched = false
if selidx == 2 then
race = 'ONLY NEW RACES'
end
if selidx > 2 then
local entry = selobj.obj
fk = entry.fk
mk = entry.mk
fa = entry.fa
ma = entry.ma
race = entry.name
id = entry.id
watched = entry.watched
end
dlg.showInputPrompt(
'Race: '..race,
'Enter desired maximum of male kids:',
COLOR_WHITE,
' '..mk,
function(text)
local count = tonumber(text)
if self:checkUserInput(count, text) then
mk = count
if selidx == 1 then
plugin.autobutcher_setDefaultTargetAll( fk, mk, fa, ma )
end
if selidx == 2 then
plugin.autobutcher_setDefaultTargetNew( fk, mk, fa, ma )
end
if selidx > 2 then
plugin.autobutcher_setWatchListRace(id, fk, mk, fa, ma, watched)
end
self:initListChoices()
end
end
)
end
function WatchList:onEditFA()
local selidx,selobj = self.subviews.list:getSelected()
local settings = plugin.autobutcher_getSettings()
local fk = settings.fk
local mk = settings.mk
local fa = settings.fa
local ma = settings.ma
local race = 'ALL RACES PLUS NEW'
local id = -1
local watched = false
if selidx == 2 then
race = 'ONLY NEW RACES'
end
if selidx > 2 then
local entry = selobj.obj
fk = entry.fk
mk = entry.mk
fa = entry.fa
ma = entry.ma
race = entry.name
id = entry.id
watched = entry.watched
end
dlg.showInputPrompt(
'Race: '..race,
'Enter desired maximum of female adults:',
COLOR_WHITE,
' '..fa,
function(text)
local count = tonumber(text)
if self:checkUserInput(count, text) then
fa = count
if selidx == 1 then
plugin.autobutcher_setDefaultTargetAll( fk, mk, fa, ma )
end
if selidx == 2 then
plugin.autobutcher_setDefaultTargetNew( fk, mk, fa, ma )
end
if selidx > 2 then
plugin.autobutcher_setWatchListRace(id, fk, mk, fa, ma, watched)
end
self:initListChoices()
end
end
)
end
function WatchList:onEditMA()
local selidx,selobj = self.subviews.list:getSelected()
local settings = plugin.autobutcher_getSettings()
local fk = settings.fk
local mk = settings.mk
local fa = settings.fa
local ma = settings.ma
local race = 'ALL RACES PLUS NEW'
local id = -1
local watched = false
if selidx == 2 then
race = 'ONLY NEW RACES'
end
if selidx > 2 then
local entry = selobj.obj
fk = entry.fk
mk = entry.mk
fa = entry.fa
ma = entry.ma
race = entry.name
id = entry.id
watched = entry.watched
end
dlg.showInputPrompt(
'Race: '..race,
'Enter desired maximum of male adults:',
COLOR_WHITE,
' '..ma,
function(text)
local count = tonumber(text)
if self:checkUserInput(count, text) then
ma = count
if selidx == 1 then
plugin.autobutcher_setDefaultTargetAll( fk, mk, fa, ma )
end
if selidx == 2 then
plugin.autobutcher_setDefaultTargetNew( fk, mk, fa, ma )
end
if selidx > 2 then
plugin.autobutcher_setWatchListRace(id, fk, mk, fa, ma, watched)
end
self:initListChoices()
end
end
)
end
function WatchList:onEditSleepTimer()
local sleep = getSleepTimer()
dlg.showInputPrompt(
'Edit Sleep Timer',
'Enter new sleep timer in ticks:'..NEWLINE..'(1 ingame day equals 1200 ticks)',
COLOR_WHITE,
' '..sleep,
function(text)
local count = tonumber(text)
if self:checkUserInputSleep(count, text) then
sleep = count
setSleepTimer(sleep)
self:updateBottom()
end
end
)
end
function WatchList:onToggleWatching()
local selidx,selobj = self.subviews.list:getSelected()
if selidx > 2 then
local entry = selobj.obj
plugin.autobutcher_setWatchListRace(entry.id, entry.fk, entry.mk, entry.fa, entry.ma, not entry.watched)
end
self:initListChoices()
end
function WatchList:onDeleteEntry()
local selidx,selobj = self.subviews.list:getSelected()
if(selidx < 3 or selobj == nil) then
return
end
dlg.showYesNoPrompt(
'Delete from Watchlist',
'Really delete the selected entry?'..NEWLINE..'(you could just toggle watch instead)',
COLOR_YELLOW,
function()
plugin.autobutcher_removeFromWatchList(selobj.obj.id)
self:initListChoices()
end
)
end
function WatchList:onAddRace()
print('onAddRace - not implemented yet')
end
function WatchList:onUnbutcherRace()
local selidx,selobj = self.subviews.list:getSelected()
if selidx < 3 then dlg.showMessage('Error', 'Select a specific race.', COLOR_LIGHTRED) end
if selidx > 2 then
local entry = selobj.obj
local race = entry.name
plugin.autobutcher_unbutcherRace(entry.id)
self:initListChoices()
self:updateBottom()
end
end
function WatchList:onButcherRace()
local selidx,selobj = self.subviews.list:getSelected()
if selidx < 3 then dlg.showMessage('Error', 'Select a specific race.', COLOR_LIGHTRED) end
if selidx > 2 then
local entry = selobj.obj
local race = entry.name
plugin.autobutcher_butcherRace(entry.id)
self:initListChoices()
self:updateBottom()
end
end
-- set whole row (fk, mk, fa, ma) to one value
function WatchList:onSetRow()
local selidx,selobj = self.subviews.list:getSelected()
local race = 'ALL RACES PLUS NEW'
local id = -1
local watched = false
if selidx == 2 then
race = 'ONLY NEW RACES'
end
local watchindex = selidx - 3
if selidx > 2 then
local entry = selobj.obj
race = entry.name
id = entry.id
watched = entry.watched
end
dlg.showInputPrompt(
'Set whole row for '..race,
'Enter desired maximum for all subtypes:',
COLOR_WHITE,
' ',
function(text)
local count = tonumber(text)
if self:checkUserInput(count, text) then
if selidx == 1 then
plugin.autobutcher_setDefaultTargetAll( count, count, count, count )
end
if selidx == 2 then
plugin.autobutcher_setDefaultTargetNew( count, count, count, count )
end
if selidx > 2 then
plugin.autobutcher_setWatchListRace(id, count, count, count, count, watched)
end
self:initListChoices()
end
end
)
end
function WatchList:onToggleAutobutcher()
if(plugin.autobutcher_isEnabled()) then
plugin.autobutcher_setEnabled(false)
plugin.autobutcher_sortWatchList()
else
plugin.autobutcher_setEnabled(true)
end
self:initListChoices()
self:updateBottom()
end
function WatchList:onToggleAutowatch()
if(plugin.autowatch_isEnabled()) then
plugin.autowatch_setEnabled(false)
else
plugin.autowatch_setEnabled(true)
end
self:initListChoices()
self:updateBottom()
end
if not dfhack.isMapLoaded() then
qerror('Map is not loaded.')
end
if string.match(dfhack.gui.getCurFocus(), '^dfhack/lua') then
qerror("This script must not be called while other lua gui stuff is running.")
end
-- maybe this is too strict, there is not really a reason why it can only be called from the status screen
-- (other than the hotkey might overlap with other scripts)
if (not string.match(dfhack.gui.getCurFocus(), '^overallstatus') and not string.match(dfhack.gui.getCurFocus(), '^pet/List/Unit')) then
qerror("This script must either be called from the overall status screen or the animal list screen.")
end
local screen = WatchList{ }
screen:show()