// automatically chop trees #include "uicommon.h" #include "listcolumn.h" #include "Core.h" #include "Console.h" #include "Export.h" #include "PluginManager.h" #include "DataDefs.h" #include "TileTypes.h" #include "df/burrow.h" #include "df/item.h" #include "df/item_flags.h" #include "df/items_other_id.h" #include "df/job.h" #include "df/map_block.h" #include "df/material.h" #include "df/plant.h" #include "df/plant_tree_info.h" #include "df/plant_tree_tile.h" #include "df/plant_raw.h" #include "df/tile_dig_designation.h" #include "df/ui.h" #include "df/viewscreen_dwarfmodest.h" #include "df/world.h" #include "modules/Burrows.h" #include "modules/Designations.h" #include "modules/Gui.h" #include "modules/MapCache.h" #include "modules/Maps.h" #include "modules/Screen.h" #include "modules/World.h" #include using std::string; using std::vector; using std::set; using namespace DFHack; using namespace df::enums; #define PLUGIN_VERSION 0.3 DFHACK_PLUGIN("autochop"); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(ui); static int get_log_count(); static bool autochop_enabled = false; static int min_logs, max_logs; static const int LOG_CAP_MAX = 99999; static bool wait_for_threshold; struct Skip { bool fruit_trees; bool food_trees; bool cook_trees; operator int() { return (fruit_trees ? 1 : 0) | (food_trees ? 2 : 0) | (cook_trees ? 4 : 0); } Skip &operator= (int in) { // set all fields to false if they haven't been set in this save yet if (in < 0) in = 0; fruit_trees = (in & 1); food_trees = (in & 2); cook_trees = (in & 4); return *this; } }; static Skip skip; static PersistentDataItem config_autochop; struct WatchedBurrow { int32_t id; df::burrow *burrow; WatchedBurrow(df::burrow *burrow) : burrow(burrow) { id = burrow->id; } }; class WatchedBurrows { public: string getSerialisedIds() { validate(); stringstream burrow_ids; bool append_started = false; for (auto it = burrows.begin(); it != burrows.end(); it++) { if (append_started) burrow_ids << " "; burrow_ids << it->id; append_started = true; } return burrow_ids.str(); } void clear() { burrows.clear(); } void add(const int32_t id) { if (!isValidBurrow(id)) return; WatchedBurrow wb(getBurrow(id)); burrows.push_back(wb); } void add(const string burrow_ids) { istringstream iss(burrow_ids); int id; while (iss >> id) { add(id); } } bool isValidPos(const df::coord &plant_pos) { validate(); if (!burrows.size()) return true; for (auto it = burrows.begin(); it != burrows.end(); it++) { df::burrow *burrow = it->burrow; if (Burrows::isAssignedTile(burrow, plant_pos)) return true; } return false; } bool isBurrowWatched(const df::burrow *burrow) { validate(); for (auto it = burrows.begin(); it != burrows.end(); it++) { if (it->burrow == burrow) return true; } return false; } private: static bool isValidBurrow(const int32_t id) { return getBurrow(id); } static df::burrow *getBurrow(const int32_t id) { return df::burrow::find(id); } void validate() { for (auto it = burrows.begin(); it != burrows.end();) { if (!isValidBurrow(it->id)) it = burrows.erase(it); else ++it; } } vector burrows; }; static WatchedBurrows watchedBurrows; static void save_config() { config_autochop.val() = watchedBurrows.getSerialisedIds(); config_autochop.ival(0) = autochop_enabled; config_autochop.ival(1) = min_logs; config_autochop.ival(2) = max_logs; config_autochop.ival(3) = wait_for_threshold; config_autochop.ival(4) = skip; } static void initialize() { watchedBurrows.clear(); autochop_enabled = false; min_logs = 80; max_logs = 100; wait_for_threshold = false; skip = 0; config_autochop = World::GetPersistentData("autochop/config"); if (config_autochop.isValid()) { watchedBurrows.add(config_autochop.val()); autochop_enabled = config_autochop.ival(0); min_logs = config_autochop.ival(1); max_logs = config_autochop.ival(2); wait_for_threshold = config_autochop.ival(3); skip = config_autochop.ival(4); } else { config_autochop = World::AddPersistentData("autochop/config"); if (config_autochop.isValid()) save_config(); } } static bool skip_plant(const df::plant * plant, bool *restricted) { if (restricted) *restricted = false; // Skip all non-trees immediately. if (plant->flags.bits.is_shrub) return true; // Skip plants with invalid tile. df::map_block *cur = Maps::getTileBlock(plant->pos); if (!cur) return true; int x = plant->pos.x % 16; int y = plant->pos.y % 16; // Skip all unrevealed plants. if (cur->designation[x][y].bits.hidden) return true; df::tiletype_material material = tileMaterial(cur->tiletype[x][y]); if (material != tiletype_material::TREE) return true; const df::plant_raw *plant_raw = df::plant_raw::find(plant->material); // Skip fruit trees if set. if (skip.fruit_trees && plant_raw->material_defs.type[plant_material_def::drink] != -1) { if (restricted) *restricted = true; return true; } if (skip.food_trees || skip.cook_trees) { for (df::material * mat : plant_raw->material) { if (skip.food_trees && mat->flags.is_set(material_flags::EDIBLE_RAW)) { if (restricted) *restricted = true; return true; } if (skip.cook_trees && mat->flags.is_set(material_flags::EDIBLE_COOKED)) { if (restricted) *restricted = true; return true; } } } return false; } static int estimate_logs(const df::plant *plant) { //adapted from code by aljohnston112 @ github df::plant_tree_tile** tiles = plant->tree_info->body; df::plant_tree_tile* tilesRow; int trunks = 0; for (int i = 0; i < plant->tree_info->body_height; i++) { tilesRow = tiles[i]; for (int j = 0; j < plant->tree_info->dim_y*plant->tree_info->dim_x; j++) { trunks += tilesRow[j].bits.trunk; } } return trunks; } static int do_chop_designation(bool chop, bool count_only, int *skipped = nullptr) { int count = 0; int estimated_yield = get_log_count(); multimap> trees_by_size; if (skipped) { *skipped = 0; } //get trees for (auto plant : world->plants.all) { bool restricted = false; if (skip_plant(plant, &restricted)) { if (restricted && skipped) { ++*skipped; } continue; } trees_by_size.insert(pair(estimate_logs(plant), plant)); } //designate for (auto & entry : trees_by_size) { const df::plant * plant = entry.second; if ((estimated_yield >= max_logs) && chop) break; if (!count_only && !watchedBurrows.isValidPos(plant->pos)) continue; if (chop && !Designations::isPlantMarked(plant)) { if (count_only) { if (Designations::canMarkPlant(plant)) count++; } else { if (Designations::markPlant(plant)) { estimated_yield += entry.first; count++; } } } if (!chop && Designations::isPlantMarked(plant)) { if (count_only) { if (Designations::canUnmarkPlant(plant)) count++; } else { if (Designations::unmarkPlant(plant)) count++; } } } return count; } static bool is_valid_item(df::item *item) { for (size_t i = 0; i < item->general_refs.size(); i++) { df::general_ref *ref = item->general_refs[i]; switch (ref->getType()) { case general_ref_type::CONTAINED_IN_ITEM: return false; case general_ref_type::UNIT_HOLDER: return false; case general_ref_type::BUILDING_HOLDER: return false; default: break; } } for (size_t i = 0; i < item->specific_refs.size(); i++) { df::specific_ref *ref = item->specific_refs[i]; if (ref->type == specific_ref_type::JOB) { // Ignore any items assigned to a job return false; } } return true; } static int get_log_count() { std::vector &items = world->items.other[items_other_id::IN_PLAY]; // Pre-compute a bitmask with the bad flags df::item_flags bad_flags; bad_flags.whole = 0; #define F(x) bad_flags.bits.x = true; F(dump); F(forbid); F(garbage_collect); F(hostile); F(on_fire); F(rotten); F(trader); F(in_building); F(construction); F(artifact); F(spider_web); F(owned); F(in_job); #undef F size_t valid_count = 0; for (size_t i = 0; i < items.size(); i++) { df::item *item = items[i]; if (item->getType() != item_type::WOOD) continue; if (item->flags.whole & bad_flags.whole) continue; if (!is_valid_item(item)) continue; ++valid_count; } return valid_count; } static void set_threshold_check(bool state) { wait_for_threshold = state; save_config(); } static void do_autochop() { int log_count = get_log_count(); if (wait_for_threshold) { if (log_count < min_logs) { set_threshold_check(false); do_chop_designation(true, false); } } else { if (log_count >= max_logs) { set_threshold_check(true); do_chop_designation(false, false); } else { do_chop_designation(true, false); } } } class ViewscreenAutochop : public dfhack_viewscreen { public: ViewscreenAutochop(): selected_column(0), current_log_count(0), marked_tree_count(0), skipped_tree_count(0) { edit_mode = EDIT_NONE; burrows_column.multiselect = true; burrows_column.setTitle("Burrows"); burrows_column.bottom_margin = 3; burrows_column.allow_search = false; burrows_column.text_clip_at = 30; populateBurrowsColumn(); message.clear(); } void populateBurrowsColumn() { selected_column = 0; burrows_column.clear(); for (df::burrow *burrow : ui->burrows.list) { string name = burrow->name; if (name.empty()) name = "Burrow " + int_to_string(burrow->id + 1); auto elem = ListEntry(name, burrow); elem.selected = watchedBurrows.isBurrowWatched(burrow); burrows_column.add(elem); } burrows_column.fixWidth(); burrows_column.filterDisplay(); current_log_count = get_log_count(); marked_tree_count = do_chop_designation(false, true, &skipped_tree_count); } void change_min_logs(int delta) { if (!autochop_enabled) return; min_logs += delta; if (min_logs < 0) min_logs = 0; if (min_logs > max_logs) max_logs = min_logs; } void change_max_logs(int delta) { if (!autochop_enabled) return; max_logs += delta; if (max_logs < min_logs) min_logs = max_logs; } void feed(set *input) { if (edit_mode != EDIT_NONE) { string entry = int_to_string(edit_mode == EDIT_MIN ? min_logs : max_logs); if (input->count(interface_key::LEAVESCREEN) || input->count(interface_key::SELECT)) { if (edit_mode == EDIT_MIN) max_logs = std::max(min_logs, max_logs); else if (edit_mode == EDIT_MAX) min_logs = std::min(min_logs, max_logs); edit_mode = EDIT_NONE; } else if (input->count(interface_key::STRING_A000)) { if (!entry.empty()) entry.erase(entry.size() - 1); } else if (entry.size() < 5) { for (auto k = input->begin(); k != input->end(); ++k) { char ch = char(Screen::keyToChar(*k)); if (ch >= '0' && ch <= '9') entry += ch; } } switch (edit_mode) { case EDIT_MIN: min_logs = string_to_int(entry); break; case EDIT_MAX: max_logs = string_to_int(entry); break; default: break; } return; } bool key_processed = false; message.clear(); switch (selected_column) { case 0: key_processed = burrows_column.feed(input); break; } if (key_processed) { if (input->count(interface_key::SELECT)) updateAutochopBurrows(); return; } if (input->count(interface_key::LEAVESCREEN)) { save_config(); input->clear(); Screen::dismiss(this); if (autochop_enabled) do_autochop(); return; } else if (input->count(interface_key::CUSTOM_A)) { autochop_enabled = !autochop_enabled; } else if (input->count(interface_key::CUSTOM_D)) { int count = do_chop_designation(true, false); message = "Trees marked for chop: " + int_to_string(count); marked_tree_count = do_chop_designation(false, true, &skipped_tree_count); if (skipped_tree_count) { message += ", skipped: " + int_to_string(skipped_tree_count); } } else if (input->count(interface_key::CUSTOM_U)) { int count = do_chop_designation(false, false); message = "Trees unmarked: " + int_to_string(count); marked_tree_count = do_chop_designation(false, true, &skipped_tree_count); if (skipped_tree_count) { message += ", skipped: " + int_to_string(skipped_tree_count); } } else if (input->count(interface_key::CUSTOM_N)) { edit_mode = EDIT_MIN; } else if (input->count(interface_key::CUSTOM_M)) { edit_mode = EDIT_MAX; } else if (input->count(interface_key::CUSTOM_SHIFT_N)) { min_logs = LOG_CAP_MAX + 1; max_logs = LOG_CAP_MAX + 1; } else if (input->count(interface_key::CUSTOM_H)) { change_min_logs(-1); } else if (input->count(interface_key::CUSTOM_SHIFT_H)) { change_min_logs(-10); } else if (input->count(interface_key::CUSTOM_J)) { change_min_logs(1); } else if (input->count(interface_key::CUSTOM_SHIFT_J)) { change_min_logs(10); } else if (input->count(interface_key::CUSTOM_K)) { change_max_logs(-1); } else if (input->count(interface_key::CUSTOM_SHIFT_K)) { change_max_logs(-10); } else if (input->count(interface_key::CUSTOM_L)) { change_max_logs(1); } else if (input->count(interface_key::CUSTOM_SHIFT_L)) { change_max_logs(10); } else if (input->count(interface_key::CUSTOM_F)) { skip.fruit_trees = !skip.fruit_trees; } else if (input->count(interface_key::CUSTOM_E)) { skip.food_trees = !skip.food_trees; } else if (input->count(interface_key::CUSTOM_C)) { skip.cook_trees = !skip.cook_trees; } else if (enabler->tracking_on && enabler->mouse_lbut) { if (burrows_column.setHighlightByMouse()) { selected_column = 0; } enabler->mouse_lbut = enabler->mouse_rbut = 0; } } void render() { if (Screen::isDismissed(this)) return; dfhack_viewscreen::render(); Screen::clear(); Screen::drawBorder(" Autochop "); burrows_column.display(selected_column == 0); int32_t y = gps->dimy - 3; int32_t x = 2; OutputHotkeyString(x, y, "Leave", "Esc"); x += 3; OutputString(COLOR_YELLOW, x, y, message); y = 3; int32_t left_margin = burrows_column.getMaxItemWidth() + 3; x = left_margin; if (burrows_column.getSelectedElems().size() > 0) { OutputString(COLOR_GREEN, x, y, "Will chop in selected burrows", true, left_margin); ++y; } else { OutputString(COLOR_YELLOW, x, y, "Will chop from whole map", true, left_margin); OutputString(COLOR_YELLOW, x, y, "Select from left to chop in specific burrows", true, left_margin); } ++y; using namespace df::enums::interface_key; OutputToggleString(x, y, "Autochop", CUSTOM_A, autochop_enabled, true, left_margin); OutputHotkeyString(x, y, "Designate Now", CUSTOM_D, true, left_margin); OutputHotkeyString(x, y, "Undesignate Now", CUSTOM_U, true, left_margin); OutputHotkeyString(x, y, "Toggle Burrow", "Enter", true, left_margin); if (autochop_enabled) { const struct { const char *caption; int count; bool in_edit; df::interface_key key; df::interface_key skeys[4]; } rows[] = { {"Min Logs: ", min_logs, edit_mode == EDIT_MIN, CUSTOM_N, {CUSTOM_H, CUSTOM_J, CUSTOM_SHIFT_H, CUSTOM_SHIFT_J}}, {"Max Logs: ", max_logs, edit_mode == EDIT_MAX, CUSTOM_M, {CUSTOM_K, CUSTOM_L, CUSTOM_SHIFT_K, CUSTOM_SHIFT_L}} }; for (size_t i = 0; i < sizeof(rows) / sizeof(rows[0]); ++i) { auto row = rows[i]; OutputHotkeyString(x, y, row.caption, row.key); auto prev_x = x; if (row.in_edit) OutputString(COLOR_LIGHTCYAN, x, y, int_to_string(row.count) + "_"); else if (row.count <= LOG_CAP_MAX) OutputString(COLOR_LIGHTGREEN, x, y, int_to_string(row.count)); else OutputString(COLOR_LIGHTBLUE, x, y, "Unlimited"); if (edit_mode == EDIT_NONE) { x = std::max(x, prev_x + 10); for (size_t j = 0; j < sizeof(row.skeys) / sizeof(row.skeys[0]); ++j) OutputString(COLOR_LIGHTGREEN, x, y, DFHack::Screen::getKeyDisplay(row.skeys[j])); OutputString(COLOR_WHITE, x, y, ": Step"); } OutputString(COLOR_WHITE, x, y, "", true, left_margin); } OutputHotkeyString(x, y, "No limit", CUSTOM_SHIFT_N, true, left_margin); OutputToggleString(x, y, "Skip Fruit Trees", CUSTOM_F, skip.fruit_trees, true, left_margin); OutputToggleString(x, y, "Skip Edible Product Trees", CUSTOM_E, skip.food_trees, true, left_margin); OutputToggleString(x, y, "Skip Cookable Product Trees", CUSTOM_C, skip.cook_trees, true, left_margin); } ++y; OutputString(COLOR_BROWN, x, y, "Current Counts", true, left_margin); OutputString(COLOR_WHITE, x, y, "Current Logs: "); OutputString(COLOR_GREEN, x, y, int_to_string(current_log_count), true, left_margin); OutputString(COLOR_WHITE, x, y, "Marked Trees: "); OutputString(COLOR_GREEN, x, y, int_to_string(marked_tree_count), true, left_margin); } std::string getFocusString() { return "autochop"; } void updateAutochopBurrows() { watchedBurrows.clear(); vector v = burrows_column.getSelectedElems(); for_each_(v, [] (df::burrow *b) { watchedBurrows.add(b->id); }); } private: ListColumn burrows_column; int selected_column; int current_log_count; int marked_tree_count; int skipped_tree_count; MapExtras::MapCache mcache; string message; enum { EDIT_NONE, EDIT_MIN, EDIT_MAX } edit_mode; void validateColumn() { set_to_limit(selected_column, 0); } void resize(int32_t x, int32_t y) { dfhack_viewscreen::resize(x, y); burrows_column.resize(); } }; struct autochop_hook : public df::viewscreen_dwarfmodest { typedef df::viewscreen_dwarfmodest interpose_base; bool isInDesignationMenu() { using namespace df::enums::ui_sidebar_mode; return (ui->main.mode == DesignateChopTrees); } void sendKey(const df::interface_key &key) { set tmp; tmp.insert(key); INTERPOSE_NEXT(feed)(&tmp); } DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) { if (isInDesignationMenu() && input->count(interface_key::CUSTOM_C)) { sendKey(interface_key::LEAVESCREEN); Screen::show(dts::make_unique(), plugin_self); } else { INTERPOSE_NEXT(feed)(input); } } DEFINE_VMETHOD_INTERPOSE(void, render, ()) { INTERPOSE_NEXT(render)(); auto dims = Gui::getDwarfmodeViewDims(); if (dims.menu_x1 <= 0) return; if (!isInDesignationMenu()) return; int left_margin = dims.menu_x1 + 1; int x = left_margin; int y = 26; OutputHotkeyString(x, y, "Autochop Dashboard", "c"); } }; IMPLEMENT_VMETHOD_INTERPOSE_PRIO(autochop_hook, feed, 100); IMPLEMENT_VMETHOD_INTERPOSE_PRIO(autochop_hook, render, 100); command_result df_autochop (color_ostream &out, vector & parameters) { for (size_t i = 0; i < parameters.size(); i++) { if (parameters[i] == "help" || parameters[i] == "?") return CR_WRONG_USAGE; if (parameters[i] == "debug") save_config(); else return CR_WRONG_USAGE; } if (Maps::IsValid()) Screen::show(dts::make_unique(), plugin_self); return CR_OK; } DFhackCExport command_result plugin_onupdate (color_ostream &out) { if (!autochop_enabled) return CR_OK; if(!Maps::IsValid()) return CR_OK; if (DFHack::World::ReadPauseState()) return CR_OK; if (world->frame_counter % 1200 != 0) // Check every day return CR_OK; do_autochop(); return CR_OK; } DFHACK_PLUGIN_IS_ENABLED(is_enabled); DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!gps) return CR_FAILURE; if (enable != is_enabled) { if (!INTERPOSE_HOOK(autochop_hook, feed).apply(enable) || !INTERPOSE_HOOK(autochop_hook, render).apply(enable)) return CR_FAILURE; is_enabled = enable; initialize(); } return CR_OK; } DFhackCExport command_result plugin_init ( color_ostream &out, vector &commands) { commands.push_back(PluginCommand( "autochop", "Auto-harvest trees when low on stockpiled logs.", df_autochop)); initialize(); return CR_OK; } DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { return CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { switch (event) { case SC_MAP_LOADED: initialize(); break; default: break; } return CR_OK; }