dfhack/plugins/search.cpp

804 lines
21 KiB
C++

#include <modules/Screen.h>
#include <modules/Translation.h>
#include <modules/Units.h>
#include <MiscUtils.h>
#include <VTableInterpose.h>
//#include "df/viewscreen_petst.h"
#include "df/viewscreen_storesst.h"
#include "df/viewscreen_layer_stockpilest.h"
#include "df/viewscreen_tradegoodsst.h"
#include "df/viewscreen_unitlistst.h"
#include "df/interface_key.h"
#include "df/interfacest.h"
#include "df/layer_object_listst.h"
using std::set;
using std::vector;
using std::string;
using namespace DFHack;
using namespace df::enums;
using df::global::gps;
using df::global::gview;
/*
Search Plugin
A plugin that adds a "Search" hotkey to some screens (Units, Trade and Stocks)
that allows filtering of the list items by a typed query.
Works by manipulating the vector(s) that the list based viewscreens use to store
their items. When a search is started the plugin saves the original vectors and
with each keystroke creates a new filtered vector off the saves for the screen
to use.
*/
void OutputString(int8_t color, int &x, int y, const std::string &text)
{
Screen::paintString(Screen::Pen(' ', color, 0), x, y, text);
x += text.length();
}
static bool is_live_screen(const df::viewscreen *screen)
{
for (df::viewscreen *cur = &gview->view; cur; cur = cur->child)
if (cur == screen)
return true;
return false;
}
//
// START: Base Search functionality
//
// Parent class that does most of the work
template <class S, class T, class V = void*>
class search_parent
{
public:
// Called each time you enter or leave a searchable screen. Resets everything.
void reset_all()
{
reset_search();
valid = false;
sort_list1 = NULL;
sort_list2 = NULL;
viewscreen = NULL;
select_key = 's';
track_secondary_values = false;
}
bool reset_on_change()
{
if (valid && is_live_screen(viewscreen))
return false;
reset_all();
return true;
}
bool is_valid()
{
return valid;
}
// A new keystroke is received in a searchable screen
virtual bool process_input(set<df::interface_key> *input)
{
// If the page has two search options (Trade screen), only allow one to operate
// at a time
if (lock != NULL && lock != this)
return false;
// Allows custom preprocessing for each screen
if (!should_check_input(input))
return false;
bool key_processed = true;
if (entry_mode)
{
// Query typing mode
df::interface_key last_token = *input->rbegin();
if (last_token >= interface_key::STRING_A032 && last_token <= interface_key::STRING_A126)
{
// Standard character
search_string += last_token - ascii_to_enum_offset;
do_search();
}
else if (last_token == interface_key::STRING_A000)
{
// Backspace
if (search_string.length() > 0)
{
search_string.erase(search_string.length()-1);
do_search();
}
}
else if (input->count(interface_key::SELECT) || input->count(interface_key::LEAVESCREEN))
{
// ENTER or ESC: leave typing mode
end_entry_mode();
}
else if (input->count(interface_key::CURSOR_UP) || input->count(interface_key::CURSOR_DOWN)
|| input->count(interface_key::CURSOR_LEFT) || input->count(interface_key::CURSOR_RIGHT))
{
// Arrow key pressed. Leave entry mode and allow screen to process key
end_entry_mode();
key_processed = false;
}
}
// Not in query typing mode
else if (input->count(select_token))
{
// Hotkey pressed, enter typing mode
start_entry_mode();
}
else if (input->count((df::interface_key) (select_token + shift_offset)))
{
// Shift + Hotkey pressed, clear query
clear_search();
}
else
{
// Not a key for us, pass it on to the screen
key_processed = false;
}
return key_processed || entry_mode; // Only pass unrecognized keys down if not in typing mode
}
// Called if the search should be redone after the screen processes the keystroke.
// Used by the stocks screen where changing categories should redo the search on
// the new category.
virtual void do_post_update_check()
{
if (redo_search)
{
do_search();
redo_search = false;
}
}
static search_parent<S,T,V> *lock;
protected:
const S *viewscreen;
vector <T> saved_list1, reference_list;
vector <V> saved_list2;
vector <V> *sort_list2;
vector <int> saved_indexes;
bool redo_search;
bool track_secondary_values;
string search_string;
search_parent() : ascii_to_enum_offset(interface_key::STRING_A048 - '0'), shift_offset('A' - 'a')
{
reset_all();
}
virtual void init(int *cursor_pos, vector <T> *sort_list1, vector <V> *sort_list2 = NULL, char select_key = 's')
{
this->cursor_pos = cursor_pos;
this->sort_list1 = sort_list1;
this->sort_list2 = sort_list2;
this->select_key = select_key;
select_token = (df::interface_key) (ascii_to_enum_offset + select_key);
track_secondary_values = false;
valid = true;
}
bool is_entry_mode()
{
return entry_mode;
}
void start_entry_mode()
{
entry_mode = true;
lock = this;
}
void end_entry_mode()
{
entry_mode = false;
lock = NULL;
}
void reset_search()
{
end_entry_mode();
search_string = "";
saved_list1.clear();
saved_list2.clear();
reference_list.clear();
saved_indexes.clear();
}
// If the second vector is editable (i.e. Trade screen vector used for marking). then it may
// have been edited while the list was filtered. We have to update the original unfiltered
// list with these values. Uses a stored reference vector to determine if the list has been
// reordered after filtering, in which case indexes must be remapped.
void update_secondary_values()
{
if (sort_list2 != NULL && track_secondary_values)
{
bool list_has_been_sorted = (sort_list1->size() == reference_list.size()
&& *sort_list1 != reference_list);
for (size_t i = 0; i < saved_indexes.size(); i++)
{
int adjusted_item_index = i;
if (list_has_been_sorted)
{
for (size_t j = 0; j < sort_list1->size(); j++)
{
if ((*sort_list1)[j] == reference_list[i])
{
adjusted_item_index = j;
break;
}
}
}
update_saved_secondary_list_item(saved_indexes[i], adjusted_item_index);
}
saved_indexes.clear();
}
}
virtual void update_saved_secondary_list_item(size_t i, size_t j)
{
saved_list2[i] = (*sort_list2)[j];
}
// Store a copy of filtered list, used later to work out if filtered list has been sorted after filtering
void store_reference_values()
{
if (track_secondary_values)
reference_list = *sort_list1;
}
// Shortcut to clear the search immediately
virtual void clear_search()
{
if (saved_list1.size() > 0)
{
*sort_list1 = saved_list1;
if (sort_list2 != NULL)
{
update_secondary_values();
*sort_list2 = saved_list2;
}
saved_list1.clear();
saved_list2.clear();
}
store_reference_values();
search_string = "";
}
// The actual sort
virtual void do_search()
{
if (search_string.length() == 0)
{
clear_search();
return;
}
if (saved_list1.size() == 0)
{
// On first run, save the original list
saved_list1 = *sort_list1;
if (sort_list2 != NULL)
saved_list2 = *sort_list2;
}
else
update_secondary_values(); // Update original list with any modified values
// Clear viewscreen vectors
sort_list1->clear();
if (sort_list2 != NULL)
{
sort_list2->clear();
saved_indexes.clear();
}
string search_string_l = toLower(search_string);
for (size_t i = 0; i < saved_list1.size(); i++ )
{
T element = saved_list1[i];
string desc = toLower(get_element_description(element));
if (desc.find(search_string_l) != string::npos)
{
sort_list1->push_back(element);
if (sort_list2 != NULL)
{
sort_list2->push_back(saved_list2[i]);
if (track_secondary_values)
saved_indexes.push_back(i); // Used to map filtered indexes back to original, if needed
}
}
}
store_reference_values(); //Keep a copy, in case user sorts new list
if (cursor_pos)
*cursor_pos = 0;
}
virtual bool should_check_input(set<df::interface_key> *input)
{
return true;
}
// Display hotkey message
void print_search_option(int x, int y = -1) const
{
auto dim = Screen::getWindowSize();
if (y == -1)
y = dim.y - 2;
OutputString((entry_mode) ? 4 : 12, x, y, string(1, select_key));
OutputString((entry_mode) ? 10 : 15, x, y, ": Search");
if (search_string.length() > 0 || entry_mode)
OutputString(15, x, y, ": " + search_string);
if (entry_mode)
OutputString(10, x, y, "_");
}
virtual string get_element_description(T element) const = 0;
virtual void render () const = 0;
private:
vector <T> *sort_list1;
int *cursor_pos;
char select_key;
bool valid;
bool entry_mode;
df::interface_key select_token;
const int ascii_to_enum_offset;
const int shift_offset;
};
template <class S, class T, class V> search_parent<S,T,V> *search_parent<S,T,V> ::lock = NULL;
// Parent struct for the hooks, use optional param D to generate multiple classes with same T & V
// but different static modules
template <class T, class V, typename D = void>
struct search_hook : T
{
typedef T interpose_base;
static V module;
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
{
if (!module.init(this))
{
INTERPOSE_NEXT(feed)(input);
return;
}
if (!module.process_input(input))
{
INTERPOSE_NEXT(feed)(input);
module.do_post_update_check();
}
}
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
bool ok = module.init(this);
INTERPOSE_NEXT(render)();
if (ok)
module.render();
}
};
template <class T, class V, typename D> V search_hook<T, V, D> ::module;
//
// END: Base Search functionality
//
//
// START: Stocks screen search
//
class stocks_search : public search_parent<df::viewscreen_storesst, df::item*>
{
public:
virtual void render() const
{
if (!viewscreen->in_group_mode)
print_search_option(2);
else
{
auto dim = Screen::getWindowSize();
int x = 2, y = dim.y - 2;
OutputString(15, x, y, "Tab to enable Search");
}
}
virtual void do_post_update_check()
{
if (viewscreen->in_group_mode)
{
// Disable search if item lists are grouped
clear_search();
reset_search();
}
else
search_parent::do_post_update_check();
}
bool init(df::viewscreen_storesst *screen)
{
if (screen != viewscreen && !reset_on_change())
return false;
if (!is_valid())
{
viewscreen = screen;
search_parent::init(&screen->item_cursor, &screen->items);
}
return true;
}
private:
virtual string get_element_description(df::item *element) const
{
return Items::getDescription(element, 0, true);
}
virtual bool should_check_input(set<df::interface_key> *input)
{
if (viewscreen->in_group_mode)
return false;
if ((input->count(interface_key::CURSOR_UP) || input->count(interface_key::CURSOR_DOWN)) && !viewscreen->in_right_list)
{
// Redo search if category changes
saved_list1.clear();
end_entry_mode();
if (search_string.length() > 0)
redo_search = true;
return false;
}
return true;
}
};
typedef search_hook<df::viewscreen_storesst, stocks_search> stocks_search_hook;
template<> IMPLEMENT_VMETHOD_INTERPOSE(stocks_search_hook, feed);
template<> IMPLEMENT_VMETHOD_INTERPOSE(stocks_search_hook, render);
//
// END: Stocks screen search
//
//
// START: Unit screen search
//
class unitlist_search : public search_parent<df::viewscreen_unitlistst, df::unit*, df::job*>
{
public:
virtual void render() const
{
print_search_option(28);
}
bool init(df::viewscreen_unitlistst *screen)
{
if (screen != viewscreen && !reset_on_change())
return false;
if (!is_valid())
{
viewscreen = screen;
search_parent::init(&screen->cursor_pos[viewscreen->page], &screen->units[viewscreen->page], &screen->jobs[viewscreen->page]);
}
return true;
}
private:
virtual string get_element_description(df::unit *element) const
{
string desc = Translation::TranslateName(Units::getVisibleName(element), false);
desc += ", " + Units::getProfessionName(element); // Check animal type too
return desc;
}
virtual bool should_check_input(set<df::interface_key> *input)
{
if (input->count(interface_key::CURSOR_LEFT) || input->count(interface_key::CURSOR_RIGHT) ||
(!is_entry_mode() && input->count(interface_key::UNITVIEW_PRF_PROF)))
{
if (!is_entry_mode())
{
// Changing screens, reset search
clear_search();
reset_all();
}
else
input->clear(); // Ignore cursor keys when typing
return false;
}
return true;
}
};
typedef search_hook<df::viewscreen_unitlistst, unitlist_search> unitlist_search_hook;
template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(unitlist_search_hook, feed, 100);
template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(unitlist_search_hook, render, 100);
//
// END: Unit screen search
//
//
// TODO: Animals screen search
//
//
// END: Animals screen search
//
//
// START: Trade screen search
//
class trade_search_base : public search_parent<df::viewscreen_tradegoodsst, df::item*, char>
{
private:
virtual string get_element_description(df::item *element) const
{
return Items::getDescription(element, 0, true);
}
virtual bool should_check_input(set<df::interface_key> *input)
{
if (is_entry_mode())
return true;
if (input->count(interface_key::TRADE_TRADE) ||
input->count(interface_key::TRADE_OFFER) ||
input->count(interface_key::TRADE_SEIZE))
{
// Block the keys if were searching
if (!search_string.empty())
input->clear();
// Trying to trade, reset search
clear_search();
reset_all();
return false;
}
return true;
}
};
class trade_search_merc : public trade_search_base
{
public:
virtual void render() const
{
print_search_option(2, 26);
}
bool init(df::viewscreen_tradegoodsst *screen)
{
if (screen != viewscreen && !reset_on_change())
return false;
if (!is_valid())
{
viewscreen = screen;
search_parent::init(&screen->trader_cursor, &screen->trader_items, &screen->trader_selected, 'q');
track_secondary_values = true;
}
return true;
}
};
typedef search_hook<df::viewscreen_tradegoodsst, trade_search_merc, int> trade_search_merc_hook;
template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_merc_hook, feed);
template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_merc_hook, render);
class trade_search_fort : public trade_search_base
{
public:
virtual void render() const
{
print_search_option(42, 26);
}
bool init(df::viewscreen_tradegoodsst *screen)
{
if (screen != viewscreen && !reset_on_change())
return false;
if (!is_valid())
{
viewscreen = screen;
search_parent::init(&screen->broker_cursor, &screen->broker_items, &screen->broker_selected, 'w');
track_secondary_values = true;
}
return true;
}
};
typedef search_hook<df::viewscreen_tradegoodsst, trade_search_fort, char> trade_search_fort_hook;
template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_fort_hook, feed);
template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_fort_hook, render);
//
// END: Trade screen search
//
//
// START: Stockpile screen search
//
class stockpile_search : public search_parent<df::viewscreen_layer_stockpilest, string *, bool *>
{
public:
void update_saved_secondary_list_item(size_t i, size_t j)
{
*saved_list2[i] = *(*sort_list2)[j];
}
string get_element_description(string *element) const
{
return *element;
}
void render() const
{
print_search_option(51, 23);
}
static df::layer_object_listst *getLayerList(const df::viewscreen_layer *layer, int idx)
{
return virtual_cast<df::layer_object_listst>(vector_get(layer->layer_objects,idx));
}
bool init(df::viewscreen_layer_stockpilest *screen)
{
if (screen != viewscreen && !reset_on_change())
return false;
auto list3 = getLayerList(screen, 2);
if (!list3->active)
{
if (is_valid())
{
clear_search();
reset_all();
}
return false;
}
if (!is_valid())
{
viewscreen = screen;
search_parent::init(&list3->cursor, &screen->item_names, &screen->item_status);
track_secondary_values = true;
}
return true;
}
void do_search()
{
search_parent::do_search();
auto list3 = getLayerList(viewscreen, 2);
list3->num_entries = viewscreen->item_names.size();
}
void clear_search()
{
search_parent::clear_search();
auto list3 = getLayerList(viewscreen, 2);
list3->num_entries = viewscreen->item_names.size();
}
};
typedef search_hook<df::viewscreen_layer_stockpilest, stockpile_search> stockpile_search_hook;
template<> IMPLEMENT_VMETHOD_INTERPOSE(stockpile_search_hook, feed);
template<> IMPLEMENT_VMETHOD_INTERPOSE(stockpile_search_hook, render);
//
// END: Stockpile screen search
//
DFHACK_PLUGIN("search");
DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCommand> &commands)
{
if (!gps || !gview ||
!INTERPOSE_HOOK(unitlist_search_hook, feed).apply() ||
!INTERPOSE_HOOK(unitlist_search_hook, render).apply() ||
!INTERPOSE_HOOK(trade_search_merc_hook, feed).apply() ||
!INTERPOSE_HOOK(trade_search_merc_hook, render).apply() ||
!INTERPOSE_HOOK(trade_search_fort_hook, feed).apply() ||
!INTERPOSE_HOOK(trade_search_fort_hook, render).apply() ||
!INTERPOSE_HOOK(stocks_search_hook, feed).apply() ||
!INTERPOSE_HOOK(stocks_search_hook, render).apply() ||
!INTERPOSE_HOOK(stockpile_search_hook, feed).apply() ||
!INTERPOSE_HOOK(stockpile_search_hook, render).apply())
out.printerr("Could not insert Search hooks!\n");
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
INTERPOSE_HOOK(unitlist_search_hook, feed).remove();
INTERPOSE_HOOK(unitlist_search_hook, render).remove();
INTERPOSE_HOOK(trade_search_merc_hook, feed).remove();
INTERPOSE_HOOK(trade_search_merc_hook, render).remove();
INTERPOSE_HOOK(trade_search_fort_hook, feed).remove();
INTERPOSE_HOOK(trade_search_fort_hook, render).remove();
INTERPOSE_HOOK(stocks_search_hook, feed).remove();
INTERPOSE_HOOK(stocks_search_hook, render).remove();
INTERPOSE_HOOK(stockpile_search_hook, feed).remove();
INTERPOSE_HOOK(stockpile_search_hook, render).remove();
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange ( color_ostream &out, state_change_event event )
{
switch (event) {
case SC_VIEWSCREEN_CHANGED:
unitlist_search_hook::module.reset_on_change();
trade_search_merc_hook::module.reset_on_change();
trade_search_fort_hook::module.reset_on_change();
stocks_search_hook::module.reset_on_change();
stockpile_search_hook::module.reset_on_change();
break;
default:
break;
}
return CR_OK;
}