diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 2f3c82f4f..e1410116b 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -130,6 +130,7 @@ if (BUILD_SUPPORTED) #DFHACK_PLUGIN(versionosd versionosd.cpp) DFHACK_PLUGIN(misery misery.cpp) DFHACK_PLUGIN(buildingplan buildingplan.cpp) + DFHACK_PLUGIN(resume resume.cpp) endif() diff --git a/plugins/resume.cpp b/plugins/resume.cpp new file mode 100644 index 000000000..8920b9ce5 --- /dev/null +++ b/plugins/resume.cpp @@ -0,0 +1,316 @@ +#include +#include +#include + +#include "Core.h" +#include +#include +#include +#include + + +// 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.1 + +#ifndef HAVE_NULLPTR +#define nullptr 0L +#endif + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + return CR_OK; +} + +template +static void for_each_(vector &v, Fn func) +{ + for_each(v.begin(), v.end(), func); +} + +template +static void transform_(vector &src, vector &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 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)(); + + DFHack::World *world = Core::getInstance().getWorld(); + if (enabled && 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 & 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 &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; +}