// // Created by josh on 7/28/21. // #include "pause.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Debugging namespace DFHack { DBG_DECLARE(spectate, plugin, DebugCategory::LINFO); } DFHACK_PLUGIN("spectate"); DFHACK_PLUGIN_IS_ENABLED(enabled); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(ui); REQUIRE_GLOBAL(pause_state); REQUIRE_GLOBAL(d_init); using namespace DFHack; using namespace Pausing; using namespace df::enums; struct Configuration { bool debug = false; bool unpause = false; bool disengage = false; bool animals = false; bool hostiles = true; bool visitors = false; int32_t tick_threshold = 1000; } config; Pausing::AnnouncementLock* pause_lock = nullptr; bool lock_collision = false; bool announcements_disabled = false; #define base 0.99 static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; enum ConfigData { UNPAUSE, DISENGAGE, TICK_THRESHOLD, ANIMALS, HOSTILES, VISITORS }; static PersistentDataItem pconfig; DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); command_result spectate (color_ostream &out, std::vector & parameters); #define COORDARGS(id) id.x, (id).y, id.z namespace SP { bool following_dwarf = false; df::unit* our_dorf = nullptr; int32_t timestamp = -1; std::default_random_engine RNG; void PrintStatus(color_ostream &out) { out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); out.print(" FEATURES:\n"); out.print(" %-20s\t%s\n", "auto-unpause: ", config.unpause ? "on." : "off."); out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off."); out.print(" %-20s\t%s\n", "animals: ", config.animals ? "on." : "off."); out.print(" %-20s\t%s\n", "hostiles: ", config.hostiles ? "on." : "off."); out.print(" %-20s\t%s\n", "visiting: ", config.visitors ? "on." : "off."); out.print(" SETTINGS:\n"); out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold); if (following_dwarf) out.print(" %-21s\t%s[id: %d]\n","FOLLOWING:", our_dorf ? our_dorf->name.first_name.c_str() : "nullptr", df::global::ui->follow_unit); } void SetUnpauseState(bool state) { // we don't need to do any of this yet if the plugin isn't enabled if (enabled) { // todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings] // lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete // The onupdate function above ensure the procedure properly completes, thus we only care about // state reversal here ergo `enabled != state` if (lock_collision && config.unpause != state) { WARN(plugin).print("Spectate auto-unpause: Not enabled yet, there was a lock collision. When the other lock holder releases, auto-unpause will engage on its own.\n"); // if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings, // therefore nothing to revert and the lock won't even be engaged (nothing to unlock) lock_collision = false; config.unpause = state; if (config.unpause) { // a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock pause_lock->lock(); } return; } // update the announcement settings if we can if (state) { if (World::SaveAnnouncementSettings()) { World::DisableAnnouncementPausing(); announcements_disabled = true; pause_lock->lock(); } else { WARN(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); lock_collision = true; } } else { pause_lock->unlock(); if (announcements_disabled) { if (!World::RestoreAnnouncementSettings()) { // this in theory shouldn't happen, if others use the lock like we do in spectate WARN(plugin).print("Spectate auto-unpause: Could not fully disable. There was a lock collision, when the other lock holder releases, auto-unpause will disengage on its own.\n"); lock_collision = true; } else { announcements_disabled = false; } } } if (lock_collision) { ERR(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); WARN(plugin).print( " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" " The action you were attempting will complete when the following lock or locks lift.\n"); pause_lock->reportLocks(Core::getInstance().getConsole()); } } config.unpause = state; } void SaveSettings() { if (pconfig.isValid()) { pconfig.ival(UNPAUSE) = config.unpause; pconfig.ival(DISENGAGE) = config.disengage; pconfig.ival(TICK_THRESHOLD) = config.tick_threshold; pconfig.ival(ANIMALS) = config.animals; pconfig.ival(HOSTILES) = config.hostiles; pconfig.ival(VISITORS) = config.visitors; } } void LoadSettings() { pconfig = World::GetPersistentData(CONFIG_KEY); if (!pconfig.isValid()) { pconfig = World::AddPersistentData(CONFIG_KEY); SaveSettings(); } else { config.unpause = pconfig.ival(UNPAUSE); config.disengage = pconfig.ival(DISENGAGE); config.tick_threshold = pconfig.ival(TICK_THRESHOLD); config.animals = pconfig.ival(ANIMALS); config.hostiles = pconfig.ival(HOSTILES); config.visitors = pconfig.ival(VISITORS); pause_lock->unlock(); SetUnpauseState(config.unpause); } } bool FollowADwarf() { if (enabled && !World::ReadPauseState()) { df::coord viewMin = Gui::getViewportPos(); df::coord viewMax{viewMin}; const auto &dims = Gui::getDwarfmodeViewDims().map().second; viewMax.x += dims.x - 1; viewMax.y += dims.y - 1; viewMax.z = viewMin.z; std::vector units; static auto add_if = [&](std::function check) { for (auto unit : world->units.active) { if (check(unit)) { units.push_back(unit); } } }; static auto valid = [](df::unit* unit) { if (Units::isAnimal(unit)) { return config.animals; } if (Units::isVisiting(unit)) { return config.visitors; } if (Units::isDanger(unit)) { return config.hostiles; } return true; }; /// RANGE 1 (in view) // grab all valid units add_if(valid); // keep only those in the box Units::getUnitsInBox(units, COORDARGS(viewMin), COORDARGS(viewMax)); int32_t inview_idx2 = units.size()-1; bool range1_exists = inview_idx2 >= 0; int32_t inview_idx1 = range1_exists ? 0 : -1; /// RANGE 2 (citizens) add_if([](df::unit* unit) { return valid(unit) && Units::isCitizen(unit, true); }); int32_t cit_idx2 = units.size()-1; bool range2_exists = cit_idx2 > inview_idx2; int32_t cit_idx1 = range2_exists ? inview_idx2+1 : cit_idx2; /// RANGE 3 (any valid) add_if(valid); int32_t all_idx2 = units.size()-1; bool range3_exists = all_idx2 > cit_idx2; int32_t all_idx1 = range3_exists ? cit_idx2+1 : all_idx2; if (!units.empty()) { std::vector i; std::vector w; if (!range1_exists && !range2_exists && !range3_exists) { return false; } if (range1_exists) { if (inview_idx1 == inview_idx2) { i.push_back(0); w.push_back(17); } else { i.push_back(inview_idx1); i.push_back(inview_idx2); w.push_back(inview_idx2 + 1); w.push_back(inview_idx2 + 1); } } if (range2_exists) { if (cit_idx1 == cit_idx2) { i.push_back(cit_idx1); w.push_back(7); } else { i.push_back(cit_idx1); i.push_back(cit_idx2); w.push_back(7); w.push_back(7); } } if (range3_exists) { if (all_idx1 == all_idx2) { i.push_back(all_idx1); w.push_back(1); } else { i.push_back(all_idx1); i.push_back(all_idx2); w.push_back(1); w.push_back(1); } } std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin()); // if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local) size_t idx = follow_any(RNG); our_dorf = units[idx]; df::global::ui->follow_unit = our_dorf->id; timestamp = df::global::world->frame_counter; return true; } else { WARN(plugin).print("units vector is empty!\n"); } } return false; } void onUpdate(color_ostream &out) { if (!World::isFortressMode() || !Maps::IsValid()) return; // keeps announcement pause settings locked World::Update(); // from pause.h // Plugin Management if (lock_collision) { if (config.unpause) { // player asked for auto-unpause enabled World::SaveAnnouncementSettings(); if (World::DisableAnnouncementPausing()) { // now that we've got what we want, we can lock it down lock_collision = false; } } else { if (World::RestoreAnnouncementSettings()) { lock_collision = false; } } } int failsafe = 0; while (config.unpause && !world->status.popups.empty() && ++failsafe <= 10) { // dismiss announcement popup(s) Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT); if (World::ReadPauseState()) { // WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is World::SetPauseState(false); } } if (failsafe >= 10) { out.printerr("spectate encountered a problem dismissing a popup!\n"); } // plugin logic static int32_t last_tick = -1; int32_t tick = world->frame_counter; if (!World::ReadPauseState() && tick - last_tick >= 1) { last_tick = tick; // validate follow state if (!following_dwarf || !our_dorf || df::global::ui->follow_unit < 0) { // we're not following anyone following_dwarf = false; if (!config.disengage) { // try to following_dwarf = FollowADwarf(); } else if (!World::ReadPauseState()) { plugin_enable(out, false); } } } } }; DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand("spectate", "Automated spectator mode.", spectate, false)); pause_lock = new AnnouncementLock("spectate"); return CR_OK; } DFhackCExport command_result plugin_shutdown (color_ostream &out) { delete pause_lock; return CR_OK; } DFhackCExport command_result plugin_load_data (color_ostream &out) { SP::LoadSettings(); SP::following_dwarf = SP::FollowADwarf(); SP::PrintStatus(out); return DFHack::CR_OK; } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable && !enabled) { out.print("Spectate mode enabled!\n"); enabled = true; // enable_auto_unpause won't do anything without this set now SP::SetUnpauseState(config.unpause); } else if (!enable && enabled) { // warp 8, engage! out.print("Spectate mode disabled!\n"); // we need to retain whether auto-unpause is enabled, but we also need to disable its effect bool temp = config.unpause; SP::SetUnpauseState(false); config.unpause = temp; } enabled = enable; return DFHack::CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (enabled) { switch (event) { case SC_MAP_UNLOADED: case SC_BEGIN_UNLOAD: case SC_WORLD_UNLOADED: SP::our_dorf = nullptr; SP::following_dwarf = false; default: break; } } return CR_OK; } DFhackCExport command_result plugin_onupdate(color_ostream &out) { SP::onUpdate(out); return DFHack::CR_OK; } command_result spectate (color_ostream &out, std::vector & parameters) { if (!parameters.empty()) { if (parameters.size() >= 2 && parameters.size() <= 3) { bool state =false; bool set = false; if (parameters[0] == "enable") { state = true; } else if (parameters[0] == "disable") { state = false; } else if (parameters[0] == "set") { set = true; } else { return DFHack::CR_WRONG_USAGE; } if(parameters[1] == "auto-unpause"){ SP::SetUnpauseState(state); } else if (parameters[1] == "auto-disengage") { config.disengage = state; } else if (parameters[1] == "animals") { config.animals = state; } else if (parameters[1] == "hostiles") { config.hostiles = state; } else if (parameters[1] == "visiting") { config.visitors = state; } else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) { try { config.tick_threshold = std::abs(std::stol(parameters[2])); } catch (const std::exception &e) { out.printerr("%s\n", e.what()); } } else { return DFHack::CR_WRONG_USAGE; } } } else { SP::PrintStatus(out); } SP::SaveSettings(); return DFHack::CR_OK; }