From 986e64aed06be13182f74dcbce8edd4babb0fb53 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Fri, 29 Sep 2023 12:27:09 +0100 Subject: [PATCH] new plugin preserve-tombs ensures that units that die keep their tomb assignments in death --- plugins/CMakeLists.txt | 1 + plugins/preserve-tombs.cpp | 243 +++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 plugins/preserve-tombs.cpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index b96606284..392a69dcc 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -144,6 +144,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) #dfhack_plugin(petcapRemover petcapRemover.cpp) #dfhack_plugin(plants plants.cpp) + dfhack_plugin(preserve-tombs preserve-tombs.cpp) dfhack_plugin(probe probe.cpp) dfhack_plugin(prospector prospector.cpp LINK_LIBRARIES lua) #dfhack_plugin(power-meter power-meter.cpp LINK_LIBRARIES lua) diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp new file mode 100644 index 000000000..857597d23 --- /dev/null +++ b/plugins/preserve-tombs.cpp @@ -0,0 +1,243 @@ +#include "Debug.h" +#include "PluginManager.h" +#include "MiscUtils.h" + +#include +#include +#include +#include +#include +#include + +#include "modules/Units.h" +#include "modules/Buildings.h" +#include "modules/Persistence.h" +#include "modules/EventManager.h" +#include "modules/World.h" + +#include "df/world.h" +#include "df/unit.h" +#include "df/building.h" +#include "df/building_civzonest.h" + +using namespace DFHack; +using namespace df::enums; + + +// +DFHACK_PLUGIN("preserve-tombs"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(world); + + +static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; +static PersistentDataItem config; + +static int32_t cycle_timestamp; +static int32_t cycle_freq; + +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_CYCLES = 1 +}; + +static std::unordered_map tomb_assignments; + +namespace DFHack { + DBG_DECLARE(preservetombs, config, DebugCategory::LINFO); +} + + +static int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +static bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +static void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +static void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); +} + +static bool assign_to_tomb(int32_t unit_id, int32_t building_id); +static void update_tomb_assignments(color_ostream& out); +void onUnitDeath(color_ostream& out, void* ptr); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + return CR_OK; +} + +// event listener +EventManager::EventHandler assign_tomb_handler(onUnitDeath, 0); + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(config,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + EventManager::registerListener(EventManager::EventType::UNIT_DEATH, assign_tomb_handler, plugin_self); + if (enable) + update_tomb_assignments(out); + } else { + EventManager::unregisterAll(plugin_self); + DEBUG(config,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(config,out).print("shutting down %s\n", plugin_name); + + return CR_OK; +} + +DFhackCExport command_result plugin_load_data (color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(config,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + set_config_val(config, CONFIG_CYCLES, 25); + } + + is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); + cycle_freq = get_config_val(config, CONFIG_CYCLES); + DEBUG(config,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(config,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + } + } + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= cycle_freq) + update_tomb_assignments(out); + return CR_OK; +} +// + + + + +// On unit death - check if we assigned them a tomb +// +// +void onUnitDeath(color_ostream& out, void* ptr) { + // input is void* that contains the unit id + int32_t unit_id = reinterpret_cast(ptr); + + // check if unit was assigned a tomb in life + auto it = tomb_assignments.find(unit_id); + if (it == tomb_assignments.end()) return; + + // assign that unit to their previously assigned tomb in life + int32_t building_id = it->second; + if (!assign_to_tomb(unit_id, building_id)) return; + + // success, print status update and remove assignment from our memo-list + out.print("Unit %d died - assigning them to tomb %d\n", unit_id, building_id); + tomb_assignments.erase(it); + +} + + +// Update tomb assignments +// +// +static void update_tomb_assignments(color_ostream &out) { + + // check tomb civzones for assigned units + for (auto* bld : world->buildings.other.ZONE_TOMB) { + + auto* tomb = virtual_cast(bld); + if (!tomb || !tomb->flags.bits.exists) continue; + if (!tomb->assigned_unit) continue; + if (Units::isDead(tomb->assigned_unit)) continue; // we only care about living units + + auto it = tomb_assignments.find(tomb->assigned_unit_id); + + if (it == tomb_assignments.end()) { + tomb_assignments.emplace(tomb->assigned_unit_id, tomb->id); + out.print("%s new tomb assignment, unit %d to tomb %d\n", plugin_name, tomb->assigned_unit_id, tomb->id); + } + + else { + if (it->second != tomb->id) { + out.print("%s tomb assignment to %d changed, (old: %d, new: %d)\n", plugin_name, tomb->assigned_unit_id, it->second, tomb->id); + } + it->second = tomb->id; + } + + } + + // now check our civzones for unassignment / deleted zone / + std::erase_if(tomb_assignments, [&](const std::pair& pair){ + const auto &[unit_id, building_id] = pair; + + const size_t tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); + if (tomb_idx == -1) { + out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); + return true; + } + const auto tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); + if (!tomb || !tomb->flags.bits.exists) { + out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); + return true; + } + if (tomb->assigned_unit_id != unit_id) { + out.print("%s unassigned unit %d from tomb %d - removing\n", unit_id, building_id); + return true; + } + + return false; + }); + +} + + +// ASSIGN UNIT TO TOMB +// +// +static bool assign_to_tomb(int32_t unit_id, int32_t building_id) { + + size_t unit_idx = Units::findIndexById(unit_id); + if (unit_idx == -1) return false; + + df::unit* unit = world->units.all[unit_idx]; + if (!Units::isDead(unit)) return false; + + size_t tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); + if (tomb_idx == -1) return false; + + df::building_civzonest* tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); + if (!tomb || tomb->assigned_unit) return false; // in the game we cannot reassign tombs - more research is required to see if reassignment is safe. + + Buildings::setOwner(tomb, unit); + return true; +} \ No newline at end of file