diff --git a/docs/Plugins.rst b/docs/Plugins.rst index 211fb9e35..54a26ed01 100644 --- a/docs/Plugins.rst +++ b/docs/Plugins.rst @@ -289,6 +289,14 @@ showmood ======== Shows all items needed for the currently active strange mood. +.. _spectate: + +spectate +======== +Simple plugin to automate following random dwarves. Most of the time things will +be weighted towards z-levels with the highest job activity. Simply enter the +``spectate`` command to toggle the plugin's state. + ======== Bugfixes diff --git a/docs/changelog.txt b/docs/changelog.txt index 194af2699..fa672db1f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -34,6 +34,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: # Future ## New Plugins +- `spectate`: automates the following of dwarves more often than not based on job zlevel activity levels, sometimes randomly though. ## Removed diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index b1ed1d4ac..72d84803e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -159,6 +159,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua) dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua) dfhack_plugin(steam-engine steam-engine.cpp) + dfhack_plugin(spectate spectate.cpp) dfhack_plugin(stockflow stockflow.cpp LINK_LIBRARIES lua) add_subdirectory(stockpiles) dfhack_plugin(stocks stocks.cpp) diff --git a/plugins/spectate.cpp b/plugins/spectate.cpp new file mode 100644 index 000000000..aae48cf5a --- /dev/null +++ b/plugins/spectate.cpp @@ -0,0 +1,152 @@ +// +// Created by josh on 7/28/21. +// + +#include "Core.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +std::map freq; +std::set job_tracker; +std::default_random_engine RNG; + +//#include "df/world.h" + +using namespace DFHack; +using namespace df::enums; + +DFHACK_PLUGIN("spectate"); +DFHACK_PLUGIN_IS_ENABLED(enabled); +bool following_dwarf = false; +void* job_watched = nullptr; +int32_t timestamp = -1; +REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(ui); + +command_result spectate (color_ostream &out, std::vector & parameters); + +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { + commands.push_back(PluginCommand("spectate", + "Automated spectator mode.", + spectate, + false, + "" + " spectate\n" + " toggles spectator mode\n" + "\n")); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + return CR_OK; +} + +void onTick(color_ostream& out, void* tick); +void onJobStart(color_ostream &out, void* job); +void onJobCompletion(color_ostream &out, void* job); + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + namespace EM = EventManager; + if (enable && !enabled) { + out.print("Spectate mode enabled!\n"); + using namespace EM::EventType; + EM::EventHandler ticking(onTick, 15); + EM::EventHandler start(onJobStart, 0); + EM::EventHandler complete(onJobCompletion, 0); + EM::registerListener(EventType::TICK, ticking, plugin_self); + EM::registerListener(EventType::JOB_STARTED, start, plugin_self); + EM::registerListener(EventType::JOB_COMPLETED, complete, plugin_self); + } else if (!enable && enabled) { + out.print("Spectate mode disabled!\n"); + EM::unregisterAll(plugin_self); + job_tracker.clear(); + freq.clear(); + } + enabled = enable; + return CR_OK; +} + +command_result spectate (color_ostream &out, std::vector & parameters) { + return plugin_enable(out, !enabled); +} + +void onTick(color_ostream& out, void* ptr) { + int32_t tick = df::global::world->frame_counter; + // || seems to be redundant as the first always evaluates true when job_watched is nullptr.. todo: figure what is supposed to happen + if (!following_dwarf || (job_watched == nullptr && (tick - timestamp) > 50)) { + std::vector dwarves; + for (auto unit: df::global::world->units.active) { + if (!Units::isCitizen(unit)) { + continue; + } + dwarves.push_back(unit); + } + std::uniform_int_distribution follow_any(0, dwarves.size() - 1); + if (df::global::ui) { + df::unit* unit = dwarves[follow_any(RNG)]; + df::global::ui->follow_unit = unit->id; + job_watched = unit->job.current_job; + following_dwarf = true; + if (!job_watched) { + timestamp = tick; + } + } + } +} + +void onJobStart(color_ostream& out, void* job_ptr) { + int32_t tick = df::global::world->frame_counter; + auto job = (df::job*) job_ptr; + int zcount = ++freq[job->pos.z]; + job_tracker.emplace(job->id); + if (!following_dwarf || (job_watched == nullptr && (tick - timestamp) > 50)) { + following_dwarf = true; + double p = 0.99 * ((double) zcount / job_tracker.size()); + std::bernoulli_distribution follow_job(p); + if (!job->flags.bits.special && follow_job(RNG)) { + job_watched = job_ptr; + df::unit* unit = Job::getWorker(job); + if (df::global::ui && unit) { + df::global::ui->follow_unit = unit->id; + } + } else { + timestamp = tick; + std::vector nonworkers; + for (auto unit: df::global::world->units.active) { + if (!Units::isCitizen(unit) || unit->job.current_job) { + continue; + } + nonworkers.push_back(unit); + } + std::uniform_int_distribution<> follow_drunk(0, nonworkers.size() - 1); + if (df::global::ui) { + df::global::ui->follow_unit = nonworkers[follow_drunk(RNG)]->id; + } + } + } +} + +void onJobCompletion(color_ostream &out, void* job_ptr) { + auto job = (df::job*)job_ptr; + freq[job->pos.z]--; + freq[job->pos.z] = freq[job->pos.z] < 0 ? 0 : freq[job->pos.z]; + job_tracker.erase(job->id); + if (following_dwarf && job_ptr == job_watched) { + following_dwarf = false; + job_watched = nullptr; + } +}