Conflicts: plugins/CMakeLists.txt plugins/stonesensedevelop
commit
2e379c4d3f
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()
|
Loading…
Reference in New Issue