From f99512d60cc3bea261ab5ed04a97908ec5e2dbcd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Jun 2024 23:51:05 -0700 Subject: [PATCH 01/11] initial skeleton for preserve-rooms --- plugins/CMakeLists.txt | 2 +- plugins/lua/preserve-tombs.lua | 72 ++++++++++++++++++++++++++++++++++ plugins/preserve-tombs.cpp | 1 - 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 plugins/lua/preserve-tombs.lua diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index a76bd79332..56682033a0 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -142,7 +142,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) dfhack_plugin(pet-uncapper pet-uncapper.cpp) dfhack_plugin(plant plant.cpp LINK_LIBRARIES lua) - dfhack_plugin(preserve-tombs preserve-tombs.cpp) + dfhack_plugin(preserve-tombs preserve-tombs.cpp LINK_LIBRARIES lua) dfhack_plugin(probe probe.cpp LINK_LIBRARIES lua) dfhack_plugin(prospector prospector.cpp LINK_LIBRARIES lua) #dfhack_plugin(power-meter power-meter.cpp LINK_LIBRARIES lua) diff --git a/plugins/lua/preserve-tombs.lua b/plugins/lua/preserve-tombs.lua new file mode 100644 index 0000000000..2113788556 --- /dev/null +++ b/plugins/lua/preserve-tombs.lua @@ -0,0 +1,72 @@ +local _ENV = mkmodule('plugins.preserve-tombs') + +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +---------------------- +-- BadgeWidget +-- + +BadgeWidget = defclass(BadgeWidget, overlay.OverlayWidget) +BadgeWidget.ATTRS{ + desc='Shows an indicator that a zone is managed by preserve-rooms.', + default_enabled=true, + default_pos={x=30, y=10}, + viewscreens={ + 'dwarfmode/Zone/Some/Office', + }, + frame={w=40, h=3}, + frame_style=gui.FRAME_MEDIUM, +} + +function BadgeWidget:init() + self:addviews{ + widgets.Label{ + frame={l=0, t=0}, + text={ + 'Autoassigned to role:', + {gap=1, text=get_current_role_assignment, pen=COLOR_MAGENTA}, + }, + }, + } +end + +function BadgeWidget:render(dc) + if not has_current_role_assignment() then + return + end + BadgetWidget.super.render(self, dc) +end + +---------------------- +-- RoleAssignWidget +-- + +RoleAssignWidget = defclass(RoleAssignWidget, overlay.OverlayWidget) +RoleAssignWidget.ATTRS{ + desc='Adds a configuration dropdown for autoassigning rooms to noble roles.', + default_enabled=true, + default_pos={x=40, y=8}, + viewscreens={ + 'dwarfmode/UnitSelector/ZONE_OFFICE_ASSIGNMENT', + }, + frame={w=20, h=1}, +} + +function RoleAssignWidget:init() + self:addviews{ + widgets.TextLabel{ + frame={l=0, t=0, w=10}, + label='Assign to role', + key='CUSTOM_SHIFT_S', + }, + } +end + +OVERLAY_WIDGETS = { + badge=BadgeWidget, + roleassign=RoleAssignWidget, +} + +return _ENV diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index a610d81afd..b7e4abd5ec 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -1,6 +1,5 @@ #include "Debug.h" #include "PluginManager.h" -#include "MiscUtils.h" #include "modules/Units.h" #include "modules/Buildings.h" From f13466861bf3d8a3640b741074abe1cfdfe57bdf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 23 Aug 2024 17:18:21 -0700 Subject: [PATCH 02/11] more skeleton --- plugins/CMakeLists.txt | 1 + plugins/preserve-rooms.cpp | 139 +++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 plugins/preserve-rooms.cpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 56682033a0..2ece3630a7 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -142,6 +142,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) dfhack_plugin(pet-uncapper pet-uncapper.cpp) dfhack_plugin(plant plant.cpp LINK_LIBRARIES lua) + dfhack_plugin(preserve-rooms preserve-rooms.cpp LINK_LIBRARIES lua) dfhack_plugin(preserve-tombs preserve-tombs.cpp LINK_LIBRARIES lua) dfhack_plugin(probe probe.cpp LINK_LIBRARIES lua) dfhack_plugin(prospector prospector.cpp LINK_LIBRARIES lua) diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp new file mode 100644 index 0000000000..949b4abdd7 --- /dev/null +++ b/plugins/preserve-rooms.cpp @@ -0,0 +1,139 @@ +#include "Core.h" +#include "Debug.h" +#include "PluginManager.h" + +#include "modules/World.h" + +#include "df/world.h" + +#include +#include +#include + +using std::string; +using std::unordered_map; +using std::vector; + +using namespace DFHack; + +DFHACK_PLUGIN("preserve-rooms"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(world); + +namespace DFHack { + DBG_DECLARE(persistent_per_save_example, control, DebugCategory::LINFO); + DBG_DECLARE(persistent_per_save_example, cycle, DebugCategory::LINFO); +} + +static const string CONFIG_KEY = string(plugin_name) + "/config"; +static PersistentDataItem config; + +static std::map last_known_assignments; +static std::map> pending_reassignment; + +enum ConfigValues { + CONFIG_IS_ENABLED = 0, +}; + +static const int32_t CYCLE_TICKS = 109; +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle + +static command_result do_command(color_ostream &out, vector ¶meters); +static void do_cycle(color_ostream &out); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(control,out).print("initializing %s\n", plugin_name); + commands.push_back(PluginCommand( + plugin_name, + "Prevent room assignments from being lost.", + do_command)); + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!World::isFortressMode() || !Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { + out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(control,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + config.set_bool(CONFIG_IS_ENABLED, is_enabled); + if (enable) + do_cycle(out); + } else { + DEBUG(control,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(control,out).print("shutting down %s\n", plugin_name); + + return CR_OK; +} + +DFhackCExport command_result plugin_load_site_data (color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentSiteData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(control,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentSiteData(CONFIG_KEY); + config.set_bool(CONFIG_IS_ENABLED, is_enabled); + } + + is_enabled = config.get_bool(CONFIG_IS_ENABLED); + DEBUG(control,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(control,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + } + } + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (world->frame_counter - cycle_timestamp >= CYCLE_TICKS) + do_cycle(out); + return CR_OK; +} + +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + if (!World::isFortressMode() || !Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { + out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); + return CR_FAILURE; + } + + // TODO: configuration logic + // simple commandline parsing can be done in C++, but there are lua libraries + // that can easily handle more complex commandlines. see the seedwatch plugin + // for a simple example. + + return CR_OK; +} + +///////////////////////////////////////////////////// +// cycle logic +// + +static void do_cycle(color_ostream &out) { + cycle_timestamp = world->frame_counter; + + DEBUG(cycle,out).print("running %s cycle\n", plugin_name); +} From 92a979f3dd3ac57ca09f3f0ee4fa9d80dda95f6e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 25 Aug 2024 14:50:52 -0700 Subject: [PATCH 03/11] move logic to preserve-rooms --- plugins/CMakeLists.txt | 2 +- plugins/lua/{preserve-tombs.lua => preserve-rooms.lua} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename plugins/lua/{preserve-tombs.lua => preserve-rooms.lua} (100%) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 2ece3630a7..f0b35ea9ef 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -143,7 +143,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(pet-uncapper pet-uncapper.cpp) dfhack_plugin(plant plant.cpp LINK_LIBRARIES lua) dfhack_plugin(preserve-rooms preserve-rooms.cpp LINK_LIBRARIES lua) - dfhack_plugin(preserve-tombs preserve-tombs.cpp LINK_LIBRARIES lua) + dfhack_plugin(preserve-tombs preserve-tombs.cpp) dfhack_plugin(probe probe.cpp LINK_LIBRARIES lua) dfhack_plugin(prospector prospector.cpp LINK_LIBRARIES lua) #dfhack_plugin(power-meter power-meter.cpp LINK_LIBRARIES lua) diff --git a/plugins/lua/preserve-tombs.lua b/plugins/lua/preserve-rooms.lua similarity index 100% rename from plugins/lua/preserve-tombs.lua rename to plugins/lua/preserve-rooms.lua From a1be28eceeec92827266fb6ae4d4037a95fdbb7e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 27 Aug 2024 02:42:58 -0700 Subject: [PATCH 04/11] finish skeleton --- data/init/dfhack.tools.init | 1 + docs/plugins/preserve-rooms.rst | 73 +++++++++++++++++++ plugins/lua/preserve-rooms.lua | 70 ++++++++++++++++--- plugins/preserve-rooms.cpp | 120 +++++++++++++++++++++++--------- 4 files changed, 224 insertions(+), 40 deletions(-) create mode 100644 docs/plugins/preserve-rooms.rst diff --git a/data/init/dfhack.tools.init b/data/init/dfhack.tools.init index 8087c2af1c..440fcac3c9 100644 --- a/data/init/dfhack.tools.init +++ b/data/init/dfhack.tools.init @@ -10,6 +10,7 @@ enable burrow enable faststart enable logistics enable overlay +enable preserve-rooms # aliases alias add autounsuspend suspendmanager diff --git a/docs/plugins/preserve-rooms.rst b/docs/plugins/preserve-rooms.rst new file mode 100644 index 0000000000..5ef4e48e60 --- /dev/null +++ b/docs/plugins/preserve-rooms.rst @@ -0,0 +1,73 @@ +preserve-rooms +============== + +.. dfhack-tool:: + :summary: Manage room assignments for off-map units and noble roles. + :tags: fort bugfix interface + +When a citizen leaves the map for any reason, e.g. when going off on a raid, +they lose all their zone assignments. Any bedrooms, offices, dining rooms, and +tombs assigned to the citizen will become unassigned and may be claimed by +another unit while they are away. + +A related issue occurs when a noble role changes hands, either because you have +reassigned them or because of an internal fort election. Rooms you have +assigned to the previous noble stay assigned to them, and the new noble +complains because their room requirements are not being met. + +This tool solves both issues. It records when units leave the map and reserves +their assigned bedrooms, offices, etc. for them. The zones will be disabled in +their absence, and will be re-enabled and reassigned to them when they appear +back on the map. If they die away from the fort, the zone will become +unreserved and available for reuse. + +When you click on an assignable zone, you will also now have the option to +associate the room with a noble role. The room will be automatically reassigned +to whoever currently holds that noble position. If multiple rooms of the same +type are assigned to a position that can be filled by multiple citizens (e.g. +you can have many barons), then only one room of that type will be assigned to +each holder of that position. + +Usage +----- + +:: + + preserve-rooms [status] + preserve-rooms now + preserve-rooms enable|disable + preserve-rooms reset + +Examples +-------- + +``preserve-rooms`` + List the types of rooms that are assigned to each noble role and which of + those rooms are being automatically assigned to new holders of the + respective office. +``preserve-rooms now`` + Do an immediate update of room assignments. The plugin does this routinely + in the background, but you can run it manually to update now. +``preserve-rooms disable track-raids`` + Disable the ``track-raids`` feature for this fort. +``preserve-rooms reset noble-roles`` + Clear all configuration related to the ``noble-roles`` feature. + +Features +-------- + +``track-raids`` + Reserve the rooms assigned to units that leave the map and reassign them + upon their return. This feature is automatically enabled for new forts + unless disabled in `gui/control-panel` ("Bugfixes" tab). +``noble-roles`` + Allow rooms to be associated with noble roles. Associated rooms will be + automatically assigned to the current holder of the specified role. This + feature is enabled by default for all forts. + +Overlay +------- + +The ``preserve-rooms.reserved`` overlay indicates whether a zone is disabled +because it is being reserved for a unit that left the map and is expected to +return. It also provides widgets to mark the zone as associated with a role. diff --git a/plugins/lua/preserve-rooms.lua b/plugins/lua/preserve-rooms.lua index 2113788556..910bb88d8f 100644 --- a/plugins/lua/preserve-rooms.lua +++ b/plugins/lua/preserve-rooms.lua @@ -1,26 +1,78 @@ -local _ENV = mkmodule('plugins.preserve-tombs') +local _ENV = mkmodule('plugins.preserve-rooms') local gui = require('gui') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') +------------------ +-- command line +-- + +local function print_status() + local features = preserve_rooms_getState() + print('Features:') + for feature,enabled in pairs(features) do + print((' %20s: %s'):format(feature, enabled)) + end +end + +local function do_set_feature(enabled, feature) + if not preserve_rooms_setFeature(enabled, feature) then + qerror(('unknown feature: "%s"'):format(feature)) + end +end + +local function do_reset_feature(feature) + if not preserve_rooms_resetFeatureState(feature) then + qerror(('unknown feature: "%s"'):format(feature)) + end +end + +function parse_commandline(args) + local opts = {} + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() opts.help = true end}, + }) + + if opts.help or not positionals or positionals[1] == 'help' then + return false + end + + local command = table.remove(positionals, 1) + if not command or command == 'status' then + print_status() + elseif command == 'now' then + preserve_rooms_cycle() + elseif command == 'enable' or command == 'disable' then + do_set_feature(command == 'enable', positionals[1]) + elseif command == 'reset' then + do_reset_feature(positionals[1]) + else + return false + end + + return true +end + ---------------------- --- BadgeWidget +-- ReservedWidget -- -BadgeWidget = defclass(BadgeWidget, overlay.OverlayWidget) -BadgeWidget.ATTRS{ - desc='Shows an indicator that a zone is managed by preserve-rooms.', +ReservedWidget = defclass(ReservedWidget, overlay.OverlayWidget) +ReservedWidget.ATTRS{ + desc='Shows whether a zone has been reserved for a unit or role.', default_enabled=true, default_pos={x=30, y=10}, viewscreens={ + 'dwarfmode/Zone/Some/Bedroom', + 'dwarfmode/Zone/Some/DiningHall', 'dwarfmode/Zone/Some/Office', + 'dwarfmode/Zone/Some/Tomb', }, frame={w=40, h=3}, - frame_style=gui.FRAME_MEDIUM, } -function BadgeWidget:init() +function ReservedWidget:init() self:addviews{ widgets.Label{ frame={l=0, t=0}, @@ -32,7 +84,7 @@ function BadgeWidget:init() } end -function BadgeWidget:render(dc) +function ReservedWidget:render(dc) if not has_current_role_assignment() then return end @@ -65,7 +117,7 @@ function RoleAssignWidget:init() end OVERLAY_WIDGETS = { - badge=BadgeWidget, + badge=ReservedWidget, roleassign=RoleAssignWidget, } diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index 949b4abdd7..ef1d6ec5b8 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -1,5 +1,6 @@ #include "Core.h" #include "Debug.h" +#include "LuaTools.h" #include "PluginManager.h" #include "modules/World.h" @@ -29,11 +30,15 @@ namespace DFHack { static const string CONFIG_KEY = string(plugin_name) + "/config"; static PersistentDataItem config; -static std::map last_known_assignments; +// zone id -> unit ids (includes spouses) +static std::map> last_known_assignments; +// unit id -> zone ids static std::map> pending_reassignment; +// as a "system" plugin, we do not persist plugin enabled state enum ConfigValues { - CONFIG_IS_ENABLED = 0, + CONFIG_TRACK_RAIDS = 0, + CONFIG_NOBLE_ROLES = 1, }; static const int32_t CYCLE_TICKS = 109; @@ -46,35 +51,19 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector frame_counter - cycle_timestamp >= CYCLE_TICKS) do_cycle(out); return CR_OK; @@ -115,17 +106,20 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { static command_result do_command(color_ostream &out, vector ¶meters) { CoreSuspender suspend; - if (!World::isFortressMode() || !Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { + if (!World::isFortressMode() || !Core::getInstance().isMapLoaded()) { out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); return CR_FAILURE; } - // TODO: configuration logic - // simple commandline parsing can be done in C++, but there are lua libraries - // that can easily handle more complex commandlines. see the seedwatch plugin - // for a simple example. + bool show_help = false; + if (!Lua::CallLuaModuleFunction(out, "plugins.preserve-rooms", "parse_commandline", std::make_tuple(parameters), + 1, [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } - return CR_OK; + return show_help ? CR_WRONG_USAGE : CR_OK; } ///////////////////////////////////////////////////// @@ -137,3 +131,67 @@ static void do_cycle(color_ostream &out) { DEBUG(cycle,out).print("running %s cycle\n", plugin_name); } + +///////////////////////////////////////////////////// +// Lua API +// + +static void preserve_rooms_cycle(color_ostream &out) { + DEBUG(control,out).print("entering preserve_rooms_cycle\n"); + do_cycle(out); +} + +static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string feature) { + DEBUG(control,out).print("entering preserve_rooms_setFeature (enabled=%d, feature=%s)\n", + enabled, feature.c_str()); + if (feature == "track-raids") { + config.set_bool(CONFIG_TRACK_RAIDS, enabled); + if (is_enabled && enabled) + do_cycle(out); + } else if (feature == "noble-roles") { + config.set_bool(CONFIG_NOBLE_ROLES, enabled); + } else { + return false; + } + + return true; +} + +static bool preserve_rooms_resetFeatureState(color_ostream &out, string feature) { + DEBUG(control,out).print("entering preserve_rooms_resetFeatureState (feature=%s)\n", feature.c_str()); + if (feature == "track-raids") { + // TODO: unpause reserved rooms + last_known_assignments.clear(); + pending_reassignment.clear(); + } else if (feature == "noble-roles") { + // TODO: reset state + } else { + return false; + } + + return true; +} + +static int preserve_rooms_getState(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(control,*out).print("entering preserve_rooms_getState\n"); + + unordered_map features; + features.emplace("track-raids", config.get_bool(CONFIG_TRACK_RAIDS)); + features.emplace("noble-roles", config.get_bool(CONFIG_NOBLE_ROLES)); + Lua::Push(L, features); + + return 1; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS{ + DFHACK_LUA_FUNCTION(preserve_rooms_cycle), + DFHACK_LUA_FUNCTION(preserve_rooms_setFeature), + DFHACK_LUA_FUNCTION(preserve_rooms_resetFeatureState), + DFHACK_LUA_END}; + +DFHACK_PLUGIN_LUA_COMMANDS{ + DFHACK_LUA_COMMAND(preserve_rooms_getState), + DFHACK_LUA_END}; From d1a7d9c35841e119e22abe4383077c800d5465a0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 27 Aug 2024 02:43:11 -0700 Subject: [PATCH 05/11] minor cleanup --- plugins/logistics.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index 724f0d00e3..5c9f1fdac8 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -605,7 +605,7 @@ static int logistics_getStockpileData(lua_State *L) { ProcessorStats melt_stats, trade_stats, dump_stats, train_stats, forbid_stats, claim_stats; - for (auto bld : df::global::world->buildings.other.STOCKPILE) { + for (auto bld : world->buildings.other.STOCKPILE) { int32_t stockpile_number = bld->stockpile_number; MeltStockProcessor melt_stock_processor(stockpile_number, false, melt_stats, false); TradeStockProcessor trade_stock_processor(stockpile_number, false, trade_stats); @@ -796,7 +796,7 @@ static int logistics_getGlobalCounts(lua_State *L) { out = &Core::getInstance().getConsole(); DEBUG(control,*out).print("entering logistics_getGlobalCounts\n"); - size_t num_melt = df::global::world->items.other.ANY_MELT_DESIGNATED.size(); + size_t num_melt = world->items.other.ANY_MELT_DESIGNATED.size(); size_t num_trade = 0; for (auto link = world->jobs.list.next; link; link = link->next) { From 734821c7c3ee1ffaa8b6421493c42e736e363d1c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 27 Aug 2024 13:26:25 -0700 Subject: [PATCH 06/11] basic functionality for role assignment --- docs/plugins/preserve-rooms.rst | 53 ++++---- plugins/lua/preserve-rooms.lua | 195 +++++++++++++++++++++++----- plugins/preserve-rooms.cpp | 220 ++++++++++++++++++++++++++++---- 3 files changed, 384 insertions(+), 84 deletions(-) diff --git a/docs/plugins/preserve-rooms.rst b/docs/plugins/preserve-rooms.rst index 5ef4e48e60..af03a8d796 100644 --- a/docs/plugins/preserve-rooms.rst +++ b/docs/plugins/preserve-rooms.rst @@ -6,27 +6,27 @@ preserve-rooms :tags: fort bugfix interface When a citizen leaves the map for any reason, e.g. when going off on a raid, -they lose all their zone assignments. Any bedrooms, offices, dining rooms, and +they lose all their zone assignments. Any bedrooms, offices, dining rooms, or tombs assigned to the citizen will become unassigned and may be claimed by another unit while they are away. -A related issue occurs when a noble role changes hands, either because you have -reassigned them or because of an internal fort election. Rooms you have -assigned to the previous noble stay assigned to them, and the new noble -complains because their room requirements are not being met. +A related issue occurs when a noble or administrative role changes hands, +either because you have reassigned them or because of an internal fort +election. Rooms you have assigned to the previous noble stay assigned to them, +and the new noble complains because their room requirements are not being met. -This tool solves both issues. It records when units leave the map and reserves -their assigned bedrooms, offices, etc. for them. The zones will be disabled in -their absence, and will be re-enabled and reassigned to them when they appear -back on the map. If they die away from the fort, the zone will become -unreserved and available for reuse. +This tool mitigates both issues. It records when units leave the map and +reserves their assigned bedrooms, offices, etc. for them. The zones will be +disabled in their absence (so other units don't steal them), and will be +re-enabled and reassigned to them when they appear back on the map. If they die +away from the fort, the zone will become unreserved and available for reuse. When you click on an assignable zone, you will also now have the option to -associate the room with a noble role. The room will be automatically reassigned -to whoever currently holds that noble position. If multiple rooms of the same -type are assigned to a position that can be filled by multiple citizens (e.g. -you can have many barons), then only one room of that type will be assigned to -each holder of that position. +associate the room with a noble or administrative role. The room will be +automatically reassigned to whoever currently holds that position. If multiple +rooms of the same type are assigned to a position, then only one room of that +type will be assigned to each holder of that position (e.g. one room per baron +or militia captain). Usage ----- @@ -48,26 +48,27 @@ Examples ``preserve-rooms now`` Do an immediate update of room assignments. The plugin does this routinely in the background, but you can run it manually to update now. -``preserve-rooms disable track-raids`` - Disable the ``track-raids`` feature for this fort. -``preserve-rooms reset noble-roles`` - Clear all configuration related to the ``noble-roles`` feature. +``preserve-rooms disable track-missions`` + Disable the ``track-missions`` feature for this fort. +``preserve-rooms reset track-roles`` + Clear all configuration related to the ``track-roles`` feature. Features -------- -``track-raids`` +``track-missions`` Reserve the rooms assigned to units that leave the map and reassign them upon their return. This feature is automatically enabled for new forts - unless disabled in `gui/control-panel` ("Bugfixes" tab). -``noble-roles`` - Allow rooms to be associated with noble roles. Associated rooms will be - automatically assigned to the current holder of the specified role. This - feature is enabled by default for all forts. + unless disabled in `gui/control-panel` ("Bugfixes" / "Autostart" tab). +``track-roles`` + Allow rooms to be associated with noble or adminstrative roles. Associated + rooms will be automatically assigned to the current holder of the specified + role. This feature is enabled by default for all forts. Overlay ------- The ``preserve-rooms.reserved`` overlay indicates whether a zone is disabled because it is being reserved for a unit that left the map and is expected to -return. It also provides widgets to mark the zone as associated with a role. +return. For unreserved rooms, it provides widgets to mark the zone as +associated with a noble or administrative role. diff --git a/plugins/lua/preserve-rooms.lua b/plugins/lua/preserve-rooms.lua index 910bb88d8f..16efb079b5 100644 --- a/plugins/lua/preserve-rooms.lua +++ b/plugins/lua/preserve-rooms.lua @@ -1,9 +1,12 @@ local _ENV = mkmodule('plugins.preserve-rooms') +local argparse = require('argparse') local gui = require('gui') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') +local GLOBAL_KEY = 'preserve-rooms' + ------------------ -- command line -- @@ -62,63 +65,189 @@ ReservedWidget = defclass(ReservedWidget, overlay.OverlayWidget) ReservedWidget.ATTRS{ desc='Shows whether a zone has been reserved for a unit or role.', default_enabled=true, - default_pos={x=30, y=10}, + default_pos={x=37, y=9}, viewscreens={ 'dwarfmode/Zone/Some/Bedroom', 'dwarfmode/Zone/Some/DiningHall', 'dwarfmode/Zone/Some/Office', 'dwarfmode/Zone/Some/Tomb', }, - frame={w=40, h=3}, + frame={w=44, h=15}, } +local new_world_loaded = true + function ReservedWidget:init() + self.code_to_idx = {} + self:addviews{ - widgets.Label{ - frame={l=0, t=0}, - text={ - 'Autoassigned to role:', - {gap=1, text=get_current_role_assignment, pen=COLOR_MAGENTA}, + widgets.Panel{ + view_id='pause_mask', + frame={t=0, l=0, w=4, h=3}, + }, + widgets.Panel{ + view_id='add_mask', + frame={t=3, l=4, w=4, h=3}, + }, + widgets.Panel{ + frame={t=0, l=9}, + visible=function() + local scr = dfhack.gui.getDFViewscreen(true) + return not dfhack.gui.matchFocusString('dwarfmode/UnitSelector', scr) and + not dfhack.gui.matchFocusString('dwarfmode/LocationSelector', scr) + end, + subviews={ + widgets.Panel{ + visible=function() return not preserve_rooms_isReserved() end, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='role', + frame={t=1, l=1, r=1, h=2}, + frame_background=gui.CLEAR_PEN, + label='Autoassign to holder of role:', + label_below=true, + key='CUSTOM_SHIFT_S', + options={{label='None', value='', pen=COLOR_YELLOW}}, + on_change=function(code) + self.subviews.list:setSelected(self.code_to_idx[code] or 1) + preserve_rooms_assignToRole(code) + end, + }, + widgets.Panel{ + view_id='hover_trigger', + frame={t=0, l=0, r=0, h=4}, + frame_style=gui.FRAME_MEDIUM, + visible=true, + }, + widgets.Panel{ + view_id='hover_expansion', + frame_style=gui.FRAME_MEDIUM, + visible=false, + subviews={ + widgets.Panel{ + frame={t=2}, + frame_background=gui.CLEAR_PEN, + }, + widgets.List{ + view_id='list', + frame={t=3, l=1}, + frame_background=gui.CLEAR_PEN, + on_submit=function(idx) + self.subviews.role:setOption(idx, true) + end, + choices={'None'}, + }, + }, + }, + }, + }, + widgets.Panel{ + frame={h=5}, + frame_style=gui.FRAME_MEDIUM, + frame_background=gui.CLEAR_PEN, + visible=preserve_rooms_isReserved, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Reserved for traveling unit:', NEWLINE, + {text=preserve_rooms_getReservationName, pen=COLOR_YELLOW}, + }, + }, + widgets.HotkeyLabel{ + frame={t=2, l=0}, + key='CUSTOM_SHIFT_R', + label='Clear reservation', + on_activate=preserve_rooms_clearReservation, + }, + }, + }, + widgets.HelpButton{ + command='preserve-rooms', + }, }, }, } end +function ReservedWidget:onInput(keys) + if ReservedWidget.super.onInput(self, keys) then + return true + end + if keys._MOUSE_L and preserve_rooms_isReserved() then + if self.subviews.pause_mask:getMousePos() then return true end + if self.subviews.add_mask:getMousePos() then return true end + end +end + function ReservedWidget:render(dc) - if not has_current_role_assignment() then - return + if new_world_loaded then + self:refresh_role_list() + new_world_loaded = false end - BadgetWidget.super.render(self, dc) + + local code = preserve_rooms_getRoleAssignment() + local role = self.subviews.role + if code ~= role:getOptionValue() then + role:setOption(code) + self.subviews.list:setSelected(self.code_to_idx[code] or 1) + end + + local hover_expansion = self.subviews.hover_expansion + if hover_expansion.visible and not hover_expansion:getMouseFramePos() then + hover_expansion.visible = false + elseif self.subviews.hover_trigger:getMousePos() then + hover_expansion.visible = true + end + + ReservedWidget.super.render(self, dc) end ----------------------- --- RoleAssignWidget --- +local function to_title_case(str) + return dfhack.capitalizeStringWords(dfhack.lowerCp437(str:gsub('_', ' '))) +end -RoleAssignWidget = defclass(RoleAssignWidget, overlay.OverlayWidget) -RoleAssignWidget.ATTRS{ - desc='Adds a configuration dropdown for autoassigning rooms to noble roles.', - default_enabled=true, - default_pos={x=40, y=8}, - viewscreens={ - 'dwarfmode/UnitSelector/ZONE_OFFICE_ASSIGNMENT', - }, - frame={w=20, h=1}, -} +local function add_codes(codes, entity) + if not entity then return end + for _,role in ipairs(entity.positions.own) do + table.insert(codes, role.code) + end +end -function RoleAssignWidget:init() - self:addviews{ - widgets.TextLabel{ - frame={l=0, t=0, w=10}, - label='Assign to role', - key='CUSTOM_SHIFT_S', - }, - } +local function add_options(options, choices, codes) + for _,code in ipairs(codes) do + local name = to_title_case(code) + table.insert(options, {label=name, value=code, pen=COLOR_YELLOW}) + table.insert(choices, name) + end +end + +function ReservedWidget:refresh_role_list() + local codes, options, choices = {}, {{label='None', value='', pen=COLOR_YELLOW}}, {'None'} + add_codes(codes, df.historical_entity.find(df.global.plotinfo.civ_id)); + add_codes(codes, df.historical_entity.find(df.global.plotinfo.group_id)); + table.sort(codes) + add_options(options, choices, codes) + + self.code_to_idx = {['']=1} + for idx,code in ipairs(codes) do + self.code_to_idx[code] = idx + 1 -- offset for None option + end + + self.subviews.role.options = options + self.subviews.role:setOption(1) + self.subviews.list:setChoices(choices, 1) end OVERLAY_WIDGETS = { - badge=ReservedWidget, - roleassign=RoleAssignWidget, + reserved=ReservedWidget, } +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end + new_world_loaded = true +end + return _ENV diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index ef1d6ec5b8..db579fa998 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -3,14 +3,22 @@ #include "LuaTools.h" #include "PluginManager.h" +#include "modules/Buildings.h" +#include "modules/Gui.h" +#include "modules/Translation.h" +#include "modules/Units.h" #include "modules/World.h" +#include "df/building_civzonest.h" +#include "df/historical_figure.h" +#include "df/unit.h" #include "df/world.h" #include #include #include +using std::pair; using std::string; using std::unordered_map; using std::vector; @@ -30,15 +38,23 @@ namespace DFHack { static const string CONFIG_KEY = string(plugin_name) + "/config"; static PersistentDataItem config; -// zone id -> unit ids (includes spouses) -static std::map> last_known_assignments; -// unit id -> zone ids -static std::map> pending_reassignment; +// zone id -> hfids (includes spouses) +static vector>> last_known_assignments_bedroom; +static vector>> last_known_assignments_office; +static vector>> last_known_assignments_dining; +static vector>> last_known_assignments_tomb; +// hfid -> zone ids +static unordered_map> pending_reassignment; +// zone id -> hfids +static unordered_map> reserved_zones; -// as a "system" plugin, we do not persist plugin enabled state +// zone id -> noble/administrative position code +static unordered_map noble_zones; + +// as a "system" plugin, we do not persist plugin enabled state, just feature enabled state enum ConfigValues { - CONFIG_TRACK_RAIDS = 0, - CONFIG_NOBLE_ROLES = 1, + CONFIG_TRACK_MISSIONS = 0, + CONFIG_TRACK_ROLES = 1, }; static const int32_t CYCLE_TICKS = 109; @@ -62,24 +78,45 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { return CR_OK; } -DFhackCExport command_result plugin_shutdown (color_ostream &out) { +DFhackCExport command_result plugin_shutdown(color_ostream &out) { DEBUG(control,out).print("shutting down %s\n", plugin_name); return CR_OK; } -DFhackCExport command_result plugin_load_site_data (color_ostream &out) { +static void clear_track_missions_state() { + last_known_assignments_bedroom.clear(); + last_known_assignments_office.clear(); + last_known_assignments_dining.clear(); + last_known_assignments_tomb.clear(); + pending_reassignment.clear(); + reserved_zones.clear(); +} + +static void clear_track_roles_state() { + noble_zones.clear(); +} + +DFhackCExport command_result plugin_load_site_data(color_ostream &out) { cycle_timestamp = 0; config = World::GetPersistentSiteData(CONFIG_KEY); if (!config.isValid()) { DEBUG(control,out).print("no config found in this save; initializing\n"); config = World::AddPersistentSiteData(CONFIG_KEY); - config.set_bool(CONFIG_TRACK_RAIDS, false); - config.set_bool(CONFIG_NOBLE_ROLES, true); + config.set_bool(CONFIG_TRACK_MISSIONS, false); + config.set_bool(CONFIG_TRACK_ROLES, true); } - last_known_assignments.clear(); - pending_reassignment.clear(); + clear_track_missions_state(); + clear_track_roles_state(); + + // TODO: restore vectors/maps from serialized state + + return CR_OK; +} + +DFhackCExport command_result plugin_save_site_data (color_ostream &out) { + // TODO: serialize vectors/maps return CR_OK; } @@ -126,10 +163,89 @@ static command_result do_command(color_ostream &out, vector ¶meters) // cycle logic // +static bool is_noble_zone(int32_t zone_id, const string & code) { + auto it = noble_zones.find(zone_id); + return it != noble_zones.end() && it->second == code; +} + +static void assign_nobles(color_ostream &out) { + for (auto &[zone_id, code] : noble_zones) { + auto zone = virtual_cast(df::building::find(zone_id)); + if (!zone) + continue; + vector units; + Units::getUnitsByNobleRole(units, code); + // if zone is already assigned to a proper unit, skip + if (linear_index(units, zone->assigned_unit) >= 0) + continue; + // assign to a relevant noble that does not already have a registered zone of this type assigned + for (auto unit : units) { + if (!Units::isCitizen(unit, true) && !Units::isResident(unit, true)) + continue; + bool found = false; + for (auto owned_zone : unit->owned_buildings) { + if (owned_zone->type == zone->type && is_noble_zone(owned_zone->id, code)) { + found = true; + break; + } + } + if (found) + continue; + Buildings::setOwner(zone, unit); + INFO(cycle,out).print("assigning %s to a %s-associated %s\n", + Translation::TranslateName(&unit->name, false).c_str(), code.c_str(), + ENUM_KEY_STR(civzone_type, zone->type).c_str()); + break; + } + } +} + +static void clear_reservation(color_ostream &out, int32_t zone_id, df::building_civzonest * zone = NULL) { + auto it = reserved_zones.find(zone_id); + if (it == reserved_zones.end()) + return; + for (auto hfid : it->second) { + auto pending_it = pending_reassignment.find(hfid); + if (pending_it != pending_reassignment.end()) { + auto & zone_ids = pending_it->second; + vector_erase_at(zone_ids, linear_index(zone_ids, zone_id)); + if (zone_ids.empty()) + pending_reassignment.erase(pending_it); + } + } + reserved_zones.erase(zone_id); + if (!zone) + zone = virtual_cast(df::building::find(zone_id)); + if (zone) + zone->spec_sub_flag.bits.active = true; +} + +static void scan_assignments(color_ostream &out, + vector>> & last_known, + const vector & vec, + bool exclude_spouse = false) +{ + // auto it = last_known.begin(); + + vector>> assignments; + // for (auto zone : vec) { + + // } + + last_known = assignments; +} + static void do_cycle(color_ostream &out) { cycle_timestamp = world->frame_counter; DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + + assign_nobles(out); + + scan_assignments(out, last_known_assignments_bedroom, world->buildings.other.ZONE_BEDROOM); + scan_assignments(out, last_known_assignments_office, world->buildings.other.ZONE_OFFICE); + scan_assignments(out, last_known_assignments_dining, world->buildings.other.ZONE_DINING_HALL); + scan_assignments(out, last_known_assignments_tomb, world->buildings.other.ZONE_TOMB, true); } ///////////////////////////////////////////////////// @@ -144,12 +260,12 @@ static void preserve_rooms_cycle(color_ostream &out) { static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string feature) { DEBUG(control,out).print("entering preserve_rooms_setFeature (enabled=%d, feature=%s)\n", enabled, feature.c_str()); - if (feature == "track-raids") { - config.set_bool(CONFIG_TRACK_RAIDS, enabled); + if (feature == "track-missions") { + config.set_bool(CONFIG_TRACK_MISSIONS, enabled); if (is_enabled && enabled) do_cycle(out); - } else if (feature == "noble-roles") { - config.set_bool(CONFIG_NOBLE_ROLES, enabled); + } else if (feature == "track-roles") { + config.set_bool(CONFIG_TRACK_ROLES, enabled); } else { return false; } @@ -159,12 +275,14 @@ static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string f static bool preserve_rooms_resetFeatureState(color_ostream &out, string feature) { DEBUG(control,out).print("entering preserve_rooms_resetFeatureState (feature=%s)\n", feature.c_str()); - if (feature == "track-raids") { - // TODO: unpause reserved rooms - last_known_assignments.clear(); - pending_reassignment.clear(); - } else if (feature == "noble-roles") { - // TODO: reset state + if (feature == "track-missions") { + vector zone_ids; + std::transform(reserved_zones.begin(), reserved_zones.end(), std::back_inserter(zone_ids), [](auto & elem){ return elem.first; }); + for (auto zone_id : zone_ids) + clear_reservation(out, zone_id); + clear_track_missions_state(); + } else if (feature == "track-roles") { + clear_track_roles_state(); } else { return false; } @@ -172,6 +290,53 @@ static bool preserve_rooms_resetFeatureState(color_ostream &out, string feature) return true; } +static void preserve_rooms_assignToRole(color_ostream &out, string code) { + auto zone = Gui::getSelectedCivZone(out, true); + if (!zone) + return; + noble_zones[zone->id] = code; + do_cycle(out); +} + +static string preserve_rooms_getRoleAssignment(color_ostream &out) { + auto zone = Gui::getSelectedCivZone(out, true); + if (!zone) + return ""; + auto it = noble_zones.find(zone->id); + if (it == noble_zones.end()) + return ""; + return it->second; +} + +static bool preserve_rooms_isReserved(color_ostream &out) { + auto zone = Gui::getSelectedCivZone(out, true); + if (!zone) + return false; + auto it = reserved_zones.find(zone->id); + return it != reserved_zones.end() && it->second.size() > 0; +} + +static string preserve_rooms_getReservationName(color_ostream &out) { + auto zone = Gui::getSelectedCivZone(out, true); + if (!zone) + return ""; + auto it = reserved_zones.find(zone->id); + if (it != reserved_zones.end() && it->second.size() > 0) { + if (auto hf = df::historical_figure::find(it->second.front())) { + return Translation::TranslateName(&hf->name, false); + } + } + return ""; +} + +static bool preserve_rooms_clearReservation(color_ostream &out) { + auto zone = Gui::getSelectedCivZone(out, true); + if (!zone) + return false; + clear_reservation(out, zone->id, zone); + return true; +} + static int preserve_rooms_getState(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) @@ -179,8 +344,8 @@ static int preserve_rooms_getState(lua_State *L) { DEBUG(control,*out).print("entering preserve_rooms_getState\n"); unordered_map features; - features.emplace("track-raids", config.get_bool(CONFIG_TRACK_RAIDS)); - features.emplace("noble-roles", config.get_bool(CONFIG_NOBLE_ROLES)); + features.emplace("track-missions", config.get_bool(CONFIG_TRACK_MISSIONS)); + features.emplace("track-roles", config.get_bool(CONFIG_TRACK_ROLES)); Lua::Push(L, features); return 1; @@ -190,6 +355,11 @@ DFHACK_PLUGIN_LUA_FUNCTIONS{ DFHACK_LUA_FUNCTION(preserve_rooms_cycle), DFHACK_LUA_FUNCTION(preserve_rooms_setFeature), DFHACK_LUA_FUNCTION(preserve_rooms_resetFeatureState), + DFHACK_LUA_FUNCTION(preserve_rooms_assignToRole), + DFHACK_LUA_FUNCTION(preserve_rooms_getRoleAssignment), + DFHACK_LUA_FUNCTION(preserve_rooms_isReserved), + DFHACK_LUA_FUNCTION(preserve_rooms_getReservationName), + DFHACK_LUA_FUNCTION(preserve_rooms_clearReservation), DFHACK_LUA_END}; DFHACK_PLUGIN_LUA_COMMANDS{ From 8ed34a59414532c5a79876615afb6c9bdcb01f40 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 27 Aug 2024 18:37:34 -0700 Subject: [PATCH 07/11] first draft of track-missions --- docs/plugins/preserve-rooms.rst | 18 +++- plugins/lua/preserve-rooms.lua | 7 +- plugins/preserve-rooms.cpp | 167 ++++++++++++++++++++++++++++---- 3 files changed, 165 insertions(+), 27 deletions(-) diff --git a/docs/plugins/preserve-rooms.rst b/docs/plugins/preserve-rooms.rst index af03a8d796..831e6f6c29 100644 --- a/docs/plugins/preserve-rooms.rst +++ b/docs/plugins/preserve-rooms.rst @@ -51,19 +51,19 @@ Examples ``preserve-rooms disable track-missions`` Disable the ``track-missions`` feature for this fort. ``preserve-rooms reset track-roles`` - Clear all configuration related to the ``track-roles`` feature. + Clear all configuration related to the ``track-roles`` feature (currently + assigned rooms are not unassigned). Features -------- ``track-missions`` Reserve the rooms assigned to units that leave the map and reassign them - upon their return. This feature is automatically enabled for new forts - unless disabled in `gui/control-panel` ("Bugfixes" / "Autostart" tab). + upon their return. This feature is enabled by default. ``track-roles`` Allow rooms to be associated with noble or adminstrative roles. Associated rooms will be automatically assigned to the current holder of the specified - role. This feature is enabled by default for all forts. + role. This feature is enabled by default. Overlay ------- @@ -71,4 +71,12 @@ Overlay The ``preserve-rooms.reserved`` overlay indicates whether a zone is disabled because it is being reserved for a unit that left the map and is expected to return. For unreserved rooms, it provides widgets to mark the zone as -associated with a noble or administrative role. +associated with a specific noble or administrative role. + +Notes +----- + +This tool fixes rooms being unassigned when a unit leaves the map. This is a +different bug from the one fixed by `preserve-tombs`, which handles the case +where a tomb is unassigned upon a unit's death, preventing them from ever being +buried in their own tomb. diff --git a/plugins/lua/preserve-rooms.lua b/plugins/lua/preserve-rooms.lua index 16efb079b5..c83e7f9a3c 100644 --- a/plugins/lua/preserve-rooms.lua +++ b/plugins/lua/preserve-rooms.lua @@ -98,7 +98,9 @@ function ReservedWidget:init() end, subviews={ widgets.Panel{ - visible=function() return not preserve_rooms_isReserved() end, + visible=function() + return not preserve_rooms_isReserved() and preserve_rooms_getFeature('track-roles') + end, subviews={ widgets.CycleHotkeyLabel{ view_id='role', @@ -164,6 +166,9 @@ function ReservedWidget:init() }, widgets.HelpButton{ command='preserve-rooms', + visible=function() + return preserve_rooms_isReserved() or preserve_rooms_getFeature('track-roles') + end, }, }, }, diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index db579fa998..d69b099f7c 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -4,6 +4,7 @@ #include "PluginManager.h" #include "modules/Buildings.h" +#include "modules/EventManager.h" #include "modules/Gui.h" #include "modules/Translation.h" #include "modules/Units.h" @@ -11,6 +12,7 @@ #include "df/building_civzonest.h" #include "df/historical_figure.h" +#include "df/histfig_hf_link.h" #include "df/unit.h" #include "df/world.h" @@ -33,16 +35,18 @@ REQUIRE_GLOBAL(world); namespace DFHack { DBG_DECLARE(persistent_per_save_example, control, DebugCategory::LINFO); DBG_DECLARE(persistent_per_save_example, cycle, DebugCategory::LINFO); + DBG_DECLARE(persistent_per_save_example, event, DebugCategory::LINFO); } static const string CONFIG_KEY = string(plugin_name) + "/config"; static PersistentDataItem config; -// zone id -> hfids (includes spouses) -static vector>> last_known_assignments_bedroom; -static vector>> last_known_assignments_office; -static vector>> last_known_assignments_dining; -static vector>> last_known_assignments_tomb; +// zone id, hfids (main, spouse) +typedef vector>> ZoneAssignments; +static ZoneAssignments last_known_assignments_bedroom; +static ZoneAssignments last_known_assignments_office; +static ZoneAssignments last_known_assignments_dining; +static ZoneAssignments last_known_assignments_tomb; // hfid -> zone ids static unordered_map> pending_reassignment; // zone id -> hfids @@ -61,6 +65,7 @@ static const int32_t CYCLE_TICKS = 109; static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle static command_result do_command(color_ostream &out, vector ¶meters); +static void on_new_active_unit(color_ostream& out, void* data); static void do_cycle(color_ostream &out); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { @@ -73,7 +78,16 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector (df::building::find(zone_id)); - if (!zone) + if (!zone || !zone->spec_sub_flag.bits.active) continue; vector units; Units::getUnitsByNobleRole(units, code); @@ -220,17 +234,76 @@ static void clear_reservation(color_ostream &out, int32_t zone_id, df::building_ zone->spec_sub_flag.bits.active = true; } -static void scan_assignments(color_ostream &out, - vector>> & last_known, - const vector & vec, - bool exclude_spouse = false) -{ - // auto it = last_known.begin(); +static int32_t get_spouse_hfid(color_ostream &out, df::historical_figure *hf) { + for (auto link : hf->histfig_links) { + if (link->getType() == df::histfig_hf_link_type::SPOUSE) + return link->target_hf; + } + return -1; +} - vector>> assignments; - // for (auto zone : vec) { +// handles when units disappear from their assignments compared to the last scan +static void handle_missing_assignments(ZoneAssignments::iterator *pit, + const ZoneAssignments::iterator & it_end, + bool share_with_spouse, + int32_t next_zone_id) +{ + for (auto & it = *pit; it != it_end && (next_zone_id == -1 || it->first <= next_zone_id); ++it) { + int32_t zone_id = it->first; + int32_t hfid = it->second.first; + int32_t spouse_hfid = it->second.second; + auto hf = df::historical_figure::find(hfid); + auto unit = df::unit::find(hf->unit_id); + if (!unit) { + // if unit data is completely gone, then they're not likely to come back + continue; + } + if (Units::isActive(unit) && !Units::isDead(unit)) { + // unit is still alive on the map; assume the unassigment was intentional/expected + continue; + } + auto zone = virtual_cast(df::building::find(zone_id)); + if (!zone) + continue; + // unit is off-screen or is dead; if we can assign room to spouse then we don't need to reserve the room + if (auto spouse_hf = df::historical_figure::find(spouse_hfid); spouse_hf && share_with_spouse) { + if (auto spouse = df::unit::find(spouse_hf->unit_id); + spouse && Units::isActive(spouse) && !Units::isDead(spouse)) + { + Buildings::setOwner(zone, spouse); + continue; + } + } + // register the hf ids for reassignment and reserve the room + // when the unit with the registered hfid returns, they will be assigned back to the zone + pending_reassignment[hfid].push_back(zone_id); + if (share_with_spouse) + pending_reassignment[spouse_hfid].push_back(zone_id); + reserved_zones[zone_id].push_back(hfid); + zone->spec_sub_flag.bits.active = false; + } +} - // } +static void process_rooms(color_ostream &out, + ZoneAssignments & last_known, + const vector & vec, + bool share_with_spouse=true) +{ + ZoneAssignments assignments; + auto it = last_known.begin(); + auto it_end = last_known.end(); + for (auto zone : vec) { + if (!zone->assigned_unit) { + handle_missing_assignments(&it, it_end, share_with_spouse, zone->id); + continue; + } + auto hf = df::historical_figure::find(zone->assigned_unit->hist_figure_id); + if (!hf) + continue; + int32_t spouse_hfid = share_with_spouse ? get_spouse_hfid(out, hf) : -1; + assignments.emplace_back(zone->id, std::make_pair(hf->id, spouse_hfid)); + } + handle_missing_assignments(&it, it_end, share_with_spouse, -1); last_known = assignments; } @@ -242,10 +315,52 @@ static void do_cycle(color_ostream &out) { assign_nobles(out); - scan_assignments(out, last_known_assignments_bedroom, world->buildings.other.ZONE_BEDROOM); - scan_assignments(out, last_known_assignments_office, world->buildings.other.ZONE_OFFICE); - scan_assignments(out, last_known_assignments_dining, world->buildings.other.ZONE_DINING_HALL); - scan_assignments(out, last_known_assignments_tomb, world->buildings.other.ZONE_TOMB, true); + process_rooms(out, last_known_assignments_bedroom, world->buildings.other.ZONE_BEDROOM); + process_rooms(out, last_known_assignments_office, world->buildings.other.ZONE_OFFICE); + process_rooms(out, last_known_assignments_dining, world->buildings.other.ZONE_DINING_HALL); + process_rooms(out, last_known_assignments_tomb, world->buildings.other.ZONE_TOMB, false); +} + +///////////////////////////////////////////////////// +// Event logic +// + +static bool spouse_has_sharable_room(color_ostream& out, int32_t hfid, df::civzone_type ztype) { + if (ztype == df::civzone_type::Tomb) + return false; + auto hf = df::historical_figure::find(hfid); + if (!hf) + return false; + auto spouse_hf = df::historical_figure::find(get_spouse_hfid(out, hf)); + if (!spouse_hf) + return false; + auto spouse = df::unit::find(spouse_hf->unit_id); + if (!spouse) + return false; + for (auto owned_zone : spouse->owned_buildings) { + if (owned_zone->type == ztype) + return true; + } + return false; +} + +static void on_new_active_unit(color_ostream& out, void* data) { + int32_t unit_id = reinterpret_cast(data); + auto unit = df::unit::find(unit_id); + if (!unit) + return; + auto hfid = unit->hist_figure_id; + auto it = pending_reassignment.find(hfid); + if (it == pending_reassignment.end()) + return; + for (auto zone_id : it->second) { + auto zone = virtual_cast(df::building::find(zone_id)); + if (!zone || zone->assigned_unit || spouse_has_sharable_room(out, hfid, zone->type)) + continue; + Buildings::setOwner(zone, unit); + zone->spec_sub_flag.bits.active = true; + } + pending_reassignment.erase(it); } ///////////////////////////////////////////////////// @@ -273,6 +388,15 @@ static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string f return true; } +static bool preserve_rooms_getFeature(color_ostream &out, string feature) { + DEBUG(control,out).print("entering preserve_rooms_getFeature (feature=%s)\n", feature.c_str()); + if (feature == "track-missions") + return config.get_bool(CONFIG_TRACK_MISSIONS); + if (feature == "track-roles") + return config.get_bool(CONFIG_TRACK_ROLES); + return false; +} + static bool preserve_rooms_resetFeatureState(color_ostream &out, string feature) { DEBUG(control,out).print("entering preserve_rooms_resetFeatureState (feature=%s)\n", feature.c_str()); if (feature == "track-missions") { @@ -354,6 +478,7 @@ static int preserve_rooms_getState(lua_State *L) { DFHACK_PLUGIN_LUA_FUNCTIONS{ DFHACK_LUA_FUNCTION(preserve_rooms_cycle), DFHACK_LUA_FUNCTION(preserve_rooms_setFeature), + DFHACK_LUA_FUNCTION(preserve_rooms_getFeature), DFHACK_LUA_FUNCTION(preserve_rooms_resetFeatureState), DFHACK_LUA_FUNCTION(preserve_rooms_assignToRole), DFHACK_LUA_FUNCTION(preserve_rooms_getRoleAssignment), From 270880ccaf9ff54c03d99628372dcd09eac99515 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 27 Aug 2024 19:28:23 -0700 Subject: [PATCH 08/11] add instrumentation --- plugins/preserve-rooms.cpp | 63 +++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index d69b099f7c..65eb4a73a4 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -6,7 +6,6 @@ #include "modules/Buildings.h" #include "modules/EventManager.h" #include "modules/Gui.h" -#include "modules/Translation.h" #include "modules/Units.h" #include "modules/World.h" @@ -33,9 +32,9 @@ DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(world); namespace DFHack { - DBG_DECLARE(persistent_per_save_example, control, DebugCategory::LINFO); - DBG_DECLARE(persistent_per_save_example, cycle, DebugCategory::LINFO); - DBG_DECLARE(persistent_per_save_example, event, DebugCategory::LINFO); + DBG_DECLARE(preserverooms, control, DebugCategory::LINFO); + DBG_DECLARE(preserverooms, cycle, DebugCategory::LINFO); + DBG_DECLARE(preserverooms, event, DebugCategory::LINFO); } static const string CONFIG_KEY = string(plugin_name) + "/config"; @@ -207,7 +206,7 @@ static void assign_nobles(color_ostream &out) { continue; Buildings::setOwner(zone, unit); INFO(cycle,out).print("assigning %s to a %s-associated %s\n", - Translation::TranslateName(&unit->name, false).c_str(), code.c_str(), + Units::getReadableName(unit).c_str(), code.c_str(), ENUM_KEY_STR(civzone_type, zone->type).c_str()); break; } @@ -243,7 +242,8 @@ static int32_t get_spouse_hfid(color_ostream &out, df::historical_figure *hf) { } // handles when units disappear from their assignments compared to the last scan -static void handle_missing_assignments(ZoneAssignments::iterator *pit, +static void handle_missing_assignments(color_ostream &out, + ZoneAssignments::iterator *pit, const ZoneAssignments::iterator & it_end, bool share_with_spouse, int32_t next_zone_id) @@ -262,23 +262,35 @@ static void handle_missing_assignments(ZoneAssignments::iterator *pit, // unit is still alive on the map; assume the unassigment was intentional/expected continue; } + if (!Units::isCitizen(unit, true) && !Units::isResident(unit, true)) + continue; auto zone = virtual_cast(df::building::find(zone_id)); if (!zone) continue; - // unit is off-screen or is dead; if we can assign room to spouse then we don't need to reserve the room + // unit is off-map or is dead; if we can assign room to spouse then we don't need to reserve the room if (auto spouse_hf = df::historical_figure::find(spouse_hfid); spouse_hf && share_with_spouse) { if (auto spouse = df::unit::find(spouse_hf->unit_id); spouse && Units::isActive(spouse) && !Units::isDead(spouse)) { + DEBUG(cycle,out).print("assigning zone %d (%s) to spouse %s\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), + Units::getReadableName(spouse).c_str()); Buildings::setOwner(zone, spouse); continue; } } + if (Units::isDead(unit)) + continue; // register the hf ids for reassignment and reserve the room - // when the unit with the registered hfid returns, they will be assigned back to the zone + DEBUG(cycle,out).print("registering primary unit for reassignment to zone %d (%s): %d %s\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), unit->id, + Units::getReadableName(unit).c_str()); pending_reassignment[hfid].push_back(zone_id); - if (share_with_spouse) + if (share_with_spouse) { + DEBUG(cycle,out).print("registering spouse unit for reassignment to zone %d (%s): hfid=%d\n", + zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), spouse_hfid); pending_reassignment[spouse_hfid].push_back(zone_id); + } reserved_zones[zone_id].push_back(hfid); zone->spec_sub_flag.bits.active = false; } @@ -294,7 +306,7 @@ static void process_rooms(color_ostream &out, auto it_end = last_known.end(); for (auto zone : vec) { if (!zone->assigned_unit) { - handle_missing_assignments(&it, it_end, share_with_spouse, zone->id); + handle_missing_assignments(out, &it, it_end, share_with_spouse, zone->id); continue; } auto hf = df::historical_figure::find(zone->assigned_unit->hist_figure_id); @@ -303,7 +315,7 @@ static void process_rooms(color_ostream &out, int32_t spouse_hfid = share_with_spouse ? get_spouse_hfid(out, hf) : -1; assignments.emplace_back(zone->id, std::make_pair(hf->id, spouse_hfid)); } - handle_missing_assignments(&it, it_end, share_with_spouse, -1); + handle_missing_assignments(out, &it, it_end, share_with_spouse, -1); last_known = assignments; } @@ -319,6 +331,10 @@ static void do_cycle(color_ostream &out) { process_rooms(out, last_known_assignments_office, world->buildings.other.ZONE_OFFICE); process_rooms(out, last_known_assignments_dining, world->buildings.other.ZONE_DINING_HALL); process_rooms(out, last_known_assignments_tomb, world->buildings.other.ZONE_TOMB, false); + + DEBUG(cycle,out).print("tracking zone assignments: bedrooms: %zd, offices: %zd, dining halls: %zd, tombs: %zd\n", + last_known_assignments_bedroom.size(), last_known_assignments_office.size(), + last_known_assignments_dining.size(), last_known_assignments_tomb.size()); } ///////////////////////////////////////////////////// @@ -338,8 +354,10 @@ static bool spouse_has_sharable_room(color_ostream& out, int32_t hfid, df::civzo if (!spouse) return false; for (auto owned_zone : spouse->owned_buildings) { - if (owned_zone->type == ztype) + if (owned_zone->type == ztype) { + DEBUG(event,out).print("spouse had sharable room; no need to set ownership\n"); return true; + } } return false; } @@ -353,10 +371,14 @@ static void on_new_active_unit(color_ostream& out, void* data) { auto it = pending_reassignment.find(hfid); if (it == pending_reassignment.end()) return; + DEBUG(event,out).print("restoring zone ownership for unit %d (%s)\n", + unit->id, Units::getReadableName(unit).c_str()); for (auto zone_id : it->second) { + reserved_zones.erase(zone_id); auto zone = virtual_cast(df::building::find(zone_id)); if (!zone || zone->assigned_unit || spouse_has_sharable_room(out, hfid, zone->type)) continue; + DEBUG(event,out).print("assigning and activating zone %d\n", zone->id); Buildings::setOwner(zone, unit); zone->spec_sub_flag.bits.active = true; } @@ -368,12 +390,12 @@ static void on_new_active_unit(color_ostream& out, void* data) { // static void preserve_rooms_cycle(color_ostream &out) { - DEBUG(control,out).print("entering preserve_rooms_cycle\n"); + DEBUG(control,out).print("preserve_rooms_cycle\n"); do_cycle(out); } static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string feature) { - DEBUG(control,out).print("entering preserve_rooms_setFeature (enabled=%d, feature=%s)\n", + DEBUG(control,out).print("preserve_rooms_setFeature: enabled=%d, feature=%s\n", enabled, feature.c_str()); if (feature == "track-missions") { config.set_bool(CONFIG_TRACK_MISSIONS, enabled); @@ -389,7 +411,7 @@ static bool preserve_rooms_setFeature(color_ostream &out, bool enabled, string f } static bool preserve_rooms_getFeature(color_ostream &out, string feature) { - DEBUG(control,out).print("entering preserve_rooms_getFeature (feature=%s)\n", feature.c_str()); + TRACE(control,out).print("preserve_rooms_getFeature: feature=%s\n", feature.c_str()); if (feature == "track-missions") return config.get_bool(CONFIG_TRACK_MISSIONS); if (feature == "track-roles") @@ -398,7 +420,7 @@ static bool preserve_rooms_getFeature(color_ostream &out, string feature) { } static bool preserve_rooms_resetFeatureState(color_ostream &out, string feature) { - DEBUG(control,out).print("entering preserve_rooms_resetFeatureState (feature=%s)\n", feature.c_str()); + DEBUG(control,out).print("preserve_rooms_resetFeatureState: feature=%s\n", feature.c_str()); if (feature == "track-missions") { vector zone_ids; std::transform(reserved_zones.begin(), reserved_zones.end(), std::back_inserter(zone_ids), [](auto & elem){ return elem.first; }); @@ -418,6 +440,7 @@ static void preserve_rooms_assignToRole(color_ostream &out, string code) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return; + DEBUG(control,out).print("preserve_rooms_assignToRole: zone_id=%d, code=%s\n", zone->id, code.c_str()); noble_zones[zone->id] = code; do_cycle(out); } @@ -426,6 +449,7 @@ static string preserve_rooms_getRoleAssignment(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return ""; + TRACE(control,out).print("preserve_rooms_getRoleAssignment: zone_id=%d\n", zone->id); auto it = noble_zones.find(zone->id); if (it == noble_zones.end()) return ""; @@ -436,6 +460,7 @@ static bool preserve_rooms_isReserved(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return false; + TRACE(control,out).print("preserve_rooms_isReserved: zone_id=%d\n", zone->id); auto it = reserved_zones.find(zone->id); return it != reserved_zones.end() && it->second.size() > 0; } @@ -444,10 +469,11 @@ static string preserve_rooms_getReservationName(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return ""; + TRACE(control,out).print("preserve_rooms_getReservationName: zone_id=%d\n", zone->id); auto it = reserved_zones.find(zone->id); if (it != reserved_zones.end() && it->second.size() > 0) { if (auto hf = df::historical_figure::find(it->second.front())) { - return Translation::TranslateName(&hf->name, false); + return Units::getReadableName(hf); } } return ""; @@ -457,6 +483,7 @@ static bool preserve_rooms_clearReservation(color_ostream &out) { auto zone = Gui::getSelectedCivZone(out, true); if (!zone) return false; + DEBUG(control,out).print("preserve_rooms_clearReservation: zone_id=%d\n", zone->id); clear_reservation(out, zone->id, zone); return true; } @@ -465,7 +492,7 @@ static int preserve_rooms_getState(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); - DEBUG(control,*out).print("entering preserve_rooms_getState\n"); + DEBUG(control,*out).print("preserve_rooms_getState\n"); unordered_map features; features.emplace("track-missions", config.get_bool(CONFIG_TRACK_MISSIONS)); From b3660352da0f1e3cbbba851b51f3ee639dca2371 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 27 Aug 2024 20:22:42 -0700 Subject: [PATCH 09/11] new unit event should trigger whenever units enter the active vector --- library/modules/EventManager.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/library/modules/EventManager.cpp b/library/modules/EventManager.cpp index ff7e0cfef2..e83ad80722 100644 --- a/library/modules/EventManager.cpp +++ b/library/modules/EventManager.cpp @@ -725,19 +725,20 @@ static void manageNewUnitActiveEvent(color_ostream& out) { multimap copy(handlers[EventType::UNIT_NEW_ACTIVE].begin(), handlers[EventType::UNIT_NEW_ACTIVE].end()); // iterate event handler callbacks - vector new_active_unit_ids; + vector newly_active_unit_ids; for (df::unit* unit : df::global::world->units.active) { - if (!activeUnits.count(unit->id)) { - activeUnits.emplace(unit->id); - new_active_unit_ids.emplace_back(unit->id); - } + if (!activeUnits.count(unit->id)) + newly_active_unit_ids.emplace_back(unit->id); } - for (int32_t unit_id : new_active_unit_ids) { + for (int32_t unit_id : newly_active_unit_ids) { for (auto &[_,handle] : copy) { DEBUG(log,out).print("calling handler for new unit event\n"); run_handler(out, EventType::UNIT_NEW_ACTIVE, handle, (void*) intptr_t(unit_id)); // intptr_t() avoids cast from smaller type warning } } + activeUnits.clear(); + std::transform(df::global::world->units.active.begin(), df::global::world->units.active.end(), + std::inserter(activeUnits, activeUnits.end()), [](auto & unit){ return unit->id; }); } From 1a058000f1f9dcfab18cd87de325a3ab7c3c8dd7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 27 Aug 2024 20:23:38 -0700 Subject: [PATCH 10/11] we need to track membership in the active vector --- plugins/lua/preserve-rooms.lua | 4 ++-- plugins/preserve-rooms.cpp | 35 ++++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/plugins/lua/preserve-rooms.lua b/plugins/lua/preserve-rooms.lua index c83e7f9a3c..59415499f8 100644 --- a/plugins/lua/preserve-rooms.lua +++ b/plugins/lua/preserve-rooms.lua @@ -144,7 +144,7 @@ function ReservedWidget:init() }, }, widgets.Panel{ - frame={h=5}, + frame={t=0, h=5}, frame_style=gui.FRAME_MEDIUM, frame_background=gui.CLEAR_PEN, visible=preserve_rooms_isReserved, @@ -153,7 +153,7 @@ function ReservedWidget:init() frame={t=0, l=0}, text={ 'Reserved for traveling unit:', NEWLINE, - {text=preserve_rooms_getReservationName, pen=COLOR_YELLOW}, + {gap=1, text=preserve_rooms_getReservationName, pen=COLOR_YELLOW}, }, }, widgets.HotkeyLabel{ diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index 65eb4a73a4..e8a1383261 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -22,6 +22,7 @@ using std::pair; using std::string; using std::unordered_map; +using std::unordered_set; using std::vector; using namespace DFHack; @@ -243,6 +244,7 @@ static int32_t get_spouse_hfid(color_ostream &out, df::historical_figure *hf) { // handles when units disappear from their assignments compared to the last scan static void handle_missing_assignments(color_ostream &out, + const unordered_set & active_unit_ids, ZoneAssignments::iterator *pit, const ZoneAssignments::iterator & it_end, bool share_with_spouse, @@ -258,7 +260,7 @@ static void handle_missing_assignments(color_ostream &out, // if unit data is completely gone, then they're not likely to come back continue; } - if (Units::isActive(unit) && !Units::isDead(unit)) { + if (Units::isActive(unit) && !Units::isDead(unit) && active_unit_ids.contains(unit->id)) { // unit is still alive on the map; assume the unassigment was intentional/expected continue; } @@ -270,7 +272,7 @@ static void handle_missing_assignments(color_ostream &out, // unit is off-map or is dead; if we can assign room to spouse then we don't need to reserve the room if (auto spouse_hf = df::historical_figure::find(spouse_hfid); spouse_hf && share_with_spouse) { if (auto spouse = df::unit::find(spouse_hf->unit_id); - spouse && Units::isActive(spouse) && !Units::isDead(spouse)) + spouse && Units::isActive(spouse) && !Units::isDead(spouse) && active_unit_ids.contains(spouse->id)) { DEBUG(cycle,out).print("assigning zone %d (%s) to spouse %s\n", zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), @@ -286,17 +288,19 @@ static void handle_missing_assignments(color_ostream &out, zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), unit->id, Units::getReadableName(unit).c_str()); pending_reassignment[hfid].push_back(zone_id); - if (share_with_spouse) { + reserved_zones[zone_id].push_back(hfid); + if (share_with_spouse && spouse_hfid > -1) { DEBUG(cycle,out).print("registering spouse unit for reassignment to zone %d (%s): hfid=%d\n", zone_id, ENUM_KEY_STR(civzone_type, zone->type).c_str(), spouse_hfid); pending_reassignment[spouse_hfid].push_back(zone_id); + reserved_zones[zone_id].push_back(spouse_hfid); } - reserved_zones[zone_id].push_back(hfid); zone->spec_sub_flag.bits.active = false; } } static void process_rooms(color_ostream &out, + const unordered_set & active_unit_ids, ZoneAssignments & last_known, const vector & vec, bool share_with_spouse=true) @@ -306,7 +310,7 @@ static void process_rooms(color_ostream &out, auto it_end = last_known.end(); for (auto zone : vec) { if (!zone->assigned_unit) { - handle_missing_assignments(out, &it, it_end, share_with_spouse, zone->id); + handle_missing_assignments(out, active_unit_ids, &it, it_end, share_with_spouse, zone->id); continue; } auto hf = df::historical_figure::find(zone->assigned_unit->hist_figure_id); @@ -315,7 +319,7 @@ static void process_rooms(color_ostream &out, int32_t spouse_hfid = share_with_spouse ? get_spouse_hfid(out, hf) : -1; assignments.emplace_back(zone->id, std::make_pair(hf->id, spouse_hfid)); } - handle_missing_assignments(out, &it, it_end, share_with_spouse, -1); + handle_missing_assignments(out, active_unit_ids, &it, it_end, share_with_spouse, -1); last_known = assignments; } @@ -323,14 +327,18 @@ static void process_rooms(color_ostream &out, static void do_cycle(color_ostream &out) { cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + TRACE(cycle,out).print("running %s cycle\n", plugin_name); assign_nobles(out); - process_rooms(out, last_known_assignments_bedroom, world->buildings.other.ZONE_BEDROOM); - process_rooms(out, last_known_assignments_office, world->buildings.other.ZONE_OFFICE); - process_rooms(out, last_known_assignments_dining, world->buildings.other.ZONE_DINING_HALL); - process_rooms(out, last_known_assignments_tomb, world->buildings.other.ZONE_TOMB, false); + unordered_set active_unit_ids; + std::transform(world->units.active.begin(), world->units.active.end(), + std::inserter(active_unit_ids, active_unit_ids.end()), [](auto & unit){ return unit->id; }); + + process_rooms(out, active_unit_ids, last_known_assignments_bedroom, world->buildings.other.ZONE_BEDROOM); + process_rooms(out, active_unit_ids, last_known_assignments_office, world->buildings.other.ZONE_OFFICE); + process_rooms(out, active_unit_ids, last_known_assignments_dining, world->buildings.other.ZONE_DINING_HALL); + process_rooms(out, active_unit_ids, last_known_assignments_tomb, world->buildings.other.ZONE_TOMB, false); DEBUG(cycle,out).print("tracking zone assignments: bedrooms: %zd, offices: %zd, dining halls: %zd, tombs: %zd\n", last_known_assignments_bedroom.size(), last_known_assignments_office.size(), @@ -365,8 +373,11 @@ static bool spouse_has_sharable_room(color_ostream& out, int32_t hfid, df::civzo static void on_new_active_unit(color_ostream& out, void* data) { int32_t unit_id = reinterpret_cast(data); auto unit = df::unit::find(unit_id); - if (!unit) + if (!unit || unit->hist_figure_id < 0) return; + TRACE(event,out).print("unit %d (%s) arrived on map (hfid: %d, in pending: %d)\n", + unit->id, Units::getReadableName(unit).c_str(), unit->hist_figure_id, + pending_reassignment.contains(unit->hist_figure_id)); auto hfid = unit->hist_figure_id; auto it = pending_reassignment.find(hfid); if (it == pending_reassignment.end()) From 847ed9689b8631660fd0160024101fdae84388a8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 27 Aug 2024 20:26:17 -0700 Subject: [PATCH 11/11] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 16278a9a93..0d170006be 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -52,6 +52,7 @@ Template for new versions: # Future ## New Tools +- `preserve-rooms`: manage room assignments for off-map units and noble roles. reserves rooms owned by traveling units and reinstates their ownership when they return to the site. also allows you to assign rooms to noble/administrator roles, and the rooms will be automatically assigned whenever the holder of the role changes ## New Features