diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index dc341b39d..c9438e04e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -163,7 +163,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) + add_subdirectory(spectate) dfhack_plugin(stockflow stockflow.cpp LINK_LIBRARIES lua) add_subdirectory(stockpiles) dfhack_plugin(stocks stocks.cpp) diff --git a/plugins/spectate/CMakeLists.txt b/plugins/spectate/CMakeLists.txt new file mode 100644 index 000000000..000291d34 --- /dev/null +++ b/plugins/spectate/CMakeLists.txt @@ -0,0 +1,8 @@ + +project(spectate) + +SET(SOURCES + spectate.cpp + pause.cpp) + +dfhack_plugin(${PROJECT_NAME} ${SOURCES} LINK_LIBRARIES lua) \ No newline at end of file diff --git a/plugins/spectate/pause.cpp b/plugins/spectate/pause.cpp new file mode 100644 index 000000000..bac3c1654 --- /dev/null +++ b/plugins/spectate/pause.cpp @@ -0,0 +1,160 @@ +#include "pause.h" +#include +#include +#include +#include +#include +#include + +#include + +using namespace DFHack; +using namespace Pausing; +using namespace df::enums; + +namespace pausing { + AnnouncementLock announcementLock("monitor"); + PlayerLock playerLock("monitor"); + + const size_t announcement_flag_arr_size = sizeof(decltype(df::announcements::flags)) / sizeof(df::announcement_flags); + bool state_saved = false; // indicates whether a restore state is ok + bool announcements_disabled = false; // indicates whether disable or restore was last enacted, could use a better name + bool saved_states[announcement_flag_arr_size]; // state to restore + bool locked_states[announcement_flag_arr_size]; // locked state (re-applied each frame) + bool allow_player_pause = true; // toggles player pause ability + + using df::global::ui; + using namespace df::enums; + struct player_pause_hook : df::viewscreen_dwarfmodest { + typedef df::viewscreen_dwarfmodest interpose_base; + DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set* input)) { + if ((ui->main.mode == ui_sidebar_mode::Default) && !allow_player_pause) { + input->erase(interface_key::D_PAUSE); + } + INTERPOSE_NEXT(feed)(input); + } + }; + + IMPLEMENT_VMETHOD_INTERPOSE(player_pause_hook, feed); +} +using namespace pausing; + +std::unordered_set PlayerLock::locks; +std::unordered_set AnnouncementLock::locks; + +template +inline bool any_lock(Locks locks) { + return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); +} + +template +inline bool only_lock(Locks locks, LockT* this_lock) { + return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { + if (lock == this_lock) { + return lock->isLocked(); + } + return !lock->isLocked(); + }); +} + +template +inline bool only_or_none_locked(Locks locks, LockT* this_lock) { + for (auto &L: locks) { + if (L == this_lock) { + continue; + } + if (L->isLocked()) { + return false; + } + } + return true; +} + +bool AnnouncementLock::captureState() { + if (only_or_none_locked(locks, this)) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + locked_states[i] = df::global::d_init->announcements.flags[i].bits.PAUSE; + } + return true; + } + return false; +} + +void AnnouncementLock::lock() { + Lock::lock(); + captureState(); +} + +bool AnnouncementLock::isAnyLocked() const { + return any_lock(locks); +} + +bool AnnouncementLock::isOnlyLocked() const { + return only_lock(locks, this); +} + +bool PlayerLock::isAnyLocked() const { + return any_lock(locks); +} + +bool PlayerLock::isOnlyLocked() const { + return only_lock(locks, this); +} + +bool World::DisableAnnouncementPausing() { + if (!announcementLock.isAnyLocked()) { + for (auto& flag : df::global::d_init->announcements.flags) { + flag.bits.PAUSE = false; + } + announcements_disabled = true; + } + return announcements_disabled; +} + +bool World::SaveAnnouncementSettings() { + if (!announcementLock.isAnyLocked()) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + saved_states[i] = df::global::d_init->announcements.flags[i].bits.PAUSE; + } + return true; + } + return false; +} + +bool World::RestoreAnnouncementSettings() { + if (!announcementLock.isAnyLocked() && state_saved) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + df::global::d_init->announcements.flags[i].bits.PAUSE = saved_states[i]; + } + announcements_disabled = false; + return true; + } + return false; +} + +bool World::EnablePlayerPausing() { + if (!playerLock.isAnyLocked()) { + allow_player_pause = true; + } + return allow_player_pause; +} + +bool World::DisablePlayerPausing() { + if (!playerLock.isAnyLocked()) { + allow_player_pause = false; + } + return !allow_player_pause; +} + +void World::Update() { + static bool did_once = false; + if (!did_once) { + did_once = true; + INTERPOSE_HOOK(player_pause_hook, feed).apply(); + } + if (announcementLock.isAnyLocked()) { + for (size_t i = 0; i < announcement_flag_arr_size; ++i) { + df::global::d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; + } + } +} \ No newline at end of file diff --git a/plugins/spectate/pause.h b/plugins/spectate/pause.h new file mode 100644 index 000000000..dafc259d1 --- /dev/null +++ b/plugins/spectate/pause.h @@ -0,0 +1,58 @@ +#pragma once +#include +#include + +namespace DFHack { + //////////// + // Locking mechanisms for control over pausing + namespace Pausing + { + class Lock + { + bool locked = false; + std::string name; + public: + explicit Lock(const char* name) { this->name = name;}; + virtual ~Lock()= default; + virtual bool isAnyLocked() const = 0; + virtual bool isOnlyLocked() const = 0; + bool isLocked() const { return locked; } + virtual void lock() { locked = true; }; //simply locks the lock + void unlock() { locked = false; }; + }; + + // non-blocking lock resource used in conjunction with the announcement functions in World + class AnnouncementLock : public Lock + { + static std::unordered_set locks; + public: + explicit AnnouncementLock(const char* name): Lock(name) { locks.emplace(this); } + ~AnnouncementLock() override { locks.erase(this); } + bool captureState(); // captures the state of announcement settings, iff this is the only locked lock (note it does nothing if 0 locks are engaged) + void lock() override; // locks and attempts to capture state + bool isAnyLocked() const override; // returns true if any instance of AnnouncementLock is locked + bool isOnlyLocked() const override; // returns true if locked and no other instance is locked + }; + + // non-blocking lock resource used in conjunction with the Player pause functions in World + class PlayerLock : public Lock + { + static std::unordered_set locks; + public: + explicit PlayerLock(const char* name): Lock(name) { locks.emplace(this); } + ~PlayerLock() override { locks.erase(this); } + bool isAnyLocked() const override; // returns true if any instance of PlayerLock is locked + bool isOnlyLocked() const override; // returns true if locked and no other instance is locked + }; + } + namespace World { + bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open + bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open + bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open + + bool EnablePlayerPausing(); // enable player pausing if all locks are open + bool DisablePlayerPausing(); // disable player pausing if all locks are open + + void Update(); + } +} \ No newline at end of file diff --git a/plugins/spectate.cpp b/plugins/spectate/spectate.cpp similarity index 99% rename from plugins/spectate.cpp rename to plugins/spectate/spectate.cpp index 2cf23df1d..fc305d33e 100644 --- a/plugins/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -2,6 +2,8 @@ // Created by josh on 7/28/21. // +#include "pause.h" + #include "Core.h" #include #include @@ -31,6 +33,7 @@ REQUIRE_GLOBAL(pause_state); REQUIRE_GLOBAL(d_init); using namespace DFHack; +using namespace Pausing; using namespace df::enums; void onTick(color_ostream& out, void* tick); @@ -53,6 +56,9 @@ std::set job_tracker; std::map freq; std::default_random_engine RNG; + + + #define base 0.99 static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; @@ -80,12 +86,12 @@ DFhackCExport command_result plugin_init (color_ostream &out, std::vector