diff --git a/library/Core.cpp b/library/Core.cpp index 1b3f1409f..15cab5f08 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -1911,6 +1911,9 @@ void Core::doUpdate(color_ostream &out, bool first_update) // Execute per-frame handlers onUpdate(out); + // Execute per-frame World module maintenance, after plugins and lua timers + World::Update(); + d->last_autosave_request = df::global::ui->main.autosave_request; d->was_load_save = is_load_save; diff --git a/library/include/modules/World.h b/library/include/modules/World.h index 659786ff5..5b87dd4cf 100644 --- a/library/include/modules/World.h +++ b/library/include/modules/World.h @@ -35,6 +35,7 @@ distribution. #include "Module.h" #include "modules/Persistence.h" #include +#include #include "DataDefs.h" @@ -61,6 +62,47 @@ namespace DFHack }; class DFContextShared; + //////////// + // 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; } + void lock() { locked = true; }; + 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 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 + }; + } + /** * The World module * \ingroup grp_modules @@ -73,6 +115,21 @@ namespace DFHack ///true if paused, false if not DFHACK_EXPORT void SetPauseState(bool paused); + // todo: prevent anything else from allocating/deleting? + // Acquire & Release pause locks controlling player pausing and announcement pause settings + DFHACK_EXPORT Pausing::AnnouncementLock* AcquireAnnouncementPauseLock(const char*); + DFHACK_EXPORT Pausing::PlayerLock* AcquirePlayerPauseLock(const char*); + DFHACK_EXPORT void ReleasePauseLock(Pausing::Lock*); + + DFHACK_EXPORT bool DisableAnnouncementPausing(); // disable announcement pausing if all locks are open + DFHACK_EXPORT bool SaveAnnouncementSettings(); // save current announcement pause settings if all locks are open + DFHACK_EXPORT bool RestoreAnnouncementSettings(); // restore saved announcement pause settings if all locks are open + + DFHACK_EXPORT bool EnablePlayerPausing(); // enable player pausing if all locks are open + DFHACK_EXPORT bool DisablePlayerPausing(); // disable player pausing if all locks are open + + void Update(); + DFHACK_EXPORT uint32_t ReadCurrentTick(); DFHACK_EXPORT uint32_t ReadCurrentYear(); DFHACK_EXPORT uint32_t ReadCurrentMonth(); diff --git a/library/modules/World.cpp b/library/modules/World.cpp index 12b339e95..b8e2da272 100644 --- a/library/modules/World.cpp +++ b/library/modules/World.cpp @@ -28,6 +28,7 @@ distribution. #include #include #include +#include #include using namespace std; @@ -51,8 +52,14 @@ using namespace std; #include "df/map_block.h" #include "df/block_square_event_world_constructionst.h" #include "df/viewscreen_legendsst.h" +#include "df/d_init.h" +#include "df/viewscreen_dwarfmodest.h" +#include "df/ui.h" +#include "VTableInterpose.h" +#include "PluginManager.h" using namespace DFHack; +using namespace Pausing; using namespace df::enums; using df::global::world; @@ -68,6 +75,133 @@ void World::SetPauseState(bool paused) DF_GLOBAL_VALUE(pause_state, dummy) = paused; } +std::unordered_set PlayerLock::locks; +std::unordered_set AnnouncementLock::locks; + +bool AnnouncementLock::isAnyLocked() const { + return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); +} + +bool AnnouncementLock::isOnlyLocked() const { + return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { + if (lock == this) { + return lock->isLocked(); + } + return !lock->isLocked(); + }); +} + +bool PlayerLock::isAnyLocked() const { + return std::any_of(locks.begin(), locks.end(), [](Lock* lock) { return lock->isLocked(); }); +} + +bool PlayerLock::isOnlyLocked() const { + return std::all_of(locks.begin(), locks.end(), [&](Lock* lock) { + if (lock == this) { + return lock->isLocked(); + } + return !lock->isLocked(); + }); +} + +namespace pausing { + AnnouncementLock announcementLock("monitor"); + PlayerLock playerLock("monitor"); + + const size_t array_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[array_size]; // state to restore + bool locked_states[array_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; + +AnnouncementLock* World::AcquireAnnouncementPauseLock(const char* name) { + return new AnnouncementLock(name); +} + +PlayerLock* World::AcquirePlayerPauseLock(const char* name) { + return new PlayerLock(name); +} + +void World::ReleasePauseLock(Lock* lock){ + delete lock; +} + +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 < array_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 < array_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 < array_size; ++i) { + df::global::d_init->announcements.flags[i].bits.PAUSE = locked_states[i]; + } + } +} + + uint32_t World::ReadCurrentYear() { return DF_GLOBAL_VALUE(cur_year, 0);