From f2684a5d55918278d07f0031ac36c69e6a63fcaf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Sep 2026 16:41:26 -0500 Subject: [PATCH 1/2] rebirth of labormanager This brings back labormanager as an automated work detail manager, using similar principles to the old labormanager, but with a new implementation mechanism --- docs/about/Removed.rst | 3 +- docs/changelog.txt | 2 + docs/plugins/autolabor.rst | 33 +- docs/plugins/labormanager.rst | 248 +-- plugins/autolabor/CMakeLists.txt | 17 +- plugins/autolabor/autolabor.cpp | 1432 ++++------------- plugins/autolabor/engines.h | 32 + plugins/autolabor/joblabormapper.cpp | 140 +- plugins/autolabor/joblabormapper.h | 12 +- plugins/autolabor/laborcommon.cpp | 325 ++++ plugins/autolabor/laborcommon.h | 112 ++ plugins/autolabor/labormanager.cpp | 2145 -------------------------- plugins/autolabor/labormanager.h | 4 - plugins/autolabor/laborstatemap.h | 2 +- plugins/autolabor/legacyengine.cpp | 949 ++++++++++++ plugins/autolabor/legacyengine.h | 24 + plugins/autolabor/modernengine.cpp | 1457 +++++++++++++++++ plugins/autolabor/modernengine.h | 50 + plugins/autolabor/monitorengine.h | 34 + plugins/autolabor/workdetails.cpp | 234 +++ plugins/autolabor/workdetails.h | 91 ++ plugins/lua/autolabor.lua | 50 +- 22 files changed, 3892 insertions(+), 3504 deletions(-) create mode 100644 plugins/autolabor/engines.h create mode 100644 plugins/autolabor/laborcommon.cpp create mode 100644 plugins/autolabor/laborcommon.h delete mode 100644 plugins/autolabor/labormanager.cpp delete mode 100644 plugins/autolabor/labormanager.h create mode 100644 plugins/autolabor/legacyengine.cpp create mode 100644 plugins/autolabor/legacyengine.h create mode 100644 plugins/autolabor/modernengine.cpp create mode 100644 plugins/autolabor/modernengine.h create mode 100644 plugins/autolabor/monitorengine.h create mode 100644 plugins/autolabor/workdetails.cpp create mode 100644 plugins/autolabor/workdetails.h diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index bf3d0118df..5235327634 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -28,7 +28,8 @@ autohauler ========== An automated labor management tool that only addressed hauling labors, leaving the assignment of skilled labors entirely up to the player. Fundamentally incompatible with the work detail -system of labor management in v50 of Dwarf Fortress. +system of labor management in v50 of Dwarf Fortress. A dedicated laborer pool for unskilled +jobs (including hauling) is now provided by the `labormanager` command of `autolabor`. .. _automaterial: diff --git a/docs/changelog.txt b/docs/changelog.txt index 097b46db0b..6cf175ac91 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,8 @@ Template for new versions: - `export-world-map`: Export world map data for GIS and other external tools. ## New Features +- `labormanager`: the `autolabor` plugin now provides a second command that manages labors through the work detail system instead of writing the labor matrix; use `` mode legacy|modern`` or the work details overlay to switch +- `labormanager`: track job posting age and show a "task starvation" notification in the DFHack warning panel when any posting sits unclaimed for over a day - `autodump`: new ``undestroy`` option reverts pending item destruction while the game is still paused - `buildingplan`: new "Do now" option marks the jobs of subsequently placed buildings as high priority - `getplants`: new ``--dry-run`` option counts affected plants without designating, and new ``--brewable``/``--edible``/``--oil``/``--cloth``/``--dye`` trait filters restrict the selection by plant use diff --git a/docs/plugins/autolabor.rst b/docs/plugins/autolabor.rst index 73d5feac53..34c997f2ff 100644 --- a/docs/plugins/autolabor.rst +++ b/docs/plugins/autolabor.rst @@ -5,6 +5,16 @@ autolabor :summary: Automatically manage dwarf labors. :tags: fort auto labors +The ``autolabor`` command runs the plugin's *legacy* engine, which writes the +labor matrix directly and bypasses the work detail system. The same plugin +also provides the `labormanager` command, a modern engine that works through +the work detail system instead. A third *monitor* mode watches the job +board and shows task starvation warnings without managing labors at all. +Only one mode can be active at a time; +``autolabor mode [legacy|modern|monitor]`` or the overlay on the work +details screen switches between them, and the selected mode persists +across save/load. + Autolabor attempts to keep as many dwarves as possible busy while allowing dwarves to specialize in specific skills. @@ -32,24 +42,21 @@ untouched by autolabor. logging level using the `debugfilter` command (setting either ``debug`` or ``trace`` level for the ``cycle`` mode) but be warned that this may generate a large amount of console spam, especially in a large fort. - When it is enabled, autolabor automatically disables the work detail system. You cannot - use autolabor and work details at the same time. If you attempt to open the work detail screen while - autolabor is active, a warning box should appear advising you that autolabor is managing labors and preventing - you from making any changes on that screen. + When it is enabled in legacy mode, autolabor automatically disables the work detail system. You cannot + use legacy autolabor and work details at the same time. The DFHack overlay panel on the work details + screen (the same panel that provides the `gui/settings-manager` save/load buttons) shows the active + mode and warns you when legacy mode is preventing changes on that screen from taking effect. - Finally, should you disable autolabor, autolabor will automatically reenable the vanilla work detail system. - However, the work detail system only updates labors when the work detail screen is open and some change is - made on that screen. Therefore, if you choose to disable autolabor, you should probably immediately - thereafter open the work details screen and make some change to force the game to recompute all labor - assignments based on the vanilla algorithm. At this time, it is not possible for autolabor to do this - automatically. + Should you disable autolabor or switch to modern mode, the vanilla work + detail system is automatically reenabled and all citizens' labors are + recomputed from the work details. Usage ----- :: - enable autolabor + autolabor enable Anything beyond this is optional - autolabor works well with the default settings. Once you have enabled it in a fortress, it stays enabled until you @@ -114,5 +121,9 @@ Advanced usage Turn off autolabor for a specific labor. ``autolabor reset-all| reset`` Return a labor (or all labors) to the default handling. +``autolabor mode [legacy|modern|monitor]`` + Show or change which engine runs when the plugin is enabled. Monitor + mode performs no labor management; it only watches the job board and + reports starving job postings via the DFHack notification panel. See `autolabor-artisans` for a differently-tuned setup. diff --git a/docs/plugins/labormanager.rst b/docs/plugins/labormanager.rst index 255de05c46..371ae99af9 100644 --- a/docs/plugins/labormanager.rst +++ b/docs/plugins/labormanager.rst @@ -2,131 +2,165 @@ labormanager ============ .. dfhack-tool:: - :summary: Automatically manage dwarf labors. - :tags: unavailable - -Labormanager is derived from `autolabor` but uses a completely different -approach to assigning jobs to dwarves. While autolabor tries to keep as many -dwarves busy as possible, labormanager instead strives to get jobs done as -quickly as possible. - -Labormanager frequently scans the current job list, current list of dwarves, and -the map to determine how many dwarves need to be assigned to what labors in -order to meet all current labor needs without starving any particular type of -job. - -Dwarves on active military duty or dwarves assigned to burrows are left -untouched. + :summary: Automatically manage dwarf labors via work details. + :tags: fort auto labors + +``labormanager`` is the modern mode of the `autolabor` plugin. Instead of +writing the labor matrix directly, it expresses labor policy through Dwarf +Fortress's work detail system, so the game's job auction chooses which +dwarves take which jobs. The plugin's managed work details are visible on +the work details screen (``u`` -> Labor -> Work Details) with names +beginning with ``auto:``. The DFHack overlay panel on that screen (the +same panel that provides work detail save/load buttons from +`gui/settings-manager`) lets you switch modes and tune the plugin's +priorities. + +Since the job auction already prefers skilled dwarves for skilled jobs, +labormanager focuses on the things the auction does poorly: + +* **Unskilled job starvation.** The auction tends to starve jobs that + require no skill (hauling, cleaning, lever pulling, and so on) in favor of + skilled work. Labormanager maintains a laborer pool, assigned to the + ``auto:Laborers`` work detail and restricted to unskilled labors, so there + are always dwarves that *can only* take unskilled work. The pool grows and + shrinks based on how many unclaimed unskilled job postings are piling up. + +* **Skill growth and specialization.** Citizens with a useful skill are + assigned to a specialized ``auto:`` work detail covering the labors that + train that skill, so they spend their time practicing rather than hauling. + If the fortress has a guild hall dedicated to the dwarf's profession, + labormanager also protects some of that specialist's time so they can + participate in guild activities (see ``idle-reserve`` below). + +Labormanager keeps statistics on which skills the fortress actually +exercises, based on the job postings that resolve on the job board. That +history shapes specialization: + +* **No dead skills.** Skills the fort never uses (animal dissection and + gelding in most forts, for example) get no reserved work detail at all -- + a dwarf whose only talent is an unused skill stays a generalist instead + of being pinned to irrelevant work. +* **Demand-weighted specialization.** Frequently-used skills are + prioritized over rarely-used ones when choosing which specialty to + assign, and units skilled in them are protected harder from laborer + duty. +* **Quality skills.** Skills whose products carry skill-scaled quality + (smithing, crafting, cooking, mechanics, engraving, and so on) are + preferred for higher-skilled workers -- both when choosing a specialty + and when deciding who to keep out of the laborer pool. +* **Strange-mood shaping.** Dwarves that can still have a strange mood + (their race has the ``STRANGE_MOODS`` token and they haven't had one + yet) and that have already started a moodable craft are steered toward + skills a strange mood can advance -- preferably valuable ones like + weaponsmithing or armorsmithing -- so a mood pays out in a useful + legendary skill. + +Labormanager also reads each citizen's unmet personal needs and biases +assignments toward work that satisfies them: + +* Citizens desperate to stay occupied, acquire things, or help others are + preferentially drafted into the laborer pool, where hauling, stockpiling, + depot runs, and patient care satisfy those needs. +* Citizens needing social time (gregarious, family-oriented, or just + wanting to take it easy) are kept out of the restrictive pools so they + can idle and socialize; guilded specialists with these needs earn guild + rest sooner. +* Citizens who crave wandering are steered toward fishing, hunting, and + plant gathering; those craving excitement toward trapping and hunting; + those craving creativity toward crafts. A completely unskilled citizen + can be assigned a specialty purely on inclination. + +Dwarves on active military duty, in meetings, or assigned to burrows are +left untouched. Children are unaffected: their chores are governed by the +vanilla chores settings. .. warning:: - As with autolabor, labormanager will override any manual changes you make to - labors while it is enabled, including through other tools such as Dwarf - Therapist. Do not run both autolabor and labormanager at the same time! + While labormanager is enabled it will overwrite the membership of its + ``auto:`` work details on every cycle. Player-created work details are + left alone, but manual edits to the managed details will be lost. Usage ----- :: - enable labormanager - -Anything beyond this is optional - labormanager works well with the default -settings. Once you have enabled it in a fortress, it stays enabled until you -explicitly disable it, even if you save and reload your game. - -The default priorities for each labor vary (some labors are higher priority by -default than others). The way the plugin works is that, once it determines how -many jobs of each labor are needed, it then sorts them by adjusted priority. -(Labors other than hauling have a bias added to them based on how long it's been -since they were last used to prevent job starvation.) The labor with the highest -priority is selected, the "best fit" dwarf for that labor is assigned to that -labor, and then its priority is *halved*. This process is repeated until either -dwarves or labors run out. - -Because there is no easy way to detect how many haulers are actually needed at -any moment, the plugin always ensures that at least one dwarf is assigned to -each of the hauling labors, even if no hauling jobs are detected. At least one -dwarf is always assigned to construction removing and cleaning because these -jobs also cannot be easily detected. Lever pulling is always assigned to -everyone. Any dwarves for which there are no jobs will be assigned hauling, -lever pulling, and cleaning labors. If you use animal trainers, note that -labormanager will misbehave if you assign specific trainers to specific animals; -results are only guaranteed if you use "any trainer". - -Labormanager also sometimes assigns extra labors to currently busy dwarfs so -that when they finish their current job, they will go off and do something -useful instead of standing around waiting for a job. - -There is special handling to ensure that at least one dwarf is assigned to haul -food whenever food is detected left in a place where it will rot if not stored. -This will cause a dwarf to go idle if you have no stockpiles to haul food to. - -Dwarves who are unable to work (child, in the military, wounded, handless, -asleep, in a meeting) are entirely excluded from labor assignment. Any dwarf -explicitly assigned to a burrow will also be completely ignored by labormanager. - -The fitness algorithm for assigning jobs to dwarves generally attempts to favor -dwarves who are more skilled over those who are less skilled. It also tries to -avoid assigning female dwarfs with children to jobs that are "outside", favors -assigning "outside" jobs to dwarfs who are carrying a tool that could be used as -a weapon, and tries to minimize how often dwarves have to reequip. - -Labormanager automatically determines medical needs and reserves health care -providers as needed. Note that this may cause idling if you have injured dwarves -but no or inadequate hospital facilities. - -Hunting is never assigned without a butchery, and fishing is never assigned -without a fishery, and neither of these labors is assigned unless specifically -enabled (see below). - -The method by which labormanager determines what labor is needed for a -particular job is complicated and, in places, incomplete. In some situations, -labormanager will detect that it cannot determine what labor is required. It -will, by default, pause and print an error message on the dfhack console, -followed by the message "LABORMANAGER: Game paused so you can investigate the -above message.". If this happens, please open an :issue:`` on GitHub, -reporting the lines that immediately preceded this message. You can tell -labormanager to ignore this error and carry on by running -``labormanager pause-on-error no``, but be warned that some job may go undone in -this situation. + labormanager enable + +Anything beyond this is optional. Once enabled in a fortress, it stays +enabled until you explicitly disable it, even if you save and reload your +game. + +The same plugin binary also provides the legacy `autolabor` command, which +uses the classic labor-matrix algorithm, and a *monitor* mode that does no +labor management at all -- it only watches the job board and reports +starving job postings (see the end of this document). Only one mode can be +active at a time; use ``labormanager mode`` (or the overlay on the work +details screen) to see or change which mode will run. Examples -------- -``labormanager priority BREWER 500`` - Boost the priority of brewing jobs. -``labormanager max FISH 1`` - Only assign fishing to one dwarf at a time. Note that you also have to run - ``labormanager allow-fishing`` for any dwarves to be assigned fishing at - all. +``labormanager balance staffing`` + Prioritize preventing unskilled jobs from starving. Specialists will be + pulled into the laborer pool more aggressively when hauling backs up. +``labormanager balance skills`` + Prioritize skill growth. Specialists keep their specialized labors even + when unskilled work is piling up. +``labormanager labor MINE unmanaged`` + Leave the mining labor to the vanilla work details and any manual + assignments, while managing everything else. Advanced usage -------------- ``labormanager list`` - Show current priorities and current allocation stats. Use this command to - see the IDs for all labors. + List every labor, whether labormanager is managing it, and which labors + currently have starving (long-unclaimed) job postings. ``labormanager status`` - Show basic status information. -``labormanager priority `` - Set the priority value for labor to . -``labormanager max `` - Set the maximum number of dwarves that can be assigned to a labor. -``labormanager max none`` - Unrestrict the number of dwarves that can be assigned to a labor. -``labormanager max disable`` - Don't manage the specified labor. Dwarves who you have manually enabled this - labor on will be less likely to have managed labors assigned to them. -``labormanager reset-all|reset `` - Return a labor (or all labors) to the default priority. + Show the current balance setting, role counts, job board statistics + (how many postings resolved, their average and longest waits, and how + many are currently starving), and the most-used skills observed on the + job board. +``labormanager balance <0-4|staffing|lean-staffing|balanced|lean-skills|skills>`` + Set where the balance slider sits. Lower values grow the laborer pool + more aggressively to keep unskilled jobs staffed; higher values protect + skill specialization. This is the same control shown as a slider on the + work details overlay. +``labormanager idle-reserve <0-100>`` + Percentage of time to keep free for specialists whose profession has a + matching guild hall, so they can take part in guild demonstrations and + other off-duty activities. Defaults to 30. +``labormanager labor managed|unmanaged`` + Include or exclude a specific labor from labormanager's work details. + Unmanaged labors are entirely governed by the player's own work detail + configuration. ``labormanager allow-fishing|forbid-fishing`` - Allow/disallow fisherdwarves. *Warning* Allowing fishing tends to result in - most of the fort going fishing. Fishing is forbidden by default. + Allow/disallow fisherdwarf specialists. Fishing is forbidden by default + and also requires a fishery. ``labormanager allow-hunting|forbid-hunting`` - Allow/disallow hunterdwarves. *Warning* Allowing hunting tends to result in - as many dwarves going hunting as you have crossbows. Hunting is forbidden by - default. -``labormanager pause-on-error yes|no`` - Make labormanager pause/continue if the labor inference engine fails. See - the above section for details. + Allow/disallow hunter specialists. Hunting is forbidden by default and + also requires a butchery. +``labormanager mode [legacy|modern|monitor]`` + Show or change which engine runs when the plugin is enabled. The same + command exists under `autolabor`. In monitor mode the plugin performs + no labor management -- all work details are left to the player -- but + still tracks the job board and shows the task starvation warning. + +Monitor mode (``labormanager mode monitor`` or the work details overlay) +runs the same tracking without any labor management, for players who want +the starvation warning while keeping full manual control. + +The tracking works as follows: job postings that sit unclaimed on the job +board are timed, and if any posting goes more than a full game day (1200 +ticks) without being +claimed, a "task starvation" warning appears in the DFHack notification +panel (alongside the other fort warnings), naming the longest-starving +job and how long it has been waiting; clicking the warning zooms the map +to that job's location. The warning can be toggled in `gui/notify` under +``labor_starvation``. + +All of labormanager's accumulated state -- job posting ages, skill usage +history, starvation streaks, and the guild rest rotation -- is saved with +the fortress and restored when the save is loaded, so statistics and +rotation carry over between play sessions. diff --git a/plugins/autolabor/CMakeLists.txt b/plugins/autolabor/CMakeLists.txt index 721654d202..1f3ce779a4 100644 --- a/plugins/autolabor/CMakeLists.txt +++ b/plugins/autolabor/CMakeLists.txt @@ -1,15 +1,26 @@ project(autolabor) # A list of source files set(COMMON_SRCS + joblabormapper.cpp + laborcommon.cpp + workdetails.cpp + legacyengine.cpp + modernengine.cpp ) # A list of headers -set(COMMON_HDRS laborstatemap.h +set(COMMON_HDRS + laborstatemap.h + joblabormapper.h + laborcommon.h + workdetails.h + engines.h + legacyengine.h + modernengine.h + monitorengine.h ) set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) # mash them together (headers are marked as headers and nothing will try to compile them) list(APPEND COMMON_SRCS ${COMMON_HDRS}) -#dfhack_plugin(labormanager labormanager.cpp joblabormapper.cpp ${COMMON_SRCS}) - dfhack_plugin(autolabor autolabor.cpp ${COMMON_SRCS} LINK_LIBRARIES lua) diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index 4fd73ce181..61770eddb1 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -1,1242 +1,412 @@ -#include "laborstatemap.h" - -#include "Debug.h" -#include "PluginManager.h" - -#include "modules/Units.h" -#include "modules/World.h" -#include "modules/MapCache.h" -#include "modules/Items.h" -#include "modules/Units.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace DFHack; -using namespace df::enums; - -DFHACK_PLUGIN("autolabor"); -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(game); - -#define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) - /* - * Autolabor module for dfhack + * autolabor plugin: unified labor management for Dwarf Fortress. * - * The idea behind this module is to constantly adjust labors so that the right dwarves - * are assigned to new tasks. The key is that, for almost all labors, once a dwarf begins - * a job it will finish that job even if the associated labor is removed. Thus the - * strategy is to frequently decide, for each labor, which dwarves should possibly take - * a new job for that labor if it comes in and which shouldn't, and then set the labors - * appropriately. The updating should happen as often as can be reasonably done without - * causing lag. + * One plugin binary exposes two commands: * - * The obvious thing to do is to just set each labor on a single idle dwarf who is best - * suited to doing new jobs of that labor. This works in a way, but it leads to a lot - * of idle dwarves since only one dwarf will be dispatched for each labor in an update - * cycle, and dwarves that finish tasks will wait for the next update before being - * dispatched. An improvement is to also set some labors on dwarves that are currently - * doing a job, so that they will immediately take a new job when they finish. The - * details of which dwarves should have labors set is mostly a heuristic. + * autolabor legacy mode: controls the labor matrix directly, bypassing + * the work detail system (the pre-v50 behavior). + * labormanager modern mode: expresses labor policy through DF's work + * details so the game's job auction does the assignment. * - * A complication to the above simple scheme is labors that have associated equipment. - * Enabling/disabling these labors causes dwarves to change equipment, and disabling - * them in the middle of a job may cause the job to be abandoned. Those labors - * (mining, hunting, and woodcutting) need to be handled carefully to minimize churn. + * A third mode, monitor, only watches the job board for starving postings + * and performs no labor management. + * + * The active mode is persisted per fort and can be switched from either + * command (" mode legacy|modern"), from the work details overlay, or + * implicitly by enabling through one of the commands. */ -DFHACK_PLUGIN_IS_ENABLED(enable_autolabor); - -namespace DFHack { - DBG_DECLARE(autolabor, cycle, DebugCategory::LINFO); -} - -static std::vector state_count(NUM_STATE); - -static PersistentDataItem config; - -command_result autolabor (color_ostream &out, std::vector & parameters); - -static bool isOptionEnabled(unsigned flag) -{ - return config.isValid() && (config.ival(0) & flag) != 0; -} - -enum ConfigFlags { - CF_ENABLED = 1, -}; - -static void setOptionEnabled(ConfigFlags flag, bool on) -{ - if (!config.isValid()) - return; - - if (on) - config.ival(0) |= flag; - else - config.ival(0) &= ~flag; -} - - -static void generate_labor_to_skill_map(); - -enum labor_mode { - DISABLE, - HAULERS, - AUTOMATIC, -}; - -struct labor_info -{ - PersistentDataItem config; - - bool is_exclusive; - int active_dwarfs; - - labor_mode mode() { return (labor_mode) config.ival(0); } - - void set_mode(labor_mode mode) { config.ival(0) = mode; } - - int minimum_dwarfs() { return config.ival(1); } - void set_minimum_dwarfs(int minimum_dwarfs) { config.ival(1) = minimum_dwarfs; } - - int maximum_dwarfs() { return config.ival(2); } - void set_maximum_dwarfs(int maximum_dwarfs) { config.ival(2) = maximum_dwarfs; } - - int talent_pool() { return config.ival(3); } - void set_talent_pool(int talent_pool) { config.ival(3) = talent_pool; } -}; - -struct labor_default -{ - labor_mode mode; - bool is_exclusive; - int minimum_dwarfs; - int maximum_dwarfs; - int active_dwarfs; -}; - -// The percentage of the dwarves assigned as haulers at any one time. -static int hauler_pct = 33; - -// The maximum percentage of dwarves who will be allowed to be idle. -// Decreasing this will encourage autolabor to keep dwarves busy, -// at the expense of making it harder for dwarves to specialize in -// specific skills. -static int idler_pct = 10; - -static std::vector labor_infos; +#include "engines.h" +#include "joblabormapper.h" +#include "laborcommon.h" +#include "legacyengine.h" +#include "modernengine.h" +#include "monitorengine.h" +#include "workdetails.h" + +#include "Core.h" +#include "LuaTools.h" +#include "PluginLua.h" +#include "PluginManager.h" -static const struct labor_default default_labor_infos[] = { - /* MINE */ {AUTOMATIC, true, 2, 200, 0}, - /* HAUL_STONE */ {HAULERS, false, 1, 200, 0}, - /* HAUL_WOOD */ {HAULERS, false, 1, 200, 0}, - /* HAUL_BODY */ {HAULERS, false, 1, 200, 0}, - /* HAUL_FOOD */ {HAULERS, false, 1, 200, 0}, - /* HAUL_REFUSE */ {HAULERS, false, 1, 200, 0}, - /* HAUL_ITEM */ {HAULERS, false, 1, 200, 0}, - /* HAUL_FURNITURE */ {HAULERS, false, 1, 200, 0}, - /* HAUL_ANIMAL */ {HAULERS, false, 1, 200, 0}, - /* CLEAN */ {HAULERS, false, 1, 200, 0}, - /* CUTWOOD */ {AUTOMATIC, true, 1, 200, 0}, - /* CARPENTER */ {AUTOMATIC, false, 1, 200, 0}, - /* STONECUTTER */ {AUTOMATIC, false, 1, 200, 0}, - /* STONE_CARVER */ {AUTOMATIC, false, 1, 200, 0}, - /* MASON */ {AUTOMATIC, false, 1, 200, 0}, - /* ANIMALTRAIN */ {AUTOMATIC, false, 1, 200, 0}, - /* ANIMALCARE */ {AUTOMATIC, false, 1, 200, 0}, - /* DIAGNOSE */ {AUTOMATIC, false, 1, 200, 0}, - /* SURGERY */ {AUTOMATIC, false, 1, 200, 0}, - /* BONE_SETTING */ {AUTOMATIC, false, 1, 200, 0}, - /* SUTURING */ {AUTOMATIC, false, 1, 200, 0}, - /* DRESSING_WOUNDS */ {AUTOMATIC, false, 1, 200, 0}, - /* FEED_WATER_CIVILIANS */ {AUTOMATIC, false, 200, 200, 0}, - /* RECOVER_WOUNDED */ {HAULERS, false, 1, 200, 0}, - /* BUTCHER */ {AUTOMATIC, false, 1, 200, 0}, - /* TRAPPER */ {AUTOMATIC, false, 1, 200, 0}, - /* DISSECT_VERMIN */ {AUTOMATIC, false, 1, 200, 0}, - /* LEATHER */ {AUTOMATIC, false, 1, 200, 0}, - /* TANNER */ {AUTOMATIC, false, 1, 200, 0}, - /* BREWER */ {AUTOMATIC, false, 1, 200, 0}, - /* ALCHEMIST */ {AUTOMATIC, false, 1, 200, 0}, - /* SOAP_MAKER */ {AUTOMATIC, false, 1, 200, 0}, - /* WEAVER */ {AUTOMATIC, false, 1, 200, 0}, - /* CLOTHESMAKER */ {AUTOMATIC, false, 1, 200, 0}, - /* MILLER */ {AUTOMATIC, false, 1, 200, 0}, - /* PROCESS_PLANT */ {AUTOMATIC, false, 1, 200, 0}, - /* MAKE_CHEESE */ {AUTOMATIC, false, 1, 200, 0}, - /* MILK */ {AUTOMATIC, false, 1, 200, 0}, - /* COOK */ {AUTOMATIC, false, 1, 200, 0}, - /* PLANT */ {AUTOMATIC, false, 1, 200, 0}, - /* HERBALIST */ {AUTOMATIC, false, 1, 200, 0}, - /* FISH */ {AUTOMATIC, false, 1, 1, 0}, - /* CLEAN_FISH */ {AUTOMATIC, false, 1, 200, 0}, - /* DISSECT_FISH */ {AUTOMATIC, false, 1, 200, 0}, - /* HUNT */ {AUTOMATIC, true, 1, 1, 0}, - /* SMELT */ {AUTOMATIC, false, 1, 200, 0}, - /* FORGE_WEAPON */ {AUTOMATIC, false, 1, 200, 0}, - /* FORGE_ARMOR */ {AUTOMATIC, false, 1, 200, 0}, - /* FORGE_FURNITURE */ {AUTOMATIC, false, 1, 200, 0}, - /* METAL_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* CUT_GEM */ {AUTOMATIC, false, 1, 200, 0}, - /* ENCRUST_GEM */ {AUTOMATIC, false, 1, 200, 0}, - /* WOOD_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* STONE_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* BONE_CARVE */ {AUTOMATIC, false, 1, 200, 0}, - /* GLASSMAKER */ {AUTOMATIC, false, 1, 200, 0}, - /* EXTRACT_STRAND */ {AUTOMATIC, false, 1, 200, 0}, - /* SIEGECRAFT */ {AUTOMATIC, false, 1, 200, 0}, - /* SIEGEOPERATE */ {AUTOMATIC, false, 1, 200, 0}, - /* BOWYER */ {AUTOMATIC, false, 1, 200, 0}, - /* MECHANIC */ {AUTOMATIC, false, 1, 200, 0}, - /* POTASH_MAKING */ {AUTOMATIC, false, 1, 200, 0}, - /* LYE_MAKING */ {AUTOMATIC, false, 1, 200, 0}, - /* DYER */ {AUTOMATIC, false, 1, 200, 0}, - /* BURN_WOOD */ {AUTOMATIC, false, 1, 200, 0}, - /* OPERATE_PUMP */ {AUTOMATIC, false, 1, 200, 0}, - /* SHEARER */ {AUTOMATIC, false, 1, 200, 0}, - /* SPINNER */ {AUTOMATIC, false, 1, 200, 0}, - /* POTTERY */ {AUTOMATIC, false, 1, 200, 0}, - /* GLAZING */ {AUTOMATIC, false, 1, 200, 0}, - /* PRESSING */ {AUTOMATIC, false, 1, 200, 0}, - /* BEEKEEPING */ {AUTOMATIC, false, 1, 200, 0}, - /* WAX_WORKING */ {AUTOMATIC, false, 1, 200, 0}, - /* HANDLE_VEHICLES */ {HAULERS, false, 1, 200, 0}, - /* HAUL_TRADE */ {HAULERS, false, 1, 200, 0}, - /* PULL_LEVER */ {HAULERS, false, 1, 200, 0}, - /* UNUSED_13 */ {DISABLE, false, 1, 0, 0}, - /* HAUL_WATER */ {HAULERS, false, 1, 200, 0}, - /* GELD */ {AUTOMATIC, false, 1, 200, 0}, - /* BUILD_ROAD */ {AUTOMATIC, false, 1, 200, 0}, - /* BUILD_CONSTRUCTION */ {AUTOMATIC, false, 1, 200, 0}, - /* PAPERMAKING */ {AUTOMATIC, false, 1, 200, 0}, - /* BOOKBINDING */ {AUTOMATIC, false, 1, 200, 0}, - /* UNUSED_20 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_21 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_22 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_23 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_24 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_25 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_26 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_27 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_28 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_29 */ {DISABLE, false, 0, 0, 0}, - /* UNUSED_30 */ {DISABLE, false, 0, 0, 0}, -}; +#include "modules/World.h" -static const int responsibility_penalties[] = { - 0, /* LAW_MAKING */ - 0, /* LAW_ENFORCEMENT */ - 3000, /* RECEIVE_DIPLOMATS */ - 0, /* MEET_WORKERS */ - 1000, /* MANAGE_PRODUCTION */ - 3000, /* TRADE */ - 1000, /* ACCOUNTING */ - 0, /* ESTABLISH_COLONY_TRADE_AGREEMENTS */ - 0, /* MAKE_INTRODUCTIONS */ - 0, /* MAKE_PEACE_AGREEMENTS */ - 0, /* MAKE_TOPIC_AGREEMENTS */ - 0, /* COLLECT_TAXES */ - 0, /* ESCORT_TAX_COLLECTOR */ - 0, /* EXECUTIONS */ - 0, /* TAME_EXOTICS */ - 0, /* RELIGION */ - 0, /* ATTACK_ENEMIES */ - 0, /* PATROL_TERRITORY */ - 0, /* MILITARY_GOALS */ - 0, /* MILITARY_STRATEGY */ - 0, /* UPGRADE_SQUAD_EQUIPMENT */ - 0, /* EQUIPMENT_MANIFESTS */ - 0, /* SORT_AMMUNITION */ - 0, /* BUILD_MORALE */ - 5000 /* HEALTH_MANAGEMENT */ -}; +using namespace DFHack; +using namespace df::enums; +using namespace autolabor; -struct dwarf_info_t -{ - int highest_skill; - int total_skill; - int mastery_penalty; - int assigned_jobs; - dwarf_state state; - bool has_exclusive_labor; - int noble_penalty; // penalty for assignment due to noble status - bool medical; // this dwarf has medical responsibility - bool trader; // this dwarf has trade responsibility - bool diplomacy; // this dwarf meets with diplomats -}; +DFHACK_PLUGIN("autolabor"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); -static void cleanup_state() -{ - enable_autolabor = false; - labor_infos.clear(); +command_result autolabor_cmd(color_ostream &out, std::vector ¶ms); +command_result labormanager_cmd(color_ostream &out, std::vector ¶ms); - // reinstate DF's work detail system - game->external_flag.bits.automatic_professions_disabled = false; -} +static WorkDetailManager wdm; +static LegacyEngine legacy_engine(&wdm); +static ModernEngine modern_engine(&wdm); +static MonitorEngine monitor_engine; +static LaborEngine *active_engine = NULL; -static void reset_labor(df::unit_labor labor) +static LaborEngine *mode_engine(int mode) { - labor_infos[labor].set_minimum_dwarfs(default_labor_infos[labor].minimum_dwarfs); - labor_infos[labor].set_maximum_dwarfs(default_labor_infos[labor].maximum_dwarfs); - labor_infos[labor].set_talent_pool(200); - labor_infos[labor].set_mode(default_labor_infos[labor].mode); + if (mode == MODE_MODERN) + return &modern_engine; + if (mode == MODE_MONITOR) + return &monitor_engine; + return &legacy_engine; } -static void init_state() +// switch the running engine to the given mode; safe when disabled +static void activate_mode(color_ostream &out, int mode) { - config = World::GetPersistentSiteData("autolabor/config"); - if (config.isValid() && config.ival(0) == -1) - config.ival(0) = 0; - - enable_autolabor = isOptionEnabled(CF_ENABLED); - - if (!enable_autolabor) + store_engine_mode(mode); + LaborEngine *want = mode_engine(mode); + if (active_engine == want) return; - - // bypass DF's work detail system - game->external_flag.bits.automatic_professions_disabled = true; - - auto cfg_haulpct = World::GetPersistentSiteData("autolabor/haulpct"); - if (cfg_haulpct.isValid()) - { - hauler_pct = cfg_haulpct.ival(0); - } - else - { - hauler_pct = 33; - } - - // Load labors from save - labor_infos.resize(ARRAY_COUNT(default_labor_infos)); - - std::vector items; - World::GetPersistentSiteData(&items, "autolabor/labors/", true); - - for (auto& p : items) - { - std::string key = p.key(); - df::unit_labor labor = (df::unit_labor) atoi(key.substr(strlen("autolabor/labors/")).c_str()); - if (labor >= 0 && size_t(labor) < labor_infos.size()) - { - labor_infos[labor].config = p; - labor_infos[labor].is_exclusive = default_labor_infos[labor].is_exclusive; - labor_infos[labor].active_dwarfs = 0; - } - } - - // Add default labors for those not in save - for (size_t i = 0; i < ARRAY_COUNT(default_labor_infos); i++) { - if (labor_infos[i].config.isValid()) - continue; - - std::stringstream name; - name << "autolabor/labors/" << i; - - labor_infos[i].config = World::AddPersistentSiteData(name.str()); - - labor_infos[i].is_exclusive = default_labor_infos[i].is_exclusive; - labor_infos[i].active_dwarfs = 0; - reset_labor((df::unit_labor) i); - } - - generate_labor_to_skill_map(); - -} - -static df::job_skill labor_to_skill[ENUM_LAST_ITEM(unit_labor) + 1]; - -static void generate_labor_to_skill_map() -{ - // Generate labor -> skill mapping - - for (int i = 0; i <= ENUM_LAST_ITEM(unit_labor); i++) - labor_to_skill[i] = job_skill::NONE; - - FOR_ENUM_ITEMS(job_skill, skill) - { - int labor = ENUM_ATTR(job_skill, labor, skill); - if (labor != unit_labor::NONE) - { - /* - assert(labor >= 0); - assert(labor < ARRAY_COUNT(labor_to_skill)); - */ - - labor_to_skill[labor] = skill; - } - } - + if (active_engine) + active_engine->disable(out); + active_engine = want; + if (is_enabled) + active_engine->enable(out); } - -static void enable_plugin(color_ostream &out) +static bool can_run(color_ostream &out) { - if (!config.isValid()) + if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - config = World::AddPersistentSiteData("autolabor/config"); - config.ival(0) = 0; + out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); + return false; } - - setOptionEnabled(CF_ENABLED, true); - enable_autolabor = true; - out << "Enabling autolabor." << std::endl; - - cleanup_state(); - init_state(); + return true; } -static void disable_plugin(color_ostream& out) +DFhackCExport command_result plugin_init(color_ostream &out, + std::vector &commands) { - if (config.isValid()) - setOptionEnabled(CF_ENABLED, false); - - enable_autolabor = false; - out << "Disabling autolabor." << std::endl; - - cleanup_state(); -} - -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - // initialize labor infos table from default table - if(ARRAY_COUNT(default_labor_infos) != ENUM_LAST_ITEM(unit_labor) + 1) - { - out.printerr("autolabor: labor size mismatch\n"); - return CR_FAILURE; - } - commands.push_back(PluginCommand( "autolabor", - "Automatically manage dwarf labors.", - autolabor)); - - init_state(); + "Automatically manage dwarf labors (legacy labor matrix mode).", + autolabor_cmd)); + commands.push_back(PluginCommand( + "labormanager", + "Automatically manage dwarf labors (work detail mode).", + labormanager_cmd)); return CR_OK; } -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +DFhackCExport command_result plugin_shutdown(color_ostream &out) { - cleanup_state(); - + if (active_engine && is_enabled && Core::getInstance().isMapLoaded()) + active_engine->disable(out); + active_engine = NULL; return CR_OK; } -// sorting objects -struct dwarfinfo_sorter -{ - dwarfinfo_sorter(std::vector & info):dwarf_info(info){}; - bool operator() (int i,int j) - { - if (dwarf_info[i].state == IDLE && dwarf_info[j].state != IDLE) - return true; - if (dwarf_info[i].state != IDLE && dwarf_info[j].state == IDLE) - return false; - return dwarf_info[i].mastery_penalty > dwarf_info[j].mastery_penalty; - }; - std::vector & dwarf_info; -}; -struct laborinfo_sorter -{ - bool operator() (int i,int j) - { - if (labor_infos[i].mode() != labor_infos[j].mode()) - return labor_infos[i].mode() < labor_infos[j].mode(); - if (labor_infos[i].is_exclusive != labor_infos[j].is_exclusive) - return labor_infos[i].is_exclusive; - if (labor_infos[i].maximum_dwarfs() != labor_infos[j].maximum_dwarfs()) - return labor_infos[i].maximum_dwarfs() < labor_infos[j].maximum_dwarfs(); - return false; - }; -}; - -struct values_sorter -{ - values_sorter(std::vector & values):values(values){}; - bool operator() (int i,int j) - { - return values[i] > values[j]; - }; - std::vector & values; -}; - - -static void assign_labor(unit_labor::unit_labor labor, - int n_dwarfs, - std::vector& dwarf_info, - bool trader_requested, - std::vector& dwarfs, - bool has_butchers, - bool has_fishery, - color_ostream& out) +DFhackCExport command_result plugin_load_site_data(color_ostream &out) { - df::job_skill skill = labor_to_skill[labor]; - - if (labor_infos[labor].mode() != AUTOMATIC) - return; - - int best_skill = 0; - - std::vector values(n_dwarfs); - std::vector candidates; - std::map dwarf_skill; - std::map dwarf_skillxp; - std::vector previously_enabled(n_dwarfs); - - // Find candidate dwarfs, and calculate a preference value for each dwarf - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) - { - if (dwarf_info[dwarf].state == CHILD) - continue; - if (dwarf_info[dwarf].state == MILITARY) - continue; - if (dwarf_info[dwarf].trader && trader_requested) - continue; - if (dwarf_info[dwarf].diplomacy) - continue; - - if (labor_infos[labor].is_exclusive && dwarf_info[dwarf].has_exclusive_labor) - continue; - - int value = dwarf_info[dwarf].mastery_penalty; - - if (skill != job_skill::NONE) - { - int skill_level = 0; - int skill_experience = 0; - - for (auto s : dwarfs[dwarf]->status.souls[0]->skills) - { - if (s->id == skill) - { - skill_level = s->rating; - skill_experience = s->experience; - break; - } - } - - dwarf_skill[dwarf] = skill_level; - dwarf_skillxp[dwarf] = skill_experience; - - if (best_skill < skill_level) - best_skill = skill_level; - - value += skill_level * 100; - value += skill_experience / 20; - if (skill_level > 0 || skill_experience > 0) - value += 200; - if (skill_level >= 15) - value += 1000 * (skill_level - 14); - } - else - { - dwarf_skill[dwarf] = 0; - } - - if (dwarfs[dwarf]->status.labors[labor]) - { - value += 5; - if (labor_infos[labor].is_exclusive) - value += 350; - } - - if (dwarf_info[dwarf].has_exclusive_labor) - value -= 500; - - // bias by happiness + load_plugin_config(); + wdm.reset(); + init_labor_to_skill(); - //value += dwarfs[dwarf]->status.happiness; + is_enabled = config_flag(CF_ENABLED); + active_engine = NULL; + if (is_enabled) + activate_mode(out, engine_mode()); - values[dwarf] = value; - - candidates.push_back(dwarf); - - } - - int pool = labor_infos[labor].talent_pool(); - if (pool < 200 && candidates.size() > 1 && size_t(abs(pool)) < candidates.size()) - { - // Sort in descending order - std::sort(candidates.begin(), candidates.end(), [&](const int lhs, const int rhs) -> bool { - if (dwarf_skill[lhs] == dwarf_skill[rhs]) - if (pool > 0) - return dwarf_skillxp[lhs] > dwarf_skillxp[rhs]; - else - return dwarf_skillxp[lhs] < dwarf_skillxp[rhs]; - else - if (pool > 0) - return dwarf_skill[lhs] > dwarf_skill[rhs]; - else - return dwarf_skill[lhs] < dwarf_skill[rhs]; - }); - - // Check if all dwarves have equivalent skills, usually zero - int first_dwarf = candidates[0]; - int last_dwarf = candidates[candidates.size() - 1]; - if (dwarf_skill[first_dwarf] == dwarf_skill[last_dwarf] && - dwarf_skillxp[first_dwarf] == dwarf_skillxp[last_dwarf]) - { - // There's no difference in skill, so change nothing - } - else - { - // Trim down to our top (or not) talents - candidates.resize(abs(pool)); - } - } - - // Sort candidates by preference value - values_sorter ivs(values); - std::sort(candidates.begin(), candidates.end(), ivs); - - // Disable the labor on everyone - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) - { - if (dwarf_info[dwarf].state == CHILD) - continue; - - previously_enabled[dwarf] = dwarfs[dwarf]->status.labors[labor]; - dwarfs[dwarf]->status.labors[labor] = false; - } - - int min_dwarfs = labor_infos[labor].minimum_dwarfs(); - int max_dwarfs = labor_infos[labor].maximum_dwarfs(); - - // Special - don't assign hunt without a butchers, or fish without a fishery - if (unit_labor::HUNT == labor && !has_butchers) - min_dwarfs = max_dwarfs = 0; - if (unit_labor::FISH == labor && !has_fishery) - min_dwarfs = max_dwarfs = 0; - - // If there are enough idle dwarves to choose from, enter an aggressive assignment - // mode. "Enough" idle dwarves is defined as 2 or 10% of the total number of dwarves, - // whichever is higher. - // - // In aggressive mode, we will always pick at least one idle dwarf for each skill, - // in order to try to get the idle dwarves to start doing something. We also pick - // any dwarf more preferable to the idle dwarf, since we'd rather have a more - // preferable dwarf do a new job if one becomes available (probably because that - // dwarf just finished a job). - // - // In non-aggressive mode, only dwarves that are good at a labor will be assigned - // to it. Dwarves good at nothing, or nothing that needs doing, will tend to get - // assigned to hauling by the hauler code. If there are no hauling jobs to do, - // they will sit around idle and when enough build up they will trigger aggressive - // mode again. - bool aggressive_mode = state_count[IDLE] >= 2 && state_count[IDLE] >= n_dwarfs * idler_pct / 100; - - /* - * Assign dwarfs to this labor. We assign at least the minimum number of dwarfs, in - * order of preference, and then assign additional dwarfs that meet any of these conditions: - * - We are in aggressive mode and have not yet assigned an idle dwarf - * - The dwarf is good at this skill - * - The labor is mining, hunting, or woodcutting and the dwarf currently has it enabled. - * We stop assigning dwarfs when we reach the maximum allowed. - * Note that only idle and busy dwarfs count towards the number of dwarfs. "Other" dwarfs - * (sleeping, eating, on break, etc.) will have labors assigned, but will not be counted. - * Military and children/nobles will not have labors assigned. - * Dwarfs with the "health management" responsibility are always assigned DIAGNOSIS. - */ - for (size_t i = 0; i < candidates.size() && labor_infos[labor].active_dwarfs < max_dwarfs; i++) - { - int dwarf = candidates[i]; - - if (dwarf_info[dwarf].trader && trader_requested) - continue; - if (dwarf_info[dwarf].diplomacy) - continue; - - assert(dwarf >= 0); - assert(dwarf < n_dwarfs); - - bool preferred_dwarf = false; - if (dwarf_skillxp[dwarf] > 0 && dwarf_skill[dwarf] >= best_skill / 2) - preferred_dwarf = true; - if (previously_enabled[dwarf] && labor_infos[labor].is_exclusive && dwarf_info[dwarf].state == EXCLUSIVE) - preferred_dwarf = true; - if (dwarf_info[dwarf].medical && labor == df::unit_labor::DIAGNOSE) - preferred_dwarf = true; - - if (labor_infos[labor].active_dwarfs >= min_dwarfs && !preferred_dwarf && !aggressive_mode) - continue; - - if (!dwarfs[dwarf]->status.labors[labor]) - dwarf_info[dwarf].assigned_jobs++; - - if (Units::isValidLabor(dwarfs[dwarf], labor)) - dwarfs[dwarf]->status.labors[labor] = true; - - if (labor_infos[labor].is_exclusive) - { - dwarf_info[dwarf].has_exclusive_labor = true; - // all the exclusive labors require equipment so this should force the dorf to reequip if needed - dwarfs[dwarf]->uniform.pickup_flags.bits.update = 1; - } - - TRACE(cycle, out).print("Dwarf {} \"{}\" assigned {}: value {} {} {}\n", - dwarf, dwarfs[dwarf]->name.first_name, ENUM_KEY_STR(unit_labor, labor), values[dwarf], - dwarf_info[dwarf].trader ? "(trader)" : "", - dwarf_info[dwarf].diplomacy ? "(diplomacy)" : ""); - - if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) - labor_infos[labor].active_dwarfs++; - - if (dwarf_info[dwarf].state == IDLE) - aggressive_mode = false; - } -} - -static const int32_t CYCLE_TICKS = 61; -static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - -DFhackCExport command_result plugin_load_site_data (color_ostream &out) { - cycle_timestamp = 0; - cleanup_state(); - init_state(); return CR_OK; } -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +DFhackCExport command_result plugin_onstatechange(color_ostream &out, + state_change_event event) { - switch (event) { - case SC_MAP_UNLOADED: - cleanup_state(); - break; - default: - break; + if (event == SC_MAP_UNLOADED) + { + legacy_engine.map_unload(); + modern_engine.map_unload(); + monitor_engine.map_unload(); + active_engine = NULL; + // is_enabled stays set; CF_ENABLED persists the user's intent and + // plugin_load_site_data will re-enable the engine on the next map } - return CR_OK; } -DFhackCExport command_result plugin_onupdate ( color_ostream &out ) +DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if(!world || !world->map.block_index || !enable_autolabor) - { - return CR_OK; - } - - if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) - return CR_OK; - - cycle_timestamp = world->frame_counter; - - std::vector dwarfs; + if (is_enabled && active_engine) + active_engine->update(out); + return CR_OK; +} - bool has_butchers = false; - bool has_fishery = false; - bool trader_requested = false; +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) +{ + if (!can_run(out)) + return CR_FAILURE; - for (auto& build : world->buildings.all) + if (enable && !is_enabled) { - auto type = build->getType(); - if (building_type::Workshop == type) - { - df::workshop_type subType = (df::workshop_type)build->getSubtype(); - if (workshop_type::Butchers == subType) - has_butchers = true; - if (workshop_type::Fishery == subType) - has_fishery = true; - } - else if (building_type::TradeDepot == type) - { - df::building_tradedepotst* depot = (df::building_tradedepotst*) build; - trader_requested = trader_requested || depot->trade_flags.bits.trader_requested; - TRACE(cycle,out).print("{}", trader_requested - ? "Trade depot found and trader requested, trader will be excluded from all labors.\n" - : "Trade depot found but trader is not requested.\n" - ); - } + set_config_flag(CF_ENABLED, true); + is_enabled = true; + activate_mode(out, engine_mode()); } - - for (auto& cre : world->units.active) + else if (!enable && is_enabled) { - if (Units::isCitizen(cre)) - { - if (cre->burrows.size() > 0) - continue; // dwarfs assigned to active burrows are skipped entirely - dwarfs.push_back(cre); - } + set_config_flag(CF_ENABLED, false); + is_enabled = false; + if (active_engine) + active_engine->disable(out); + active_engine = NULL; } - int n_dwarfs = dwarfs.size(); - - if (n_dwarfs == 0) - return CR_OK; - - std::vector dwarf_info(n_dwarfs); + return CR_OK; +} - // Find total skill and highest skill for each dwarf. More skilled dwarves shouldn't be used for minor tasks. +// handle " mode" / " mode legacy|modern"; returns true if handled +static bool mode_command(color_ostream &out, std::vector ¶meters) +{ + if (parameters.empty() || parameters[0] != "mode") + return false; - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + if (parameters.size() == 1) { - if (dwarfs[dwarf]->status.souls.size() <= 0) - continue; - - // compute noble penalty - - int noble_penalty = 0; - - df::historical_figure* hf = df::historical_figure::find(dwarfs[dwarf]->hist_figure_id); - if(hf!=NULL) //can be NULL. E.g. script created citizens - for (auto& hfelink : hf->entity_links) - { - if (hfelink->getType() == df::histfig_entity_link_type::POSITION) - { - df::histfig_entity_link_positionst *epos = - (df::histfig_entity_link_positionst*) hfelink; - df::historical_entity* entity = df::historical_entity::find(epos->entity_id); - if (!entity) - continue; - df::entity_position_assignment* assignment = binsearch_in_vector(entity->positions.assignments, epos->assignment_id); - if (!assignment) - continue; - df::entity_position* position = binsearch_in_vector(entity->positions.own, assignment->position_id); - if (!position) - continue; - - for (int n = 0; n < 25; n++) - if (position->responsibilities[n]) - noble_penalty += responsibility_penalties[n]; - - if (position->responsibilities[df::entity_position_responsibility::HEALTH_MANAGEMENT]) - dwarf_info[dwarf].medical = true; - - if (position->responsibilities[df::entity_position_responsibility::TRADE]) - dwarf_info[dwarf].trader = true; - - } - - } - - dwarf_info[dwarf].noble_penalty = noble_penalty; - - // identify dwarfs who are needed for meetings and mark them for exclusion - - for (auto& act : plotinfo->activities) - { - if (!act) continue; - bool p1 = act->unit_actor == dwarfs[dwarf]->id; - bool p2 = act->unit_noble == dwarfs[dwarf]->id; - - if (p1 || p2) - { - dwarf_info[dwarf].diplomacy = true; - DEBUG(cycle, out).print("Dwarf {} \"{}\" has a meeting, will be cleared of all labors\n", - dwarf, dwarfs[dwarf]->name.first_name); - break; - } - } - - for (auto& skill : dwarfs[dwarf]->status.souls[0]->skills) - { - df::job_skill_class skill_class = ENUM_ATTR(job_skill, type, skill->id); - - int skill_level = skill->rating; - - // Track total & highest skill among normal/medical skills. (We don't care about personal or social skills.) - - if (skill_class != job_skill_class::Normal && skill_class != job_skill_class::Medical) - continue; - - if (dwarf_info[dwarf].highest_skill < skill_level) - dwarf_info[dwarf].highest_skill = skill_level; - dwarf_info[dwarf].total_skill += skill_level; - } + out << "Mode: " + << (engine_mode() == MODE_MODERN ? "modern (labormanager)" : + engine_mode() == MODE_MONITOR ? "monitor (starvation warnings)" : + "legacy (autolabor)") + << std::endl; + return true; } - // Calculate a base penalty for using each dwarf for a task he isn't good at. - - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + int want = -1; + if (parameters.size() == 2) { - dwarf_info[dwarf].mastery_penalty -= 40 * dwarf_info[dwarf].highest_skill; - dwarf_info[dwarf].mastery_penalty -= 10 * dwarf_info[dwarf].total_skill; - dwarf_info[dwarf].mastery_penalty -= dwarf_info[dwarf].noble_penalty; - - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE) - continue; - - if (labor_infos[labor].is_exclusive && dwarfs[dwarf]->status.labors[labor]) - dwarf_info[dwarf].mastery_penalty -= 100; - } + if (parameters[1] == "legacy") + want = MODE_LEGACY; + else if (parameters[1] == "modern") + want = MODE_MODERN; + else if (parameters[1] == "monitor") + want = MODE_MONITOR; } - // Find the activity state for each dwarf. It's important to get this right - a dwarf who we think is IDLE but - // can't work will gum everything up. In the future I might add code to auto-detect slacker dwarves. - - state_count.clear(); - state_count.resize(NUM_STATE); - - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + if (want == -1) { - if (Units::isBaby(dwarfs[dwarf]) || - Units::isChild(dwarfs[dwarf]) || - dwarfs[dwarf]->profession == profession::DRUNK) - { - dwarf_info[dwarf].state = CHILD; - } - else if (ENUM_ATTR(profession, military, dwarfs[dwarf]->profession)) - dwarf_info[dwarf].state = MILITARY; - else if (dwarfs[dwarf]->job.current_job == NULL) - { - if (Units::getMiscTrait(dwarfs[dwarf], misc_trait_type::Migrant)) - dwarf_info[dwarf].state = OTHER; - else if (dwarfs[dwarf]->specific_refs.size() > 0) - dwarf_info[dwarf].state = OTHER; - else - dwarf_info[dwarf].state = IDLE; - } - else - { - int job = dwarfs[dwarf]->job.current_job->job_type; - if (job >= 0 && size_t(job) < ARRAY_COUNT(dwarf_states)) - dwarf_info[dwarf].state = dwarf_states[job]; - else - { - WARN(cycle, out).print("Dwarf {} \"{}\" has unknown job {}\n", dwarf, dwarfs[dwarf]->name.first_name, job); - dwarf_info[dwarf].state = OTHER; - } - } - - state_count[dwarf_info[dwarf].state]++; - - TRACE(cycle, out).print("Dwarf {} \"{}\": penalty {}, state {}\n", - dwarf, dwarfs[dwarf]->name.first_name, dwarf_info[dwarf].mastery_penalty, state_names[dwarf_info[dwarf].state]); + out.printerr("Syntax: mode [legacy|modern|monitor]\n"); + return true; } - std::vector labors; + if (!can_run(out)) + return true; - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE) - continue; + activate_mode(out, want); + out << "Mode: " << parameters[1] << std::endl; + return true; +} - labor_infos[labor].active_dwarfs = 0; +command_result autolabor_cmd(color_ostream &out, std::vector ¶meters) +{ + if (!can_run(out)) + return CR_FAILURE; - labors.push_back(labor); - } - laborinfo_sorter lasorter; - std::sort(labors.begin(), labors.end(), lasorter); + if (mode_command(out, parameters)) + return CR_OK; - // Handle DISABLED skills (just bookkeeping). - // Note that autolabor should *NEVER* enable or disable a skill that has been marked as DISABLED, for any reason. - // The user has told us that they want manage this skill manually, and we must respect that. - for (auto& labor: labors) + if (parameters.size() == 1 && + (parameters[0] == "0" || parameters[0] == "enable" || + parameters[0] == "1" || parameters[0] == "disable")) { - if (labor_infos[labor].mode() != DISABLE) - continue; - - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + bool enable = parameters[0] == "1" || parameters[0] == "enable"; + if (enable) { - if (dwarfs[dwarf]->status.labors[labor]) - { - if (labor_infos[labor].is_exclusive) - dwarf_info[dwarf].has_exclusive_labor = true; - - dwarf_info[dwarf].assigned_jobs++; - } + store_engine_mode(MODE_LEGACY); + if (is_enabled) + activate_mode(out, MODE_LEGACY); } + return plugin_enable(out, enable); } - // Handle all skills except those marked HAULERS - - for (auto& labor : labors) - { - assign_labor(labor, n_dwarfs, dwarf_info, trader_requested, dwarfs, has_butchers, has_fishery, out); - } + if (legacy_engine.command(out, parameters)) + return CR_OK; - // Set about 1/3 of the dwarfs as haulers. The haulers have all HAULER labors enabled. Having a lot of haulers helps - // make sure that hauling jobs are handled quickly rather than building up. + out.print("Automatically assigns labors to dwarves by writing the labor\n" + "matrix directly (legacy mode). Activate with 'autolabor enable',\n" + "deactivate with 'autolabor disable'.\n" + "Current state: {}, mode: {}.\n", (int)is_enabled, + engine_mode() == MODE_MODERN ? "modern" : "legacy"); + return CR_OK; +} - int num_haulers = state_count[IDLE] + (state_count[BUSY] + state_count[EXCLUSIVE]) * hauler_pct / 100; +command_result labormanager_cmd(color_ostream &out, std::vector ¶meters) +{ + if (!can_run(out)) + return CR_FAILURE; - if (num_haulers < 1) - num_haulers = 1; + if (mode_command(out, parameters)) + return CR_OK; - std::vector hauler_ids; - for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + if (parameters.size() == 1 && + (parameters[0] == "0" || parameters[0] == "enable" || + parameters[0] == "1" || parameters[0] == "disable")) { - if ((dwarf_info[dwarf].trader && trader_requested) || - dwarf_info[dwarf].diplomacy) + bool enable = parameters[0] == "1" || parameters[0] == "enable"; + if (enable) { - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE) - continue; - if (labor_infos[labor].mode() != HAULERS) - continue; - dwarfs[dwarf]->status.labors[labor] = false; - } - if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) - { - num_haulers--; - } - continue; + store_engine_mode(MODE_MODERN); + if (is_enabled) + activate_mode(out, MODE_MODERN); } - - if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) - hauler_ids.push_back(dwarf); - } - dwarfinfo_sorter sorter(dwarf_info); - // Idle dwarves come first, then we sort from least-skilled to most-skilled. - std::sort(hauler_ids.begin(), hauler_ids.end(), sorter); - - // don't set any haulers if everyone is off drinking or something - if (hauler_ids.size() == 0) { - num_haulers = 0; + return plugin_enable(out, enable); } - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE) - continue; - - if (labor_infos[labor].mode() != HAULERS) - continue; - - for (int i = 0; i < num_haulers; i++) - { - assert(size_t(i) < hauler_ids.size()); - - int dwarf = hauler_ids[i]; - - assert(dwarf >= 0); - assert(dwarf < n_dwarfs); - dwarfs[dwarf]->status.labors[labor] = true; - dwarf_info[dwarf].assigned_jobs++; - - if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) - labor_infos[labor].active_dwarfs++; - - TRACE(cycle, out).print("Dwarf {} \"{}\" assigned {}: hauler\n", - dwarf, dwarfs[dwarf]->name.first_name, ENUM_KEY_STR(unit_labor, labor)); - } - - for (size_t i = num_haulers; i < hauler_ids.size(); i++) - { - assert(i < hauler_ids.size()); + // in monitor mode the monitor engine handles status/list; everything + // else (balance, labor config, ...) falls through to the modern dialect + if (engine_mode() == MODE_MONITOR && monitor_engine.command(out, parameters)) + return CR_OK; - int dwarf = hauler_ids[i]; + if (modern_engine.command(out, parameters)) + return CR_OK; - assert(dwarf >= 0); - assert(dwarf < n_dwarfs); + out.print("Automatically assigns labors to dwarves through the work detail\n" + "system (modern mode). Activate with 'labormanager enable',\n" + "deactivate with 'labormanager disable'.\n" + "Current state: {}, mode: {}.\n", (int)is_enabled, + engine_mode() == MODE_MODERN ? "modern" : "legacy"); + return CR_OK; +} - dwarfs[dwarf]->status.labors[labor] = false; - } - } +// --------------------------------------------------------------------------- +// Lua API for the work details overlay (plugins.autolabor.*) +// --------------------------------------------------------------------------- - return CR_OK; +static int autolabor_getMode(lua_State *L) +{ + Lua::Push(L, engine_mode()); + return 1; } -void print_labor (df::unit_labor labor, color_ostream &out) +static int autolabor_getStatus(lua_State *L) { - std::string labor_name = ENUM_KEY_STR(unit_labor, labor); - out << labor_name << ": "; - for (int i = 0; i < 20 - (int)labor_name.length(); i++) - out << ' '; - if (labor_infos[labor].mode() == DISABLE) - out << "disabled" << std::endl; - else - { - if (labor_infos[labor].mode() == HAULERS) - out << "haulers"; - else - out << "minimum " << labor_infos[labor].minimum_dwarfs() << ", maximum " << labor_infos[labor].maximum_dwarfs() - << ", pool " << labor_infos[labor].talent_pool(); - out << ", currently " << labor_infos[labor].active_dwarfs << " dwarfs" << std::endl; - } + std::string status; + if (is_enabled && active_engine == &modern_engine) + status = modern_engine.status_line(); + Lua::Push(L, status); + return 1; } -DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable ) +static int autolabor_getBalanceStops(lua_State *L) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot enable {} without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } + for (int i = 0; i < NUM_BALANCE_STOPS; i++) + Lua::Push(L, balance_stop_name(i)); + return NUM_BALANCE_STOPS; +} - if (enable && !enable_autolabor) - { - enable_plugin(out); - } - else if(!enable && enable_autolabor) - { - disable_plugin(out); - } +static int autolabor_getBalance(lua_State *L) +{ + Lua::Push(L, balance()); + return 1; +} - return CR_OK; +static void autolabor_setBalance(color_ostream &out, int32_t stop) +{ + if (can_run(out)) + store_balance(stop); } -command_result autolabor (color_ostream &out, std::vector & parameters) +static int autolabor_getIdleReserve(lua_State *L) { - if (!Core::getInstance().isMapLoaded() || !World::isFortressMode()) { - out.printerr("Cannot run {} without a loaded fort.\n", plugin_name); - return CR_FAILURE; - } + Lua::Push(L, idle_reserve()); + return 1; +} - if (parameters.size() == 1 && - (parameters[0] == "0" || parameters[0] == "enable" || - parameters[0] == "1" || parameters[0] == "disable")) +// starving job postings on the board; only tracked while the modern or +// monitor engine is running. returns the count plus, when nonzero, the +// caption, age (ticks), and map position of the longest-starving posting +static int autolabor_getStarvingJobs(lua_State *L) +{ + int count = 0; + std::string name; + int32_t wait = 0; + df::coord pos; + if (is_enabled && active_engine == &modern_engine) { - bool enable = (parameters[0] == "1" || parameters[0] == "enable"); - - return plugin_enable(out, enable); + count = modern_engine.starving_jobs(); + name = modern_engine.oldest_starving_name(); + wait = modern_engine.oldest_starving_wait(); + pos = modern_engine.oldest_starving_pos(); } - else if (parameters.size() == 2 && parameters[0] == "haulpct") + else if (is_enabled && active_engine == &monitor_engine) { - if (!enable_autolabor) - { - out << "Error: The plugin is not enabled." << std::endl; - return CR_FAILURE; - } - - int pct = atoi (parameters[1].c_str()); - hauler_pct = pct; - return CR_OK; + count = monitor_engine.starving_jobs(); + name = monitor_engine.oldest_starving_name(); + wait = monitor_engine.oldest_starving_wait(); + pos = monitor_engine.oldest_starving_pos(); } - else if (parameters.size() >= 2 && parameters.size() <= 4) + Lua::Push(L, count); + if (count > 0) { - if (!enable_autolabor) - { - out << "Error: The plugin is not enabled." << std::endl; - return CR_FAILURE; - } - - df::unit_labor labor = unit_labor::NONE; - - FOR_ENUM_ITEMS(unit_labor, test_labor) - { - if (parameters[0] == ENUM_KEY_STR(unit_labor, test_labor)) - labor = test_labor; - } - - if (labor == unit_labor::NONE) - { - out.printerr("Could not find labor {}.\n", parameters[0]); - return CR_WRONG_USAGE; - } - - if (parameters[1] == "haulers") - { - labor_infos[labor].set_mode(HAULERS); - print_labor(labor, out); - return CR_OK; - } - if (parameters[1] == "disable") - { - labor_infos[labor].set_mode(DISABLE); - print_labor(labor, out); - return CR_OK; - } - if (parameters[1] == "reset") - { - reset_labor(labor); - print_labor(labor, out); - return CR_OK; - } - - int minimum = atoi (parameters[1].c_str()); - int maximum = 200; - int pool = 200; - - if (parameters.size() >= 3) - maximum = atoi (parameters[2].c_str()); - if (parameters.size() == 4) - pool = std::stoi(parameters[3]); - - if (maximum < minimum || maximum < 0 || minimum < 0) - { - out.printerr("Syntax: autolabor [] [], {} > {}\n", maximum, minimum); - return CR_WRONG_USAGE; - } - - labor_infos[labor].set_minimum_dwarfs(minimum); - labor_infos[labor].set_maximum_dwarfs(maximum); - labor_infos[labor].set_talent_pool(pool); - labor_infos[labor].set_mode(AUTOMATIC); - print_labor(labor, out); - - return CR_OK; + Lua::Push(L, name); + Lua::Push(L, (int32_t)wait); + Lua::Push(L, (int32_t)pos.x); + Lua::Push(L, (int32_t)pos.y); + Lua::Push(L, (int32_t)pos.z); + return 6; } - else if (parameters.size() == 1 && parameters[0] == "reset-all") - { - if (!enable_autolabor) - { - out << "Error: The plugin is not enabled." << std::endl; - return CR_FAILURE; - } + return 1; +} - for (size_t i = 0; i < labor_infos.size(); i++) - { - reset_labor((df::unit_labor) i); - } - out << "All labors reset." << std::endl; - return CR_OK; - } - else if (parameters.size() == 1 && (parameters[0] == "list" || parameters[0] == "status")) - { - if (!enable_autolabor) - { - out << "Error: The plugin is not enabled." << std::endl; - return CR_FAILURE; - } +static void autolabor_setIdleReserve(color_ostream &out, int32_t pct) +{ + if (can_run(out)) + store_idle_reserve(pct); +} - bool need_comma = 0; - for (int i = 0; i < NUM_STATE; i++) - { - if (state_count[i] == 0) - continue; - if (need_comma) - out << ", "; - out << state_count[i] << ' ' << state_names[i]; - need_comma = 1; - } - out << std::endl; +// returns a list of unit_labor enum names that the engines' static labor +// data doesn't cover; used by the labor coverage test in scripts/test +static int autolabor_checkLaborCoverage(lua_State *L) +{ + init_labor_to_skill(); + std::vector missing; + legacy_labor_coverage(missing); + modern_labor_coverage(missing); + Lua::Push(L, missing); + return 1; +} - if (parameters[0] == "list") - { - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE) - continue; +// returns a list of job_type enum names with no entry in the job to labor +// table; used by the job coverage test in scripts/test +static int autolabor_checkJobCoverage(lua_State *L) +{ + std::vector missing; + JobLaborMapper mapper; + mapper.job_coverage(missing); + Lua::Push(L, missing); + return 1; +} - print_labor(labor, out); - } - } +static void autolabor_setMode(color_ostream &out, int32_t mode) +{ + if (!can_run(out)) + return; + if (mode != MODE_LEGACY && mode != MODE_MODERN && mode != MODE_MONITOR) + return; + activate_mode(out, mode); +} - return CR_OK; - } - else - { - out.print("Automatically assigns labors to dwarves.\n" - "Activate with 'enable autolabor', deactivate with 'disable autolabor'.\n" - "Current state: {}.\n", enable_autolabor); +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(autolabor_setMode), + DFHACK_LUA_FUNCTION(autolabor_setBalance), + DFHACK_LUA_FUNCTION(autolabor_setIdleReserve), + DFHACK_LUA_END +}; - return CR_OK; - } -} +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(autolabor_getMode), + DFHACK_LUA_COMMAND(autolabor_getStatus), + DFHACK_LUA_COMMAND(autolabor_getBalance), + DFHACK_LUA_COMMAND(autolabor_getBalanceStops), + DFHACK_LUA_COMMAND(autolabor_getIdleReserve), + DFHACK_LUA_COMMAND(autolabor_getStarvingJobs), + DFHACK_LUA_COMMAND(autolabor_checkLaborCoverage), + DFHACK_LUA_COMMAND(autolabor_checkJobCoverage), + DFHACK_LUA_END +}; diff --git a/plugins/autolabor/engines.h b/plugins/autolabor/engines.h new file mode 100644 index 0000000000..b236f34f89 --- /dev/null +++ b/plugins/autolabor/engines.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "ColorText.h" +#include "PluginManager.h" + +using namespace DFHack; + +// One labor-management strategy. Exactly one engine is active at a time, +// selected by the plugin's persisted mode. Engines are stateless between +// update() calls except for persisted config and small persistent caches. +class LaborEngine { +public: + virtual ~LaborEngine() {} + + // Called when the plugin is enabled in this engine's mode (including on + // map load). Must leave the fort's labor state consistent. + virtual void enable(color_ostream &out) = 0; + + // Called when the plugin is disabled or the mode is switched away. + // Must undo any labor-affecting state so vanilla behaves normally. + virtual void disable(color_ostream &out) = 0; + + // Called every frame while active; engines throttle internally. + virtual void update(color_ostream &out) = 0; + + // Handle the engine's command dialect. Returns false if the command + // isn't recognized so the caller can try shared handling. + virtual bool command(color_ostream &out, std::vector ¶meters) = 0; +}; diff --git a/plugins/autolabor/joblabormapper.cpp b/plugins/autolabor/joblabormapper.cpp index c1d773e046..4d991f58e2 100644 --- a/plugins/autolabor/joblabormapper.cpp +++ b/plugins/autolabor/joblabormapper.cpp @@ -15,8 +15,18 @@ */ +#include "joblabormapper.h" + +#include +#include +#include +#include + +#include "Core.h" #include "DataDefs.h" +#include "Debug.h" #include "MiscUtils.h" + #include "modules/Materials.h" #include @@ -26,31 +36,23 @@ #include #include #include - +#include #include - #include #include #include - +#include #include #include - #include #include #include - +#include #include - #include - #include - #include -#include -#include - using namespace std; using std::string; using std::endl; @@ -59,8 +61,19 @@ using namespace df::enums; using df::global::plotinfo; using df::global::world; -#include "labormanager.h" -#include "joblabormapper.h" +namespace DFHack { + DBG_DECLARE(autolabor, mapper, DebugCategory::LWARNING); +} + +// stand-ins for the old labormanager debug hooks; these only fire on +// failure-to-map paths that indicate a gap in the labor tables +template +static void jlm_debug(fmt::format_string fmt, Args&&... args) +{ + WARN(mapper, Core::getInstance().getConsole()).print("{}", + fmt::format(fmt, std::forward(args)...)); +} +static void jlm_debug_pause() {} static df::unit_labor hauling_labor_map[] = { @@ -302,8 +315,6 @@ class jlfunc_construct_bld : public jlfunc case df::building_type::Workshop: { df::building_workshopst* ws = (df::building_workshopst*) bld; - if (ws->design && !ws->design->flags.bits.designed) - return df::unit_labor::ARCHITECT; if (ws->type == df::workshop_type::Custom) { df::building_def* def = df::building_def::find(ws->custom_type); @@ -316,8 +327,6 @@ class jlfunc_construct_bld : public jlfunc case df::building_type::Furnace: { df::building_furnacest* frn = (df::building_furnacest*) bld; - if (frn->design && !frn->design->flags.bits.designed) - return df::unit_labor::ARCHITECT; if (frn->type == df::furnace_type::Custom) { df::building_def* def = df::building_def::find(frn->custom_type); @@ -343,8 +352,6 @@ class jlfunc_construct_bld : public jlfunc case df::building_type::Windmill: { df::building_actual* b = (df::building_actual*) bld; - if (b->design && !b->design->flags.bits.designed) - return df::unit_labor::ARCHITECT; return construction_build_labor(b); } break; @@ -397,9 +404,9 @@ class jlfunc_construct_bld : public jlfunc return df::unit_labor::BUILD_ROAD; } - debug("LABORMANAGER: Cannot deduce labor for construct building job of type %s\n", - ENUM_KEY_STR(building_type, bld->getType()).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for construct building job of type {}\n", + ENUM_KEY_STR(building_type, bld->getType())); + jlm_debug_pause(); return df::unit_labor::NONE; } @@ -512,9 +519,9 @@ class jlfunc_destroy_bld : public jlfunc return df::unit_labor::SIEGECRAFT; } - debug("LABORMANAGER: Cannot deduce labor for destroy building job of type %s\n", - ENUM_KEY_STR(building_type, type).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for destroy building job of type {}\n", + ENUM_KEY_STR(building_type, type)); + jlm_debug_pause(); return df::unit_labor::NONE; } @@ -550,8 +557,8 @@ class jlfunc_make : public jlfunc return df::unit_labor::BONE_CARVE; else { - debug("LABORMANAGER: Cannot deduce labor for make crafts job (not bone)\n"); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for make crafts job (not bone)\n"); + jlm_debug_pause(); return df::unit_labor::NONE; } case df::item_type::WOOD: @@ -561,9 +568,9 @@ class jlfunc_make : public jlfunc case df::item_type::SKIN_TANNED: return df::unit_labor::LEATHER; default: - debug("LABORMANAGER: Cannot deduce labor for make crafts job, item type %s\n", - ENUM_KEY_STR(item_type, jobitem).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for make crafts job, item type {}\n", + ENUM_KEY_STR(item_type, jobitem)); + jlm_debug_pause(); return df::unit_labor::NONE; } } @@ -581,9 +588,9 @@ class jlfunc_make : public jlfunc case df::workshop_type::MetalsmithsForge: return metaltype; default: - debug("LABORMANAGER: Cannot deduce labor for make job, workshop type %s\n", - ENUM_KEY_STR(workshop_type, type).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for make job, workshop type {}\n", + ENUM_KEY_STR(workshop_type, type)); + jlm_debug_pause(); return df::unit_labor::NONE; } } @@ -596,16 +603,16 @@ class jlfunc_make : public jlfunc case df::furnace_type::GlassFurnace: return df::unit_labor::GLASSMAKER; default: - debug("LABORMANAGER: Cannot deduce labor for make job, furnace type %s\n", - ENUM_KEY_STR(furnace_type, type).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for make job, furnace type {}\n", + ENUM_KEY_STR(furnace_type, type)); + jlm_debug_pause(); return df::unit_labor::NONE; } } - debug("LABORMANAGER: Cannot deduce labor for make job, building type %s\n", - ENUM_KEY_STR(building_type, bld->getType()).c_str()); - debug_pause(); + jlm_debug("LABORMANAGER: Cannot deduce labor for make job, building type {}\n", + ENUM_KEY_STR(building_type, bld->getType())); + jlm_debug_pause(); return df::unit_labor::NONE; } @@ -685,9 +692,9 @@ JobLaborMapper::JobLaborMapper() jlfunc* jlf_no_labor = jlf_const(df::unit_labor::NONE); - job_to_labor_table[df::job_type::CarveFortification] = jlf_const(df::unit_labor::DETAIL); - job_to_labor_table[df::job_type::DetailWall] = jlf_const(df::unit_labor::DETAIL); - job_to_labor_table[df::job_type::DetailFloor] = jlf_const(df::unit_labor::DETAIL); + job_to_labor_table[df::job_type::CarveFortification] = jlf_const(df::unit_labor::ENGRAVER); + job_to_labor_table[df::job_type::DetailWall] = jlf_const(df::unit_labor::ENGRAVER); + job_to_labor_table[df::job_type::DetailFloor] = jlf_const(df::unit_labor::ENGRAVER); job_to_labor_table[df::job_type::Dig] = jlf_const(df::unit_labor::MINE); job_to_labor_table[df::job_type::CarveUpwardStaircase] = jlf_const(df::unit_labor::MINE); job_to_labor_table[df::job_type::CarveDownwardStaircase] = jlf_const(df::unit_labor::MINE); @@ -719,19 +726,16 @@ JobLaborMapper::JobLaborMapper() job_to_labor_table[df::job_type::CatchLiveLandAnimal] = jlf_const(df::unit_labor::HUNT); job_to_labor_table[df::job_type::CatchLiveFish] = jlf_const(df::unit_labor::FISH); job_to_labor_table[df::job_type::ReturnKill] = jlf_no_labor; - job_to_labor_table[df::job_type::CheckChest] = jlf_no_labor; job_to_labor_table[df::job_type::StoreOwnedItem] = jlf_no_labor; job_to_labor_table[df::job_type::PlaceItemInTomb] = jlf_const(df::unit_labor::HAUL_BODY); job_to_labor_table[df::job_type::StoreItemInStockpile] = jlf_hauling; job_to_labor_table[df::job_type::StoreItemInBag] = jlf_hauling; - job_to_labor_table[df::job_type::StoreItemInHospital] = jlf_hauling; job_to_labor_table[df::job_type::StoreWeapon] = jlf_hauling; job_to_labor_table[df::job_type::StoreArmor] = jlf_hauling; job_to_labor_table[df::job_type::StoreItemInBarrel] = jlf_hauling; job_to_labor_table[df::job_type::StoreItemInBin] = jlf_hauling; job_to_labor_table[df::job_type::SeekArtifact] = jlf_no_labor; job_to_labor_table[df::job_type::SeekInfant] = jlf_no_labor; - job_to_labor_table[df::job_type::AttendParty] = jlf_no_labor; job_to_labor_table[df::job_type::GoShopping] = jlf_no_labor; job_to_labor_table[df::job_type::GoShoppingSpecific] = jlf_no_labor; job_to_labor_table[df::job_type::Clean] = jlf_const(df::unit_labor::CLEAN); @@ -828,7 +832,6 @@ JobLaborMapper::JobLaborMapper() job_to_labor_table[df::job_type::LoadStoneTrap] = jlf_const(df::unit_labor::MECHANIC); job_to_labor_table[df::job_type::LoadWeaponTrap] = jlf_const(df::unit_labor::MECHANIC); job_to_labor_table[df::job_type::CleanTrap] = jlf_const(df::unit_labor::MECHANIC); - job_to_labor_table[df::job_type::CastSpell] = jlf_no_labor; job_to_labor_table[df::job_type::LinkBuildingToTrigger] = jlf_const(df::unit_labor::MECHANIC); job_to_labor_table[df::job_type::PullLever] = jlf_const(df::unit_labor::PULL_LEVER); job_to_labor_table[df::job_type::ExtractFromPlants] = jlf_const(df::unit_labor::HERBALIST); @@ -893,7 +896,7 @@ JobLaborMapper::JobLaborMapper() job_to_labor_table[df::job_type::ApplyCast] = jlf_const(df::unit_labor::BONE_SETTING); job_to_labor_table[df::job_type::CustomReaction] = new jlfunc_custom(); job_to_labor_table[df::job_type::ConstructSlab] = jlf_make_furniture; - job_to_labor_table[df::job_type::EngraveSlab] = jlf_const(df::unit_labor::DETAIL); + job_to_labor_table[df::job_type::EngraveSlab] = jlf_const(df::unit_labor::ENGRAVER); job_to_labor_table[df::job_type::ShearCreature] = jlf_const(df::unit_labor::SHEARER); job_to_labor_table[df::job_type::SpinThread] = jlf_const(df::unit_labor::SPINNER); job_to_labor_table[df::job_type::PenLargeAnimal] = jlf_const(df::unit_labor::HAUL_ANIMALS); @@ -907,7 +910,7 @@ JobLaborMapper::JobLaborMapper() job_to_labor_table[df::job_type::ReportCrime] = jlf_no_labor; job_to_labor_table[df::job_type::ExecuteCriminal] = jlf_no_labor; job_to_labor_table[df::job_type::TrainAnimal] = jlf_const(df::unit_labor::ANIMALTRAIN); - job_to_labor_table[df::job_type::CarveTrack] = jlf_const(df::unit_labor::DETAIL); + job_to_labor_table[df::job_type::CarveTrack] = jlf_const(df::unit_labor::ENGRAVER); job_to_labor_table[df::job_type::PushTrackVehicle] = jlf_const(df::unit_labor::HANDLE_VEHICLES); job_to_labor_table[df::job_type::PlaceTrackVehicle] = jlf_const(df::unit_labor::HANDLE_VEHICLES); job_to_labor_table[df::job_type::StoreItemInVehicle] = jlf_hauling; @@ -927,8 +930,45 @@ JobLaborMapper::JobLaborMapper() job_to_labor_table[df::job_type::HeistItem] = jlf_no_labor; // added for 47.04 - see #1561 job_to_labor_table[df::job_type::InterrogateSubject] = jlf_no_labor; // added for 47.04 - see #1561 job_to_labor_table[df::job_type::AcceptHeistItem] = jlf_no_labor; // added for 47.04 - see #1561 + + // v50 additions + job_to_labor_table[df::job_type::SmoothWall] = jlf_const(df::unit_labor::ENGRAVER); + job_to_labor_table[df::job_type::SmoothFloor] = jlf_const(df::unit_labor::ENGRAVER); + job_to_labor_table[df::job_type::PolishStones] = jlf_const(df::unit_labor::STONE_CRAFT); + job_to_labor_table[df::job_type::ConstructBag] = jlf_const(df::unit_labor::LEATHER); + job_to_labor_table[df::job_type::EncrustWithStones] = jlf_const(df::unit_labor::ENCRUST_GEM); + job_to_labor_table[df::job_type::StoreSquadEquipmentItem] = jlf_no_labor; + job_to_labor_table[df::job_type::MixDye] = jlf_const(df::unit_labor::DYER); + job_to_labor_table[df::job_type::DyeLeather] = jlf_const(df::unit_labor::DYER); + job_to_labor_table[df::job_type::ConstructBoltThrowerParts] = jlf_const(df::unit_labor::SIEGECRAFT); + job_to_labor_table[df::job_type::LoadBoltThrower] = jlf_const(df::unit_labor::SIEGEOPERATE); + job_to_labor_table[df::job_type::FireBoltThrower] = jlf_const(df::unit_labor::SIEGEOPERATE); + job_to_labor_table[df::job_type::UNUSED_31] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_32] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_33] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_34] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_35] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_36] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_37] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_38] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_39] = jlf_no_labor; + job_to_labor_table[df::job_type::UNUSED_40] = jlf_no_labor; }; +void JobLaborMapper::job_coverage(std::vector &missing) +{ + FOR_ENUM_ITEMS(job_type, jt) + { + if (jt < 0) + continue; + // a job type with no table entry is invisible to labor management; + // DF releases that add job types must add a mapping (or jlf_no_labor) + if (!job_to_labor_table.count(jt)) + missing.push_back(ENUM_KEY_STR(job_type, jt) + + ": no entry in job to labor table"); + } +} + df::unit_labor JobLaborMapper::find_job_labor(df::job* j) { if (j->job_type == df::job_type::CustomReaction) @@ -948,8 +988,8 @@ df::unit_labor JobLaborMapper::find_job_labor(df::job* j) df::unit_labor labor; if (job_to_labor_table.count(j->job_type) == 0) { - debug("LABORMANAGER: job has no job to labor table entry: %s (%d)\n", ENUM_KEY_STR(job_type, j->job_type).c_str(), j->job_type); - debug_pause(); + jlm_debug("LABORMANAGER: job has no job to labor table entry: {} ({})\n", ENUM_KEY_STR(job_type, j->job_type), int(j->job_type)); + jlm_debug_pause(); labor = df::unit_labor::NONE; } else { diff --git a/plugins/autolabor/joblabormapper.h b/plugins/autolabor/joblabormapper.h index 03e06a67f4..a117de384c 100644 --- a/plugins/autolabor/joblabormapper.h +++ b/plugins/autolabor/joblabormapper.h @@ -1,15 +1,17 @@ #pragma once #include +#include +#include -using namespace DFHack; -using namespace df::enums; - #include "df/job.h" #include "df/job_type.h" #include "df/unit_labor.h" +using namespace DFHack; +using namespace df::enums; + class jlfunc; @@ -27,5 +29,9 @@ class JobLaborMapper { df::unit_labor find_job_labor(df::job* j); + // appends names of job_type enum items with no entry in the labor + // table; used by the job coverage self-test + void job_coverage(std::vector &missing); + }; diff --git a/plugins/autolabor/laborcommon.cpp b/plugins/autolabor/laborcommon.cpp new file mode 100644 index 0000000000..4b6867d815 --- /dev/null +++ b/plugins/autolabor/laborcommon.cpp @@ -0,0 +1,325 @@ +#include "laborcommon.h" + +#include "modules/Units.h" +#include "modules/World.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +using df::global::plotinfo; +using df::global::world; + +namespace autolabor { + +PersistentDataItem plugin_config; + +void load_plugin_config() +{ + plugin_config = World::GetPersistentSiteData("autolabor/config", true); + if (plugin_config.isValid() && plugin_config.ival(0) == -1) + plugin_config.ival(0) = 0; +} + +bool config_flag(unsigned flag) +{ + return plugin_config.isValid() && (plugin_config.ival(0) & flag) != 0; +} + +void set_config_flag(unsigned flag, bool on) +{ + if (!plugin_config.isValid()) + return; + if (on) + plugin_config.ival(0) |= flag; + else + plugin_config.ival(0) &= ~flag; +} + +int engine_mode() +{ + if (!plugin_config.isValid()) + return MODE_LEGACY; + int m = plugin_config.get_int(1); + return (m >= MODE_LEGACY && m <= MODE_MONITOR) ? m : MODE_LEGACY; +} + +void store_engine_mode(int mode) +{ + if (plugin_config.isValid()) + plugin_config.ival(1) = mode; +} + +int balance() +{ + if (!plugin_config.isValid()) + return NUM_BALANCE_STOPS / 2; + int v = plugin_config.get_int(2); + if (v < 0 || v >= NUM_BALANCE_STOPS) + v = NUM_BALANCE_STOPS / 2; // default: balanced + return v; +} + +void store_balance(int v) +{ + if (plugin_config.isValid() && v >= 0 && v < NUM_BALANCE_STOPS) + plugin_config.ival(2) = v; +} + +static const char * const balance_names[] = { + "staffing", "lean-staffing", "balanced", "lean-skills", "skills" +}; + +const char *balance_stop_name(int i) +{ + if (i < 0 || i >= NUM_BALANCE_STOPS) + return NULL; + return balance_names[i]; +} + +int idle_reserve() +{ + if (!plugin_config.isValid()) + return 30; + int v = plugin_config.get_int(3); + return (v < 0 || v > 100) ? 30 : v; +} + +void store_idle_reserve(int v) +{ + if (plugin_config.isValid() && v >= 0 && v <= 100) + plugin_config.ival(3) = v; +} + +df::job_skill labor_to_skill[NUM_LABORS]; + +void init_labor_to_skill() +{ + for (int i = 0; i < NUM_LABORS; i++) + labor_to_skill[i] = job_skill::NONE; + + FOR_ENUM_ITEMS(job_skill, skill) + { + int labor = ENUM_ATTR(job_skill, labor, skill); + if (labor >= 0 && labor < NUM_LABORS) + labor_to_skill[labor] = skill; + } +} + +bool is_exclusive_labor(df::unit_labor l) +{ + // labors that carry tools; toggling them causes equipment churn + switch (l) { + case unit_labor::MINE: + case unit_labor::CUTWOOD: + case unit_labor::HUNT: + return true; + default: + return false; + } +} + +BuildingScan scan_buildings() +{ + BuildingScan scan; + + for (auto build : world->buildings.all) + { + auto type = build->getType(); + if (building_type::Workshop == type) + { + df::workshop_type subType = (df::workshop_type)build->getSubtype(); + if (workshop_type::Butchers == subType) + scan.has_butchers = true; + if (workshop_type::Fishery == subType) + scan.has_fishery = true; + } + else if (building_type::TradeDepot == type) + { + df::building_tradedepotst* depot = + strict_virtual_cast(build); + if (depot) + scan.trader_requested = + scan.trader_requested || depot->trade_flags.bits.trader_requested; + } + } + + return scan; +} + +static const int responsibility_penalties[] = { + 0, /* LAW_MAKING */ + 0, /* LAW_ENFORCEMENT */ + 3000, /* RECEIVE_DIPLOMATS */ + 0, /* MEET_WORKERS */ + 1000, /* MANAGE_PRODUCTION */ + 3000, /* TRADE */ + 1000, /* ACCOUNTING */ + 0, /* ESTABLISH_COLONY_TRADE_AGREEMENTS */ + 0, /* MAKE_INTRODUCTIONS */ + 0, /* MAKE_PEACE_AGREEMENTS */ + 0, /* MAKE_TOPIC_AGREEMENTS */ + 0, /* COLLECT_TAXES */ + 0, /* ESCORT_TAX_COLLECTOR */ + 0, /* EXECUTIONS */ + 0, /* TAME_EXOTICS */ + 0, /* RELIGION */ + 0, /* ATTACK_ENEMIES */ + 0, /* PATROL_TERRITORY */ + 0, /* MILITARY_GOALS */ + 0, /* MILITARY_STRATEGY */ + 0, /* UPGRADE_SQUAD_EQUIPMENT */ + 0, /* EQUIPMENT_MANIFESTS */ + 0, /* SORT_AMMUNITION */ + 0, /* BUILD_MORALE */ + 5000 /* HEALTH_MANAGEMENT */ +}; + +PositionInfo unit_position_info(df::unit *u) +{ + PositionInfo info; + + df::historical_figure* hf = df::historical_figure::find(u->hist_figure_id); + if (!hf) // can be NULL, e.g. script-created citizens + return info; + + for (auto hfelink : hf->entity_links) + { + if (hfelink->getType() != df::histfig_entity_link_type::POSITION) + continue; + + df::histfig_entity_link_positionst *epos = + (df::histfig_entity_link_positionst*) hfelink; + df::historical_entity* entity = df::historical_entity::find(epos->entity_id); + if (!entity) + continue; + df::entity_position_assignment* assignment = + binsearch_in_vector(entity->positions.assignments, epos->assignment_id); + if (!assignment) + continue; + df::entity_position* position = + binsearch_in_vector(entity->positions.own, assignment->position_id); + if (!position) + continue; + + for (int n = 0; n < 25; n++) + if (position->responsibilities[n]) + info.noble_penalty += responsibility_penalties[n]; + + if (position->responsibilities[df::entity_position_responsibility::HEALTH_MANAGEMENT]) + info.medical = true; + + if (position->responsibilities[df::entity_position_responsibility::TRADE]) + info.trader = true; + } + + return info; +} + +bool in_diplomacy_meeting(df::unit *u) +{ + for (auto act : plotinfo->activities) + { + if (!act) continue; + if (act->unit_actor == u->id || act->unit_noble == u->id) + return true; + } + return false; +} + +bool is_assignable(df::unit *u) +{ + return Units::isOwnCiv(u) && + Units::isOwnGroup(u) && + Units::isActive(u) && + !u->flags2.bits.visitor && + !u->flags3.bits.ghostly && + ENUM_ATTR(profession, can_assign_labor, u->profession); +} + +dwarf_state get_dwarf_state(df::unit *u) +{ + if (Units::isBaby(u) || Units::isChild(u) || + u->profession == profession::DRUNK) + return dwarf_state::CHILD; + + if (ENUM_ATTR(profession, military, u->profession)) + return dwarf_state::MILITARY; + + if (!u->job.current_job) + { + if (Units::getMiscTrait(u, misc_trait_type::Migrant) || + u->specific_refs.size() > 0) + return dwarf_state::OTHER; + return dwarf_state::IDLE; + } + + int job = u->job.current_job->job_type; + if (job >= 0 && size_t(job) < dwarf_state_count) + return dwarf_states[job]; + + return dwarf_state::OTHER; +} + +int top_skill(df::unit *u, df::job_skill *skill_out) +{ + int best = 0; + df::job_skill best_skill = job_skill::NONE; + + if (u->status.souls.size() > 0) + { + for (auto skill : u->status.souls[0]->skills) + { + df::job_skill_class skill_class = ENUM_ATTR(job_skill, type, skill->id); + if (skill_class != job_skill_class::Normal && + skill_class != job_skill_class::Medical) + continue; + + if (best < skill->rating) + { + best = skill->rating; + best_skill = skill->id; + } + } + } + + if (skill_out) + *skill_out = best_skill; + return best; +} + +int total_skill(df::unit *u) +{ + int total = 0; + if (u->status.souls.size() > 0) + { + for (auto skill : u->status.souls[0]->skills) + { + df::job_skill_class skill_class = ENUM_ATTR(job_skill, type, skill->id); + if (skill_class != job_skill_class::Normal && + skill_class != job_skill_class::Medical) + continue; + total += skill->rating; + } + } + return total; +} + +} diff --git a/plugins/autolabor/laborcommon.h b/plugins/autolabor/laborcommon.h new file mode 100644 index 0000000000..102a05f240 --- /dev/null +++ b/plugins/autolabor/laborcommon.h @@ -0,0 +1,112 @@ +#pragma once + +#include "laborstatemap.h" + +#include +#include + +#include "modules/Persistence.h" + +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +// Shared infrastructure for the autolabor plugin's two engines. + +namespace autolabor { + +// --------------------------------------------------------------------------- +// plugin-global persisted config ("autolabor/config" site data) +// ival(0): flags ival(1): mode ival(2): balance stop (0-4) +// ival(3): idle reserve pct +// --------------------------------------------------------------------------- + +extern PersistentDataItem plugin_config; +void load_plugin_config(); + +enum ConfigFlags { + CF_ENABLED = 1, + CF_ALLOW_FISHING = 2, + CF_ALLOW_HUNTING = 4, +}; + +enum EngineMode { + MODE_LEGACY = 0, // classic autolabor: writes unit->status.labors directly + MODE_MODERN = 1, // labormanager: works through the work detail system + MODE_MONITOR = 2, // starvation warnings only; no labor management +}; + +bool config_flag(unsigned flag); +void set_config_flag(unsigned flag, bool on); + +int engine_mode(); +void store_engine_mode(int mode); + +int balance(); // 0..4 slider stop; 0 = max staffing +void store_balance(int v); +int idle_reserve(); // percent idle time reserved for guilded specialists +void store_idle_reserve(int v); + +const int NUM_BALANCE_STOPS = 5; +// CLI/overlay label for a balance stop; NULL if out of range +const char *balance_stop_name(int i); + +// --------------------------------------------------------------------------- + +const int NUM_LABORS = ENUM_LAST_ITEM(unit_labor) + 1; + +// labor_to_skill[l] is the job_skill trained by labor l, or job_skill::NONE +// for labors that require no skill. +extern df::job_skill labor_to_skill[]; +void init_labor_to_skill(); + +inline bool is_unskilled(df::unit_labor l) { + return l >= 0 && l < NUM_LABORS && labor_to_skill[l] == job_skill::NONE; +} + +// does the labor require a tool (and thus churn equipment when toggled)? +bool is_exclusive_labor(df::unit_labor l); + +struct BuildingScan { + bool has_butchers = false; + bool has_fishery = false; + bool trader_requested = false; +}; +BuildingScan scan_buildings(); + +struct PositionInfo { + int noble_penalty = 0; + bool medical = false; + bool trader = false; +}; +// noble status, medical/trade responsibilities from entity positions +PositionInfo unit_position_info(df::unit *u); + +// is the unit currently involved in a diplomatic/noble meeting? +bool in_diplomacy_meeting(df::unit *u); + +// --------------------------------------------------------------------------- +// labor coverage self-checks +// --------------------------------------------------------------------------- + +// Appends names of unit_labor enum items that the engine's static labor +// data doesn't cover (e.g. a labor added by a new DF release that has no +// entry in a fixed-size table). Used by the autolabor_checkLaborCoverage +// lua function; an empty result means every labor is handled. +void legacy_labor_coverage(std::vector &missing); +void modern_labor_coverage(std::vector &missing); + +// can the unit take labors at all (own civ/group, active, not visitor/ghost)? +bool is_assignable(df::unit *u); + +// classify what the unit is currently doing +dwarf_state get_dwarf_state(df::unit *u); + +// highest rating in a Normal or Medical skill; also reports the skill id +int top_skill(df::unit *u, df::job_skill *skill_out = nullptr); +int total_skill(df::unit *u); + +} diff --git a/plugins/autolabor/labormanager.cpp b/plugins/autolabor/labormanager.cpp deleted file mode 100644 index f72be79913..0000000000 --- a/plugins/autolabor/labormanager.cpp +++ /dev/null @@ -1,2145 +0,0 @@ -/* -* Labor manager (formerly Autolabor 2) module for dfhack -* -* */ - - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "modules/Units.h" -#include "modules/World.h" -#include "modules/Maps.h" -#include "modules/MapCache.h" -#include "modules/Items.h" - -// DF data structure definition headers -#include "DataDefs.h" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "labormanager.h" -#include "joblabormapper.h" - -#include "laborstatemap.h" - -using namespace std; -using std::string; -using std::endl; -using namespace DFHack; -using namespace df::enums; -using df::global::plotinfo; -using df::global::world; - -#define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) - -DFHACK_PLUGIN_IS_ENABLED(enable_labormanager); - -static bool print_debug = 0; -static bool pause_on_error = 1; - -static std::vector state_count(5); - -static PersistentDataItem config; - -enum ConfigFlags { - CF_ENABLED = 1, - CF_ALLOW_FISHING = 2, - CF_ALLOW_HUNTING = 4, -}; - -// Value of 0 for max dwarfs means uncapped. -const int MAX_DWARFS_NONE = 0; -// Value < 0 for max dwarfs means don't manager the labor. -const int MAX_DWARFS_UNMANAGED = -1; - - -// Here go all the command declarations... -// mostly to allow having the mandatory stuff on top of the file and commands on the bottom -command_result labormanager(color_ostream &out, std::vector & parameters); - -// A plugin must be able to return its name and version. -// The name string provided must correspond to the filename - labormanager.plug.so or labormanager.plug.dll in this case -DFHACK_PLUGIN("labormanager"); - -static void generate_labor_to_skill_map(); - -struct labor_info -{ - PersistentDataItem config; - - int active_dwarfs; - int idle_dwarfs; - int busy_dwarfs; - - int priority() const { return config.ival(1); } - void set_priority(int priority) { config.ival(1) = priority; } - - bool is_unmanaged() const { return maximum_dwarfs() == MAX_DWARFS_UNMANAGED; } - int maximum_dwarfs() const { return config.ival(2); } - void set_maximum_dwarfs(int maximum_dwarfs) { config.ival(2) = maximum_dwarfs; } - - int time_since_last_assigned() const - { - return (*df::global::cur_year - config.ival(3)) * 403200 + *df::global::cur_year_tick - config.ival(4); - } - - void mark_assigned() { - config.ival(3) = (*df::global::cur_year); - config.ival(4) = (*df::global::cur_year_tick); - } - -}; - -enum tools_enum { - TOOL_NONE, TOOL_PICK, TOOL_AXE, TOOL_CROSSBOW, - TOOLS_MAX -}; - -struct labor_default -{ - int priority; - int maximum_dwarfs; - tools_enum tool; -}; - -static std::vector labor_infos; - -static const struct labor_default default_labor_infos[] = { - /* MINE */ {100, 0, TOOL_PICK}, - /* HAUL_STONE */ {100, 0, TOOL_NONE}, - /* HAUL_WOOD */ {100, 0, TOOL_NONE}, - /* HAUL_BODY */ {1000, 0, TOOL_NONE}, - /* HAUL_FOOD */ {500, 0, TOOL_NONE}, - /* HAUL_REFUSE */ {200, 0, TOOL_NONE}, - /* HAUL_ITEM */ {100, 0, TOOL_NONE}, - /* HAUL_FURNITURE */ {100, 0, TOOL_NONE}, - /* HAUL_ANIMAL */ {100, 0, TOOL_NONE}, - /* CLEAN */ {100, 0, TOOL_NONE}, - /* CUTWOOD */ {100, 0, TOOL_AXE}, - /* CARPENTER */ {100, 0, TOOL_NONE}, - /* DETAIL */ {100, 0, TOOL_NONE}, - /* MASON */ {100, 0, TOOL_NONE}, - /* ARCHITECT */ {100, 0, TOOL_NONE}, - /* ANIMALTRAIN */ {100, 0, TOOL_NONE}, - /* ANIMALCARE */ {100, 0, TOOL_NONE}, - /* DIAGNOSE */ {1000, 0, TOOL_NONE}, - /* SURGERY */ {1000, 0, TOOL_NONE}, - /* BONE_SETTING */ {1000, 0, TOOL_NONE}, - /* SUTURING */ {1000, 0, TOOL_NONE}, - /* DRESSING_WOUNDS */ {1000, 0, TOOL_NONE}, - /* FEED_WATER_CIVILIANS */ {1000, 0, TOOL_NONE}, - /* RECOVER_WOUNDED */ {200, 0, TOOL_NONE}, - /* BUTCHER */ {500, 0, TOOL_NONE}, - /* TRAPPER */ {100, 0, TOOL_NONE}, - /* DISSECT_VERMIN */ {100, 0, TOOL_NONE}, - /* LEATHER */ {100, 0, TOOL_NONE}, - /* TANNER */ {100, 0, TOOL_NONE}, - /* BREWER */ {100, 0, TOOL_NONE}, - /* ALCHEMIST */ {100, 0, TOOL_NONE}, - /* SOAP_MAKER */ {100, 0, TOOL_NONE}, - /* WEAVER */ {100, 0, TOOL_NONE}, - /* CLOTHESMAKER */ {100, 0, TOOL_NONE}, - /* MILLER */ {100, 0, TOOL_NONE}, - /* PROCESS_PLANT */ {100, 0, TOOL_NONE}, - /* MAKE_CHEESE */ {100, 0, TOOL_NONE}, - /* MILK */ {100, 0, TOOL_NONE}, - /* COOK */ {100, 0, TOOL_NONE}, - /* PLANT */ {100, 0, TOOL_NONE}, - /* HERBALIST */ {100, 0, TOOL_NONE}, - /* FISH */ {100, 0, TOOL_NONE}, - /* CLEAN_FISH */ {100, 0, TOOL_NONE}, - /* DISSECT_FISH */ {100, 0, TOOL_NONE}, - /* HUNT */ {100, 0, TOOL_CROSSBOW}, - /* SMELT */ {100, 0, TOOL_NONE}, - /* FORGE_WEAPON */ {100, 0, TOOL_NONE}, - /* FORGE_ARMOR */ {100, 0, TOOL_NONE}, - /* FORGE_FURNITURE */ {100, 0, TOOL_NONE}, - /* METAL_CRAFT */ {100, 0, TOOL_NONE}, - /* CUT_GEM */ {100, 0, TOOL_NONE}, - /* ENCRUST_GEM */ {100, 0, TOOL_NONE}, - /* WOOD_CRAFT */ {100, 0, TOOL_NONE}, - /* STONE_CRAFT */ {100, 0, TOOL_NONE}, - /* BONE_CARVE */ {100, 0, TOOL_NONE}, - /* GLASSMAKER */ {100, 0, TOOL_NONE}, - /* EXTRACT_STRAND */ {100, 0, TOOL_NONE}, - /* SIEGECRAFT */ {100, 0, TOOL_NONE}, - /* SIEGEOPERATE */ {100, 0, TOOL_NONE}, - /* BOWYER */ {100, 0, TOOL_NONE}, - /* MECHANIC */ {100, 0, TOOL_NONE}, - /* POTASH_MAKING */ {100, 0, TOOL_NONE}, - /* LYE_MAKING */ {100, 0, TOOL_NONE}, - /* DYER */ {100, 0, TOOL_NONE}, - /* BURN_WOOD */ {100, 0, TOOL_NONE}, - /* OPERATE_PUMP */ {100, 0, TOOL_NONE}, - /* SHEARER */ {100, 0, TOOL_NONE}, - /* SPINNER */ {100, 0, TOOL_NONE}, - /* POTTERY */ {100, 0, TOOL_NONE}, - /* GLAZING */ {100, 0, TOOL_NONE}, - /* PRESSING */ {100, 0, TOOL_NONE}, - /* BEEKEEPING */ {100, 0, TOOL_NONE}, - /* WAX_WORKING */ {100, 0, TOOL_NONE}, - /* PUSH_HAUL_VEHICLES */ {100, 0, TOOL_NONE}, - /* HAUL_TRADE */ {1000, 0, TOOL_NONE}, - /* PULL_LEVER */ {1000, 0, TOOL_NONE}, - /* UNUSED_13 */ {0, 0, TOOL_NONE}, - /* HAUL_WATER */ {100, 0, TOOL_NONE}, - /* GELD */ {100, 0, TOOL_NONE}, - /* BUILD_ROAD */ {100, 0, TOOL_NONE}, - /* BUILD_CONSTRUCTION */ {100, 0, TOOL_NONE}, - /* PAPERMAKING */ {100, 0, TOOL_NONE}, - /* BOOKBINDING */ {100, 0, TOOL_NONE} -}; - - - -struct dwarf_info_t -{ - df::unit* dwarf; - dwarf_state state; - - bool clear_all; - - bool has_tool[TOOLS_MAX]; - - int high_skill; - - bool has_children; - bool armed; - - int unmanaged_labors_assigned; - - df::unit_labor using_labor; - - dwarf_info_t(df::unit* dw) : dwarf(dw), state(OTHER), - clear_all(false), high_skill(0), has_children(false), armed(false), - unmanaged_labors_assigned(0), using_labor(df::unit_labor::NONE) - { - for (int e = TOOL_NONE; e < TOOLS_MAX; e++) - has_tool[e] = false; - } - - ~dwarf_info_t() - { - } - -}; - -color_ostream* debug_stream; - -void debug(const char* fmt, ...) -{ - if (debug_stream) - { - va_list args; - va_start(args, fmt); - debug_stream->vprint(fmt, args); - va_end(args); - } -} - -void debug_pause() -{ - if (pause_on_error) - { - debug("LABORMANAGER: Game paused so you can investigate the above message.\nUse 'labormanager pause-on-error no' to disable autopausing.\n"); - *df::global::pause_state = true; - } -} - -static JobLaborMapper* labor_mapper = 0; - -static bool initialized = false; - -static bool isOptionEnabled(unsigned flag) -{ - return config.isValid() && (config.ival(0) & flag) != 0; -} - -static void setOptionEnabled(ConfigFlags flag, bool on) -{ - if (!config.isValid()) - return; - - if (on) - config.ival(0) |= flag; - else - config.ival(0) &= ~flag; -} - -static void cleanup_state() -{ - enable_labormanager = false; - labor_infos.clear(); - initialized = false; -} - -static void reset_labor(df::unit_labor labor) -{ - labor_infos[labor].set_priority(default_labor_infos[labor].priority); - labor_infos[labor].set_maximum_dwarfs(default_labor_infos[labor].maximum_dwarfs); -} - -static void init_state() -{ - config = World::GetPersistentData("labormanager/2.0/config"); - if (config.isValid() && config.ival(0) == -1) - config.ival(0) = 0; - - enable_labormanager = isOptionEnabled(CF_ENABLED); - - if (!enable_labormanager) - return; - - // Load labors from save - labor_infos.resize(ARRAY_COUNT(default_labor_infos)); - - std::vector items; - World::GetPersistentData(&items, "labormanager/2.0/labors/", true); - - for (auto p = items.begin(); p != items.end(); p++) - { - string key = p->key(); - df::unit_labor labor = (df::unit_labor) atoi(key.substr(strlen("labormanager/2.0/labors/")).c_str()); - if (labor >= 0 && size_t(labor) < labor_infos.size()) - { - labor_infos[labor].config = *p; - labor_infos[labor].active_dwarfs = 0; - } - } - - // Add default labors for those not in save - for (size_t i = 0; i < ARRAY_COUNT(default_labor_infos); i++) { - if (labor_infos[i].config.isValid()) - continue; - - std::stringstream name; - name << "labormanager/2.0/labors/" << i; - - labor_infos[i].config = World::AddPersistentData(name.str()); - labor_infos[i].mark_assigned(); - labor_infos[i].active_dwarfs = 0; - reset_labor((df::unit_labor) i); - } - - initialized = true; - -} - -static df::job_skill labor_to_skill[ENUM_LAST_ITEM(unit_labor) + 1]; - -static void generate_labor_to_skill_map() -{ - // Generate labor -> skill mapping - - for (int i = 0; i <= ENUM_LAST_ITEM(unit_labor); i++) - labor_to_skill[i] = job_skill::NONE; - - FOR_ENUM_ITEMS(job_skill, skill) - { - int labor = ENUM_ATTR(job_skill, labor, skill); - if (labor != unit_labor::NONE) - { - /* - assert(labor >= 0); - assert(labor < ARRAY_COUNT(labor_to_skill)); - */ - - labor_to_skill[labor] = skill; - } - } -} - -struct skill_attr_weight { - int phys_attr_weights[6]; - int mental_attr_weights[13]; -}; - -static struct skill_attr_weight skill_attr_weights[ENUM_LAST_ITEM(job_skill) + 1] = -{ - // S A T E R D AA F W C I P M LA SS M KS E SA - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* MINING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WOODCUTTING */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CARPENTRY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* ENGRAVE_STONE */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MASONRY */, - { { 0, 1, 1, 1, 0, 0 }, { 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0 } } /* ANIMALTRAIN */, - { { 0, 1, 0, 0, 0, 0 }, { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0 } } /* ANIMALCARE */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* DISSECT_FISH */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* DISSECT_VERMIN */, - { { 0, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PROCESSFISH */, - { { 0, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BUTCHER */, - { { 0, 1, 0, 0, 0, 0 }, { 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* TRAPPING */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* TANNER */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WEAVING */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BREWING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* ALCHEMY */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* CLOTHESMAKING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MILLING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PROCESSPLANTS */, - { { 1, 1, 0, 1, 0, 0 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CHEESEMAKING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MILK */, - { { 0, 1, 0, 0, 0, 0 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* COOK */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PLANT */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 } } /* HERBALISM */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 } } /* FISH */, - { { 1, 0, 1, 1, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SMELT */, - { { 1, 1, 0, 1, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* EXTRACT_STRAND */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* FORGE_WEAPON */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* FORGE_ARMOR */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* FORGE_FURNITURE */, - { { 0, 1, 0, 0, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CUTGEM */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* ENCRUSTGEM */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WOODCRAFT */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* STONECRAFT */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* METALCRAFT */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* GLASSMAKER */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* LEATHERWORK */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* BONECARVE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* AXE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SWORD */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* DAGGER */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* MACE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* HAMMER */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SPEAR */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* CROSSBOW */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SHIELD */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* ARMOR */, - { { 1, 1, 0, 1, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SIEGECRAFT */, - { { 1, 0, 1, 1, 0, 0 }, { 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SIEGEOPERATE */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BOWYER */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* PIKE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WHIP */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* BOW */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* BLOWGUN */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* THROW */, - { { 1, 1, 0, 1, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MECHANICS */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MAGIC_NATURE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SNEAK */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* DESIGNBUILDING */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0 } } /* DRESS_WOUNDS */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0 } } /* DIAGNOSE */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SURGERY */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SET_BONE */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SUTURE */, - { { 0, 1, 0, 1, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* CRUTCH_WALK */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* WOOD_BURNING */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* LYE_MAKING */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SOAP_MAKING */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* POTASH_MAKING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* DYER */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* OPERATE_PUMP */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SWIMMING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* PERSUASION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* NEGOTIATION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1 } } /* JUDGING_INTENT */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0 } } /* APPRAISAL */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 } } /* ORGANIZATION */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 } } /* RECORD_KEEPING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 } } /* LYING */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 } } /* INTIMIDATION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* CONVERSATION */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0 } } /* COMEDY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* FLATTERY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0 } } /* CONSOLE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* PACIFY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* TRACKING */, - { { 0, 1, 0, 0, 0, 0 }, { 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 } } /* KNOWLEDGE_ACQUISITION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 } } /* CONCENTRATION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* DISCIPLINE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SITUATIONAL_AWARENESS */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* WRITING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PROSE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* POETRY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* READING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SPEAKING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* COORDINATION */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BALANCE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* LEADERSHIP */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1 } } /* TEACHING */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* MELEE_COMBAT */, - { { 1, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* RANGED_COMBAT */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WRESTLING */, - { { 1, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* BITE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* GRASP_STRIKE */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* STANCE_STRIKE */, - { { 0, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* DODGING */, - { { 1, 1, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* MISC_WEAPON */, - { { 0, 0, 0, 0, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* KNAPPING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MILITARY_TACTICS */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SHEARING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* SPINNING */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* POTTERY */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* GLAZING */, - { { 1, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PRESSING */, - { { 1, 1, 0, 1, 0, 0 }, { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BEEKEEPING */, - { { 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0 } } /* WAX_WORKING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CLIMBING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* GELD */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* DANCE */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MAKE_MUSIC */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* SING_MUSIC */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PLAY_KEYBOARD_INSTRUMENT */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PLAY_STRINGED_INSTRUMENT */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PLAY_WIND_INSTRUMENT */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PLAY_PERCUSSION_INSTRUMENT */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CRITICAL_THINKING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* LOGIC */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* MATHEMATICS */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* ASTRONOMY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* CHEMISTRY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* GEOGRAPHY */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* OPTICS_ENGINEER */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* FLUID_ENGINEER */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* PAPERMAKING */, - { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } } /* BOOKBINDING */ -}; - -static void enable_plugin(color_ostream &out) -{ - if (!config.isValid()) - { - config = World::AddPersistentData("labormanager/2.0/config"); - config.ival(0) = 0; - } - - setOptionEnabled(CF_ENABLED, true); - enable_labormanager = true; - out << "Enabling the plugin." << endl; - - cleanup_state(); - init_state(); -} - -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) -{ - // initialize labor infos table from default table - if (ARRAY_COUNT(default_labor_infos) != ENUM_LAST_ITEM(unit_labor) + 1) - return CR_FAILURE; - - // Fill the command list with your commands. - commands.push_back(PluginCommand( - "labormanager", - "Automatically manage dwarf labors.", - labormanager)); - - generate_labor_to_skill_map(); - - labor_mapper = new JobLaborMapper(); - - init_state(); - - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown(color_ostream &out) -{ - cleanup_state(); - - delete labor_mapper; - - return CR_OK; -} - -class AutoLaborManager { - color_ostream& out; - -public: - AutoLaborManager(color_ostream& o) : out(o) - { - dwarf_info.clear(); - } - - ~AutoLaborManager() - { - for (auto d = dwarf_info.begin(); d != dwarf_info.end(); d++) - { - delete (*d); - } - } - - dwarf_info_t* add_dwarf(df::unit* u) - { - dwarf_info_t* dwarf = new dwarf_info_t(u); - dwarf_info.push_back(dwarf); - return dwarf; - } - - -private: - bool has_butchers; - bool has_fishery; - bool trader_requested; - - int dig_count; - int tree_count; - int plant_count; - int detail_count; - - bool labors_changed; - - int tool_count[TOOLS_MAX]; - int tool_in_use[TOOLS_MAX]; - - int cnt_recover_wounded; - int cnt_diagnosis; - int cnt_immobilize; - int cnt_dressing; - int cnt_cleaning; - int cnt_surgery; - int cnt_suture; - int cnt_setting; - int cnt_traction; - int cnt_crutch; - - int need_food_water; - - int priority_food; - - std::map labor_needed; - std::map labor_in_use; - std::map labor_outside; - std::vector dwarf_info; - std::list available_dwarfs; - std::list busy_dwarfs; - -private: - void set_labor(dwarf_info_t* dwarf, df::unit_labor labor, bool value) - { - if (labor >= 0 && labor <= ENUM_LAST_ITEM(unit_labor) && !labor_infos[labor].is_unmanaged()) - { - if (!Units::isValidLabor(dwarf->dwarf, labor)) - { - debug("WARN(labormanager): Attempted to %s dwarf %s with ineligible labor %s\n", - value ? "set" : "unset", - dwarf->dwarf->name.first_name.c_str(), - ENUM_KEY_STR(unit_labor, labor).c_str()); - return; - } - bool old = dwarf->dwarf->status.labors[labor]; - dwarf->dwarf->status.labors[labor] = value; - if (old != value) - { - labors_changed = true; - - tools_enum tool = default_labor_infos[labor].tool; - if (tool != TOOL_NONE) - tool_in_use[tool] += value ? 1 : -1; - } - } - } - - void process_job(df::job* j) - { - if (j->flags.bits.suspend || j->flags.bits.item_lost) - return; - - int worker = -1; - int bld = -1; - - for (auto ref : j->general_refs) - { - if (ref->getType() == df::general_ref_type::UNIT_WORKER) - worker = ((df::general_ref_unit_workerst *)ref)->unit_id; - if (ref->getType() == df::general_ref_type::BUILDING_HOLDER) - bld = ((df::general_ref_building_holderst *)ref)->building_id; - } - - if (bld != -1) - { - df::building* b = binsearch_in_vector(world->buildings.all, bld); - - // check if this job is the first nonsuspended job on this building; if not, ignore it - // (except for farms and trade depots) - - if (b->getType() != df::building_type::FarmPlot && - b->getType() != df::building_type::TradeDepot) - { - int fjid = -1; - for (size_t jn = 0; jn < b->jobs.size(); jn++) - { - if (b->jobs[jn]->flags.bits.suspend) - continue; - fjid = b->jobs[jn]->id; - break; - } - if (fjid != j->id) - return; - } - } - - df::unit_labor labor = labor_mapper->find_job_labor(j); - - if (labor != df::unit_labor::NONE && !labor_infos[labor].is_unmanaged()) - { - labor_needed[labor]++; - if (worker == -1) - { - if (j->pos.isValid()) - { - df::tile_designation* d = Maps::getTileDesignation(j->pos); - if (d->bits.outside) - labor_outside[labor] = true; - } - } - else { - labor_infos[labor].mark_assigned(); - labor_in_use[labor]++; - } - - } - } - - void scan_buildings() - { - has_butchers = false; - has_fishery = false; - trader_requested = false; - labors_changed = false; - - for (auto b = world->buildings.all.begin(); b != world->buildings.all.end(); b++) - { - df::building *build = *b; - auto type = build->getType(); - if (building_type::Workshop == type) - { - df::workshop_type subType = (df::workshop_type)build->getSubtype(); - if (workshop_type::Butchers == subType) - has_butchers = true; - if (workshop_type::Fishery == subType) - has_fishery = true; - } - else if (building_type::TradeDepot == type) - { - df::building_tradedepotst* depot = (df::building_tradedepotst*) build; - trader_requested = depot->trade_flags.bits.trader_requested; - - if (print_debug) - { - if (trader_requested) - out.print("Trade depot found and trader requested, trader will be excluded from all labors.\n"); - else - out.print("Trade depot found but trader is not requested.\n"); - } - - } - } - } - - void count_map_designations() - { - dig_count = 0; - tree_count = 0; - plant_count = 0; - detail_count = 0; - - for (size_t i = 0; i < world->map.map_blocks.size(); ++i) - { - df::map_block* bl = world->map.map_blocks[i]; - - if (!bl->flags.bits.designated) - continue; - - for (int x = 0; x < 16; x++) - for (int y = 0; y < 16; y++) - { - if (bl->designation[x][y].bits.hidden) - { - df::coord p = bl->map_pos; - if (! Maps::isTileVisible(p.x, p.y, p.z-1)) - continue; - } - - df::tile_dig_designation dig = bl->designation[x][y].bits.dig; - if (dig != df::enums::tile_dig_designation::No) - { - df::tiletype tt = bl->tiletype[x][y]; - df::tiletype_material ttm = ENUM_ATTR(tiletype, material, tt); - df::tiletype_shape tts = ENUM_ATTR(tiletype, shape, tt); - if (ttm == df::enums::tiletype_material::TREE) - tree_count++; - else if (tts == df::enums::tiletype_shape::SHRUB) - plant_count++; - else - dig_count++; - } - if (bl->designation[x][y].bits.smooth != 0) - detail_count++; - } - } - - if (print_debug) - out.print("Dig count = %d, Cut tree count = %d, gather plant count = %d, detail count = %d\n", dig_count, tree_count, plant_count, detail_count); - - } - - void count_tools() - { - for (int e = TOOL_NONE; e < TOOLS_MAX; e++) - { - tool_count[e] = 0; - tool_in_use[e] = 0; - } - - priority_food = 0; - - df::item_flags bad_flags; - bad_flags.whole = 0; - -#define F(x) bad_flags.bits.x = true; - F(dump); F(forbid); F(garbage_collect); - F(hostile); F(on_fire); F(rotten); F(trader); - F(in_building); F(construction); -#undef F - - for (auto item : world->items.other.IN_PLAY) { - if (item->flags.bits.dump && !labor_infos[df::unit_labor::HAUL_REFUSE].is_unmanaged()) - labor_needed[df::unit_labor::HAUL_REFUSE]++; - - if (item->flags.whole & bad_flags.whole) - continue; - - df::item_type t = item->getType(); - - if (item->materialRots() && t != df::item_type::CORPSEPIECE && t != df::item_type::CORPSE && item->getRotTimer() > 1) - priority_food++; - - if (!item->isWeapon()) - continue; - - df::itemdef_weaponst* weapondef = ((df::item_weaponst*)item)->subtype; - df::job_skill weaponsk = (df::job_skill) weapondef->skill_melee; - df::job_skill weaponsk2 = (df::job_skill) weapondef->skill_ranged; - if (weaponsk == df::job_skill::AXE) - tool_count[TOOL_AXE]++; - else if (weaponsk == df::job_skill::MINING) - tool_count[TOOL_PICK]++; - else if (weaponsk2 == df::job_skill::CROSSBOW) - tool_count[TOOL_CROSSBOW]++; - } - - } - - void collect_job_list() - { - for (df::job_list_link* jll = world->jobs.list.next; jll; jll = jll->next) - { - df::job* j = jll->item; - if (!j) - continue; - process_job(j); - } - - for (auto jp = world->jobs.postings.begin(); jp != world->jobs.postings.end(); jp++) - { - if ((*jp)->flags.bits.dead) - continue; - - process_job((*jp)->job); - } - - } - - void collect_dwarf_list() - { - state_count.clear(); - state_count.resize(NUM_STATE); - - for (auto u = world->units.active.begin(); u != world->units.active.end(); ++u) - { - df::unit* cre = *u; - - // following tests shamelessly stolen from Dwarf Manipulator plugin - - bool isAssignable = - (Units::isOwnCiv(cre)) && - (Units::isOwnGroup(cre)) && - (Units::isActive(cre)) && - (!cre->flags2.bits.visitor) && - (!cre->flags3.bits.ghostly) && - (ENUM_ATTR(profession, can_assign_labor, cre->profession)); - - if (isAssignable) - { - dwarf_info_t* dwarf = add_dwarf(cre); - - df::historical_figure* hf = df::historical_figure::find(dwarf->dwarf->hist_figure_id); - for (size_t i = 0; i < hf->entity_links.size(); i++) - { - df::histfig_entity_link* hfelink = hf->entity_links.at(i); - if (hfelink->getType() == df::histfig_entity_link_type::POSITION) - { - df::histfig_entity_link_positionst *epos = - (df::histfig_entity_link_positionst*) hfelink; - df::historical_entity* entity = df::historical_entity::find(epos->entity_id); - if (!entity) - continue; - df::entity_position_assignment* assignment = binsearch_in_vector(entity->positions.assignments, epos->assignment_id); - if (!assignment) - continue; - df::entity_position* position = binsearch_in_vector(entity->positions.own, assignment->position_id); - if (!position) - continue; - - if (position->responsibilities[df::entity_position_responsibility::TRADE]) - if (trader_requested) - dwarf->clear_all = true; - } - - } - - // identify dwarfs who are needed for meetings and mark them for exclusion - - for (size_t i = 0; i < plotinfo->activities.size(); ++i) - { - df::activity_info *act = plotinfo->activities[i]; - if (!act) continue; - - bool p1 = act->unit_actor == dwarf->dwarf; - bool p2 = act->unit_noble == dwarf->dwarf; - - if (p1 || p2) - { - df::unit* other = p1 ? act->unit_noble : act->unit_actor; - if (other && !(!Units::isActive(other) || - (other->job.current_job && - (other->job.current_job->job_type == df::job_type::Sleep || - other->job.current_job->job_type == df::job_type::Rest)) || - ENUM_ATTR(profession, military, other->profession))) { - dwarf->clear_all = true; - if (print_debug) - out.print("Dwarf \"%s\" has a meeting, will be cleared of all labors\n", dwarf->dwarf->name.first_name.c_str()); - break; - } - else - { - if (print_debug) - out.print("Dwarf \"%s\" has a meeting, but with someone who can't make the meeting.\n", dwarf->dwarf->name.first_name.c_str()); - } - } - } - - // check to see if dwarf has minor children - - for (auto u2 = world->units.active.begin(); u2 != world->units.active.end(); ++u2) - { - if ((*u2)->relationship_ids[df::unit_relationship_type::Mother] == dwarf->dwarf->id && - Units::isActive(*u2) && - ((*u2)->profession == df::profession::CHILD || (*u2)->profession == df::profession::BABY)) - { - dwarf->has_children = true; - if (print_debug) - out.print("Dwarf %s has minor children\n", dwarf->dwarf->name.first_name.c_str()); - break; - } - } - - // check if dwarf has an axe, pick, or crossbow - - for (size_t j = 0; j < dwarf->dwarf->inventory.size(); j++) - { - df::unit_inventory_item* plotinfo = dwarf->dwarf->inventory[j]; - if (plotinfo->mode == df::unit_inventory_item::Weapon && plotinfo->item->isWeapon()) - { - dwarf->armed = true; - df::itemdef_weaponst* weapondef = ((df::item_weaponst*)(plotinfo->item))->subtype; - df::job_skill weaponsk = (df::job_skill) weapondef->skill_melee; - df::job_skill rangesk = (df::job_skill) weapondef->skill_ranged; - if (weaponsk == df::job_skill::AXE) - { - dwarf->has_tool[TOOL_AXE] = true; - } - else if (weaponsk == df::job_skill::MINING) - { - dwarf->has_tool[TOOL_PICK] = true; - } - else if (rangesk == df::job_skill::CROSSBOW) - { - dwarf->has_tool[TOOL_CROSSBOW] = true; - } - } - } - - // Find the activity state for each dwarf - - bool is_migrant = false; - dwarf_state state = OTHER; - - for (auto p = dwarf->dwarf->status.misc_traits.begin(); p < dwarf->dwarf->status.misc_traits.end(); p++) - { - if ((*p)->id == misc_trait_type::Migrant) - is_migrant = true; - } - - if (dwarf->dwarf->social_activities.size() > 0) - { - if (print_debug) - out.print("Dwarf %s is engaged in a social activity. Info only.\n", dwarf->dwarf->name.first_name.c_str()); - } - - if (dwarf->dwarf->profession == profession::BABY || - dwarf->dwarf->profession == profession::CHILD || - dwarf->dwarf->profession == profession::DRUNK) - { - state = CHILD; - } - - else if (ENUM_ATTR(profession, military, dwarf->dwarf->profession)) - state = MILITARY; - - else if (dwarf->dwarf->burrows.size() > 0) - state = OTHER; // dwarfs assigned to burrows are treated as if permanently busy - - else if (dwarf->dwarf->job.current_job == NULL) - { - if (is_migrant || dwarf->dwarf->flags1.bits.chained || dwarf->dwarf->flags1.bits.caged) - { - state = OTHER; - dwarf->clear_all = true; - } - else if (dwarf->dwarf->status2.limbs_grasp_count == 0) - { - state = OTHER; // dwarfs unable to grasp are incapable of nearly all labors - dwarf->clear_all = true; - if (print_debug) - out.print("Dwarf %s is disabled, will not be assigned labors\n", dwarf->dwarf->name.first_name.c_str()); - } - else - { - state = IDLE; - } - } - else - { - df::job_type job = dwarf->dwarf->job.current_job->job_type; - if (job >= 0 && size_t(job) < ARRAY_COUNT(dwarf_states)) - state = dwarf_states[job]; - else - { - out.print("Dwarf \"%s\" has unknown job %i\n", dwarf->dwarf->name.first_name.c_str(), job); - debug_pause(); - state = OTHER; - } - if (state == BUSY) - { - df::unit_labor labor = labor_mapper->find_job_labor(dwarf->dwarf->job.current_job); - - dwarf->using_labor = labor; - - if (labor != df::unit_labor::NONE) - { - labor_infos[labor].busy_dwarfs++; - if (default_labor_infos[labor].tool != TOOL_NONE) - { - tool_in_use[default_labor_infos[labor].tool]++; - } - } - } - } - - dwarf->state = state; - - dwarf->unmanaged_labors_assigned = 0; - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE) - continue; - if (dwarf->dwarf->status.labors[l]) - if (state == IDLE) - labor_infos[l].idle_dwarfs++; - if (labor_infos[l].is_unmanaged()) - dwarf->unmanaged_labors_assigned++; - } - - - if (print_debug) - out.print("Dwarf \"%s\": state %s %d\n", dwarf->dwarf->name.first_name.c_str(), state_names[dwarf->state], dwarf->clear_all); - - state_count[dwarf->state]++; - - // determine if dwarf has medical needs - if (dwarf->dwarf->health && !( - // on-duty military will not necessarily break to get minor injuries attended - ENUM_ATTR(profession, military, dwarf->dwarf->profession) || - // babies cannot currently receive health care even if they need it - dwarf->dwarf->profession == profession::BABY) - ) - { - if (dwarf->dwarf->health->flags.bits.should_not_move) - cnt_recover_wounded++; - if (dwarf->dwarf->health->flags.bits.rq_diagnosis) - cnt_diagnosis++; - if (dwarf->dwarf->health->flags.bits.rq_immobilize) - cnt_immobilize++; - if (dwarf->dwarf->health->flags.bits.rq_dressing) - cnt_dressing++; - if (dwarf->dwarf->health->flags.bits.rq_cleaning) - cnt_cleaning++; - if (dwarf->dwarf->health->flags.bits.rq_surgery) - cnt_surgery++; - if (dwarf->dwarf->health->flags.bits.rq_suture) - cnt_suture++; - if (dwarf->dwarf->health->flags.bits.rq_setting) - cnt_setting++; - if (dwarf->dwarf->health->flags.bits.rq_traction) - cnt_traction++; - if (dwarf->dwarf->health->flags.bits.rq_crutch) - cnt_crutch++; - } - - if (dwarf->dwarf->counters2.hunger_timer > 60000 || dwarf->dwarf->counters2.thirst_timer > 40000) - need_food_water++; - - // find dwarf's highest effective skill - - int high_skill = 0; - - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == df::unit_labor::NONE || labor_infos[labor].is_unmanaged()) - continue; - - df::job_skill skill = labor_to_skill[labor]; - if (skill != df::job_skill::NONE) - { - int skill_level = Units::getNominalSkill(dwarf->dwarf, skill, false); - high_skill = std::max(high_skill, skill_level); - } - } - - dwarf->high_skill = high_skill; - - - // clear labors of dwarfs with clear_all set - - if (dwarf->clear_all) - { - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE || labor_infos[labor].is_unmanaged()) - continue; - if (Units::isValidLabor(dwarf->dwarf, labor)) - set_labor(dwarf, labor, false); - } - } - else { - if (state == IDLE) - available_dwarfs.push_back(dwarf); - - if (state == BUSY) - busy_dwarfs.push_back(dwarf); - } - } - - } - } - - void release_dwarf_list() - { - while (!dwarf_info.empty()) { - auto d = dwarf_info.begin(); - delete *d; - dwarf_info.erase(d); - } - available_dwarfs.clear(); - busy_dwarfs.clear(); - } - - int score_labor(dwarf_info_t* d, df::unit_labor labor) - { - int skill_level = 0; - int xp = 0; - int attr_weight = 0; - - if (labor != df::unit_labor::NONE) - { - df::job_skill skill = labor_to_skill[labor]; - if (skill != df::job_skill::NONE) - { - skill_level = Units::getEffectiveSkill(d->dwarf, skill); - xp = Units::getExperience(d->dwarf, skill, false); - - for (int pa = 0; pa < 6; pa++) - attr_weight += (skill_attr_weights[skill].phys_attr_weights[pa]) * (d->dwarf->body.physical_attrs[pa].value - 1000); - - for (int ma = 0; ma < 13; ma++) - attr_weight += (skill_attr_weights[skill].mental_attr_weights[ma]) * (d->dwarf->status.current_soul->mental_attrs[ma].value - 1000); - } - - } - - int score = skill_level * 1000 - (d->high_skill - skill_level) * 2000 + (xp / (skill_level + 5) * 10) + attr_weight; - - if (labor != df::unit_labor::NONE) - { - if (d->dwarf->status.labors[labor]) - { - if (labor == df::unit_labor::OPERATE_PUMP) - score += 50000; - else - score += 25000; - } - if (default_labor_infos[labor].tool != TOOL_NONE && - d->has_tool[default_labor_infos[labor].tool]) - score += 10000000; - if (d->has_children && labor_outside[labor]) - score -= 15000; - if (d->armed && labor_outside[labor]) - score += 5000; - } - - // Favor/disfavor RECOVER_WOUNDED based on ALTRUISM personality facet - - if (labor == df::unit_labor::RECOVER_WOUNDED) - { - int altruism = d->dwarf->status.current_soul->personality.traits[df::personality_facet_type::ALTRUISM]; - if (altruism >= 61) - score += 5000; - else if (altruism <= 24) - score -= 50000; - } - - // Favor/disfavor BUTCHER (covers slaughtering), HAUL_ANIMALS (covers caging), and CUTWOOD based on NATURE value - - if (labor == df::unit_labor::BUTCHER || labor == df::unit_labor::HAUL_ANIMALS || labor == df::unit_labor::CUTWOOD) - { - int nature = 0; - for (auto i = d->dwarf->status.current_soul->personality.values.begin(); - i != d->dwarf->status.current_soul->personality.values.end(); - i++) - { - if ((*i)->type == df::value_type::NATURE) - nature = (*i)->strength; - } - - if (nature <= -11) - score += 5000; - else if (nature >= 26) - score -= 50000; - } - - // This should reweight assigning CUTWOOD jobs based on a citizen's ethic toward killing plants - - if (labor == df::unit_labor::CUTWOOD) - { - if (auto culture = df::cultural_identity::find(d->dwarf->cultural_identity)) - { - auto ethics = culture->ethic[df::ethic_type::KILL_PLANT]; - if (ethics != df::ethic_response::NOT_APPLICABLE && ethics != df::ethic_response::REQUIRED) - score += 10000 * (df::ethic_response::ACCEPTABLE - ethics); - } - } - - score -= Units::computeMovementSpeed(d->dwarf); - - // significantly disfavor dwarves who have unmanaged labors assigned - score -= 1000 * d->unmanaged_labors_assigned; - - return score; - } - -public: - void process() - { - if (*df::global::process_dig || *df::global::process_jobs) - return; - - release_dwarf_list(); - - dig_count = tree_count = plant_count = detail_count = 0; - cnt_recover_wounded = cnt_diagnosis = cnt_immobilize = cnt_dressing = cnt_cleaning = cnt_surgery = cnt_suture = - cnt_setting = cnt_traction = cnt_crutch = 0; - need_food_water = 0; - - labor_needed.clear(); - - for (int e = 0; e < TOOLS_MAX; e++) - tool_count[e] = 0; - - trader_requested = false; - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE) - continue; - - labor_infos[l].active_dwarfs = labor_infos[l].busy_dwarfs = labor_infos[l].idle_dwarfs = 0; - } - - // scan for specific buildings of interest - - scan_buildings(); - - // count number of squares designated for dig, wood cutting, detailing, and plant harvesting - - count_map_designations(); - - // collect current job list - - collect_job_list(); - - // count number of picks and axes available for use - - count_tools(); - - // collect list of dwarfs - - collect_dwarf_list(); - - // add job entries for designation-related jobs - - labor_needed[df::unit_labor::MINE] += dig_count; - labor_needed[df::unit_labor::CUTWOOD] += tree_count; - labor_needed[df::unit_labor::DETAIL] += detail_count; - labor_needed[df::unit_labor::HERBALIST] += plant_count; - - // add job entries for health care - - labor_needed[df::unit_labor::RECOVER_WOUNDED] += cnt_recover_wounded; - labor_needed[df::unit_labor::DIAGNOSE] += cnt_diagnosis; - labor_needed[df::unit_labor::BONE_SETTING] += cnt_immobilize; - labor_needed[df::unit_labor::DRESSING_WOUNDS] += cnt_dressing; - labor_needed[df::unit_labor::DRESSING_WOUNDS] += cnt_cleaning; - labor_needed[df::unit_labor::SURGERY] += cnt_surgery; - labor_needed[df::unit_labor::SUTURING] += cnt_suture; - labor_needed[df::unit_labor::BONE_SETTING] += cnt_setting; - labor_needed[df::unit_labor::BONE_SETTING] += cnt_traction; - labor_needed[df::unit_labor::BONE_SETTING] += cnt_crutch; - - labor_needed[df::unit_labor::FEED_WATER_CIVILIANS] += need_food_water; - - // add entries for hauling jobs - - labor_needed[df::unit_labor::HAUL_STONE] += world->stockpile.num_jobs[1]; - labor_needed[df::unit_labor::HAUL_WOOD] += world->stockpile.num_jobs[2]; - labor_needed[df::unit_labor::HAUL_ITEM] += world->stockpile.num_jobs[3]; - labor_needed[df::unit_labor::HAUL_ITEM] += world->stockpile.num_jobs[4]; - labor_needed[df::unit_labor::HAUL_BODY] += world->stockpile.num_jobs[5]; - labor_needed[df::unit_labor::HAUL_FOOD] += world->stockpile.num_jobs[6]; - labor_needed[df::unit_labor::HAUL_REFUSE] += world->stockpile.num_jobs[7]; - labor_needed[df::unit_labor::HAUL_FURNITURE] += world->stockpile.num_jobs[8]; - labor_needed[df::unit_labor::HAUL_ANIMALS] += world->stockpile.num_jobs[9]; - - labor_needed[df::unit_labor::HAUL_STONE] += (world->stockpile.num_jobs[1] >= world->stockpile.num_haulers[1]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_WOOD] += (world->stockpile.num_jobs[2] >= world->stockpile.num_haulers[2]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_ITEM] += (world->stockpile.num_jobs[3] >= world->stockpile.num_haulers[3]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_BODY] += (world->stockpile.num_jobs[5] >= world->stockpile.num_haulers[5]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_FOOD] += (world->stockpile.num_jobs[6] >= world->stockpile.num_haulers[6]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_REFUSE] += (world->stockpile.num_jobs[7] >= world->stockpile.num_haulers[7]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_FURNITURE] += (world->stockpile.num_jobs[8] >= world->stockpile.num_haulers[8]) ? 1 : 0; - labor_needed[df::unit_labor::HAUL_ANIMALS] += (world->stockpile.num_jobs[9] >= world->stockpile.num_haulers[9]) ? 1 : 0; - - int binjobs = world->stockpile.num_jobs[4] + ((world->stockpile.num_jobs[4] >= world->stockpile.num_haulers[4]) ? 1 : 0); - - labor_needed[df::unit_labor::HAUL_ITEM] += binjobs; - labor_needed[df::unit_labor::HAUL_FOOD] += priority_food; - - // add entries for vehicle hauling - - for (auto v = world->vehicles.all.begin(); v != world->vehicles.all.end(); v++) - if ((*v)->route_id != -1) - labor_needed[df::unit_labor::HANDLE_VEHICLES]++; - - // add fishing & hunting - - labor_needed[df::unit_labor::FISH] = - (isOptionEnabled(CF_ALLOW_FISHING) && has_fishery) ? 1 : 0; - - labor_needed[df::unit_labor::HUNT] = - (isOptionEnabled(CF_ALLOW_HUNTING) && has_butchers) ? 1 : 0; - - /* add animal trainers */ - for (auto a = df::global::plotinfo->training.training_assignments.begin(); - a != df::global::plotinfo->training.training_assignments.end(); - a++) - { - labor_needed[df::unit_labor::ANIMALTRAIN]++; - // note: this doesn't test to see if the trainer is actually needed, and thus will overallocate trainers. bleah. - } - - /* set requirements to zero for labors with currently idle dwarfs, and remove from requirement dwarfs actually working */ - - FOR_ENUM_ITEMS(unit_labor, l) { - if (l == df::unit_labor::NONE) - continue; - - if (!labor_infos[l].is_unmanaged()) - { - int before = labor_needed[l]; - - labor_needed[l] = max(0, labor_needed[l] - labor_in_use[l]); - - if (default_labor_infos[l].tool != TOOL_NONE) - labor_needed[l] = std::min(labor_needed[l], tool_count[default_labor_infos[l].tool] - tool_in_use[default_labor_infos[l].tool]); - - if (print_debug && before != labor_needed[l]) - out.print("labor %s reduced from %d to %d\n", ENUM_KEY_STR(unit_labor, l).c_str(), before, labor_needed[l]); - } - else - { - labor_needed[l] = 0; - } - } - - /* assign food haulers for rotting food items */ - if (!labor_infos[df::unit_labor::HAUL_FOOD].is_unmanaged()) - { - if (priority_food > 0 && labor_infos[df::unit_labor::HAUL_FOOD].idle_dwarfs > 0) - priority_food = 1; - - if (print_debug) - out.print("priority food count = %d\n", priority_food); - - while (!available_dwarfs.empty() && priority_food > 0) - { - std::list::iterator bestdwarf = available_dwarfs.begin(); - - int best_score = INT_MIN; - - for (std::list::iterator k = available_dwarfs.begin(); k != available_dwarfs.end(); k++) - { - dwarf_info_t* d = (*k); - - if (Units::isValidLabor(d->dwarf, df::unit_labor::HAUL_FOOD)) - { - int score = score_labor(d, df::unit_labor::HAUL_FOOD); - - if (score > best_score) - { - bestdwarf = k; - best_score = score; - } - } - } - - if (best_score > INT_MIN) - { - if (print_debug) - out.print("LABORMANAGER: assign \"%s\" labor %s score=%d (priority food)\n", (*bestdwarf)->dwarf->name.first_name.c_str(), ENUM_KEY_STR(unit_labor, df::unit_labor::HAUL_FOOD).c_str(), best_score); - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE) - continue; - if (Units::isValidLabor((*bestdwarf)->dwarf, l)) - set_labor(*bestdwarf, l, l == df::unit_labor::HAUL_FOOD); - } - - available_dwarfs.erase(bestdwarf); - priority_food--; - } - else - break; - - } - } - else - { - priority_food = 0; - } - - if (print_debug) - { - for (auto i = labor_needed.begin(); i != labor_needed.end(); i++) - { - out.print("labor_needed [%s] = %d, busy = %d, outside = %d, idle = %d\n", ENUM_KEY_STR(unit_labor, i->first).c_str(), i->second, - labor_infos[i->first].busy_dwarfs, labor_outside[i->first], labor_infos[i->first].idle_dwarfs); - } - } - - std::map base_priority; - priority_queue> pq; - priority_queue> pq2; - - for (auto i = labor_needed.begin(); i != labor_needed.end(); i++) - { - df::unit_labor l = i->first; - if (l == df::unit_labor::NONE || labor_infos[l].is_unmanaged()) - continue; - - const int user_specified_max_dwarfs = labor_infos[l].maximum_dwarfs(); - - if (user_specified_max_dwarfs != MAX_DWARFS_NONE && i->second > user_specified_max_dwarfs) - { - i->second = user_specified_max_dwarfs; - } - - int priority = labor_infos[l].priority(); - - priority += labor_infos[l].time_since_last_assigned() / 12; - priority -= labor_infos[l].busy_dwarfs; - - base_priority[l] = priority; - - if (i->second > 0) - { - pq.push(make_pair(priority, l)); - } - } - - if (print_debug) - out.print("available count = %zu, distinct labors needed = %zu\n", available_dwarfs.size(), pq.size()); - - std::map to_assign; - - to_assign.clear(); - - size_t av = available_dwarfs.size(); - - while (!pq.empty() && av > 0) - { - df::unit_labor labor = pq.top().second; - int priority = pq.top().first; - to_assign[labor]++; - pq.pop(); - av--; - - if (print_debug) - out.print("Will assign: %s priority %d (%d)\n", ENUM_KEY_STR(unit_labor, labor).c_str(), priority, to_assign[labor]); - - if (--labor_needed[labor] > 0) - { - priority -= 10; - pq2.push(make_pair(priority, labor)); - } - - if (pq.empty()) - while (!pq2.empty()) - { - pq.push(pq2.top()); - pq2.pop(); - } - } - - while (!pq2.empty()) - { - pq.push(pq2.top()); - pq2.pop(); - } - - int canary = (1 << df::unit_labor::HAUL_STONE) | - (1 << df::unit_labor::HAUL_WOOD) | - (1 << df::unit_labor::HAUL_BODY) | - (1 << df::unit_labor::HAUL_FOOD) | - (1 << df::unit_labor::HAUL_REFUSE) | - (1 << df::unit_labor::HAUL_ITEM) | - (1 << df::unit_labor::HAUL_FURNITURE) | - (1 << df::unit_labor::HAUL_ANIMALS); - - while (!available_dwarfs.empty()) - { - std::list::iterator bestdwarf = available_dwarfs.begin(); - - int best_score = INT_MIN; - df::unit_labor best_labor = df::unit_labor::NONE; - - for (auto j = to_assign.begin(); j != to_assign.end(); j++) - { - if (j->second <= 0) - continue; - - df::unit_labor labor = j->first; - - for (std::list::iterator k = available_dwarfs.begin(); k != available_dwarfs.end(); k++) - { - dwarf_info_t* d = (*k); - if (Units::isValidLabor(d->dwarf, labor)) - { - int score = score_labor(d, labor); - if (score > best_score) - { - bestdwarf = k; - best_score = score; - best_labor = labor; - } - } - } - } - - if (best_labor == df::unit_labor::NONE) - break; - - if (print_debug) - out.print("assign \"%s\" labor %s score=%d\n", (*bestdwarf)->dwarf->name.first_name.c_str(), ENUM_KEY_STR(unit_labor, best_labor).c_str(), best_score); - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE) - continue; - - tools_enum t = default_labor_infos[l].tool; - - if (l == best_labor && - Units::isValidLabor((*bestdwarf)->dwarf, l) && - (t == TOOL_NONE || tool_in_use[t] < tool_count[t])) - { - set_labor(*bestdwarf, l, true); - if (t != TOOL_NONE && !((*bestdwarf)->has_tool[t])) - { - df::job_type j; - j = df::job_type::NONE; - - if ((*bestdwarf)->dwarf->job.current_job) - j = (*bestdwarf)->dwarf->job.current_job->job_type; - - if (print_debug) - out.print("LABORMANAGER: asking %s to pick up tools, current job %s\n", (*bestdwarf)->dwarf->name.first_name.c_str(), ENUM_KEY_STR(job_type, j).c_str()); - - (*bestdwarf)->dwarf->uniform.pickup_flags.bits.update = true; - labors_changed = true; - } - } - else if (l == df::unit_labor::CLEAN && best_score < 0) - { - if (Units::isValidLabor((*bestdwarf)->dwarf, l)) - set_labor(*bestdwarf, l, true); - } - else if ((*bestdwarf)->state == IDLE) - { - if (Units::isValidLabor((*bestdwarf)->dwarf, l)) - set_labor(*bestdwarf, l, false); - } - } - - if (best_labor == df::unit_labor::HAUL_FOOD && priority_food > 0) - priority_food--; - - if (best_labor >= df::unit_labor::HAUL_STONE && best_labor <= df::unit_labor::HAUL_ANIMALS) - canary &= ~(1 << best_labor); - - if (best_labor != df::unit_labor::NONE) - { - labor_infos[best_labor].active_dwarfs++; - to_assign[best_labor]--; - } - - busy_dwarfs.push_back(*bestdwarf); - available_dwarfs.erase(bestdwarf); - } - - for (auto d = busy_dwarfs.begin(); d != busy_dwarfs.end(); d++) - { - int current_score = score_labor(*d, (*d)->using_labor); - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE || labor_infos[l].is_unmanaged()) - continue; - if (l == (*d)->using_labor) - continue; - if (labor_needed[l] <= 0) - continue; - if (!Units::isValidLabor((*d)->dwarf, l)) - continue; - - int score = score_labor(*d, l); - - if (l == df::unit_labor::HAUL_FOOD && priority_food > 0) - score += 1000000; - - if (score > current_score) - { - tools_enum t = default_labor_infos[l].tool; - if (t == TOOL_NONE || (*d)->has_tool[t]) - { - set_labor(*d, l, true); - } - - if (score < 0) - set_labor(*d, df::unit_labor::CLEAN, true); - - if ((*d)->using_labor != df::unit_labor::NONE && - (score > current_score + 5000 || base_priority[(*d)->using_labor] < base_priority[l]) && - default_labor_infos[(*d)->using_labor].tool == TOOL_NONE) - set_labor(*d, (*d)->using_labor, false); - } - } - } - - - dwarf_info_t* canary_dwarf = 0; - - for (auto di = busy_dwarfs.begin(); di != busy_dwarfs.end(); di++) - if (!(*di)->clear_all) - { - canary_dwarf = *di; - break; - } - - if (canary_dwarf) - { - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l >= df::unit_labor::HAUL_STONE && l <= df::unit_labor::HAUL_ANIMALS && - canary & (1 << l) && - Units::isValidLabor(canary_dwarf->dwarf, l)) - set_labor(canary_dwarf, l, true); - } - - set_labor(canary_dwarf, df::unit_labor::CLEAN, true); - - /* Set HAUL_WATER so we can detect ponds that need to be filled ponds. */ - - if (!labor_infos[df::unit_labor::HAUL_WATER].is_unmanaged()) - { - set_labor(canary_dwarf, df::unit_labor::HAUL_WATER, true); - } - - if (print_debug) - out.print("Setting %s as the hauling canary\n", canary_dwarf->dwarf->name.first_name.c_str()); - } - else - { - if (print_debug) - out.print("No dwarf available to set as the hauling canary!\n"); - } - - /* Assign any leftover dwarfs to "standard" labors */ - - if (print_debug) - out.print("After assignment, %zu dwarfs left over\n", available_dwarfs.size()); - - for (auto d = available_dwarfs.begin(); d != available_dwarfs.end(); d++) - { - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE || labor_infos[l].is_unmanaged()) - continue; - - if (Units::isValidLabor((*d)->dwarf, l)) - set_labor(*d, l, - (l >= df::unit_labor::HAUL_STONE && l <= df::unit_labor::HAUL_ANIMALS) || - l == df::unit_labor::CLEAN || - l == df::unit_labor::HAUL_WATER || - l == df::unit_labor::PULL_LEVER || - l == df::unit_labor::HAUL_TRADE); - } - } - - /* check for dwarfs assigned no labors and assign them the bucket list if there are */ - for (auto d = dwarf_info.begin(); d != dwarf_info.end(); d++) - { - if ((*d)->state == CHILD) - continue; - - bool any = false; - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE) - continue; - if ((*d)->dwarf->status.labors[l]) - { - any = true; - break; - } - } - - if (!labor_infos[df::unit_labor::PULL_LEVER].is_unmanaged()) - { - set_labor(*d, df::unit_labor::PULL_LEVER, true); - } - - if (any) continue; - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE || labor_infos[l].is_unmanaged()) - continue; - - if (to_assign[l] > 0 || l == df::unit_labor::CLEAN) - set_labor(*d, l, true); - } - } - - - /* set reequip on any dwarfs who are carrying tools needed by others */ - - for (auto d = dwarf_info.begin(); d != dwarf_info.end(); d++) - { - if ((*d)->dwarf->job.current_job && (*d)->dwarf->job.current_job->job_type == df::job_type::PickupEquipment) - continue; - - if ((*d)->dwarf->uniform.pickup_flags.bits.update) - continue; - - FOR_ENUM_ITEMS(unit_labor, l) - { - if (l == df::unit_labor::NONE || labor_infos[l].is_unmanaged()) - continue; - - tools_enum t = default_labor_infos[l].tool; - if (t == TOOL_NONE) - continue; - - bool has_tool = (*d)->has_tool[t]; - bool needs_tool = (*d)->dwarf->status.labors[l]; - - if ((needs_tool && !has_tool) || - (has_tool && !needs_tool && tool_in_use[t] >= tool_count[t])) - { - df::job_type j = df::job_type::NONE; - - if ((*d)->dwarf->job.current_job) - j = (*d)->dwarf->job.current_job->job_type; - - if (print_debug) - out.print("LABORMANAGER: asking %s to %s tools, current job %s, %d %d \n", (*d)->dwarf->name.first_name.c_str(), (has_tool) ? "drop" : "pick up", ENUM_KEY_STR(job_type, j).c_str(), has_tool, needs_tool); - - (*d)->dwarf->uniform.pickup_flags.bits.update = true; - labors_changed = true; - - if (needs_tool) - tool_in_use[t]++; - } - } - } - - release_dwarf_list(); - - if (labors_changed) - { - *df::global::process_dig = true; - *df::global::process_jobs = true; - } - - if (print_debug) { - *df::global::pause_state = true; - } - - print_debug = 0; - - } - -}; - - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - cleanup_state(); - init_state(); - break; - case SC_MAP_UNLOADED: - cleanup_state(); - break; - default: - break; - } - - return CR_OK; -} - -DFhackCExport command_result plugin_onupdate(color_ostream &out) -{ - // static int step_count = 0; - // check run conditions - if (!initialized || !world || !world->map.block_index || !enable_labormanager) - { - // give up if we shouldn't be running' - return CR_OK; - } - - // if (++step_count < 60) - // return CR_OK; - - if (*df::global::process_jobs) - return CR_OK; - - // step_count = 0; - - debug_stream = &out; - AutoLaborManager alm(out); - alm.process(); - - return CR_OK; -} - -void print_labor(df::unit_labor labor, color_ostream &out) -{ - string labor_name = ENUM_KEY_STR(unit_labor, labor); - out << labor_name << ": "; - for (int i = 0; i < 20 - (int)labor_name.length(); i++) - out << ' '; - const auto& labor_info = labor_infos[labor]; - if (labor_info.is_unmanaged()) - { - out << "UNMANAGED"; - } - else - { - out << "priority " << labor_info.priority(); - - if (labor_info.maximum_dwarfs() == MAX_DWARFS_NONE) - out << ", no maximum"; - else - out << ", maximum " << labor_info.maximum_dwarfs(); - } - out << ", currently " << labor_info.active_dwarfs << " dwarfs (" - << labor_info.busy_dwarfs << " busy, " - << labor_info.idle_dwarfs << " idle)" - << endl; -} - -df::unit_labor lookup_labor_by_name(std::string name) -{ - // We should accept incorrect casing, there is no ambiguity. - std::transform(name.begin(), name.end(), name.begin(), ::toupper); - - FOR_ENUM_ITEMS(unit_labor, test_labor) - { - if (name == ENUM_KEY_STR(unit_labor, test_labor)) - { - return test_labor; - } - } - - return df::unit_labor::NONE; -} - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (!Core::getInstance().isWorldLoaded()) { - out.printerr("World is not loaded: please load a fort first.\n"); - return CR_FAILURE; - } - - if (enable && !enable_labormanager) - { - enable_plugin(out); - } - else if (!enable && enable_labormanager) - { - enable_labormanager = false; - setOptionEnabled(CF_ENABLED, false); - - out << "LaborManager is disabled." << endl; - } - - return CR_OK; -} - -command_result labormanager(color_ostream &out, std::vector & parameters) -{ - if (!Core::getInstance().isWorldLoaded()) { - out.printerr("World is not loaded: please load a game first.\n"); - return CR_FAILURE; - } - - if (parameters.size() == 1 && - (parameters[0] == "enable" || parameters[0] == "disable")) - { - bool enable = (parameters[0] == "enable"); - return plugin_enable(out, enable); - } - else if (parameters.size() == 3 && - (parameters[0] == "max" || parameters[0] == "priority")) - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - df::unit_labor labor = lookup_labor_by_name(parameters[1]); - - if (labor == df::unit_labor::NONE) - { - out.printerr("Could not find labor %s.\n", parameters[0].c_str()); - return CR_WRONG_USAGE; - } - - int v; - - if (parameters[2] == "none") - v = MAX_DWARFS_NONE; - else if (parameters[2] == "disable" || parameters[2] == "unmanaged") - v = MAX_DWARFS_UNMANAGED; - else - v = atoi(parameters[2].c_str()); - - if (parameters[0] == "max") - labor_infos[labor].set_maximum_dwarfs(v); - else if (parameters[0] == "priority") - labor_infos[labor].set_priority(v); - - print_labor(labor, out); - return CR_OK; - } - else if (parameters.size() == 2 && parameters[0] == "reset") - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - df::unit_labor labor = lookup_labor_by_name(parameters[1]); - - if (labor == df::unit_labor::NONE) - { - out.printerr("Could not find labor %s.\n", parameters[0].c_str()); - return CR_WRONG_USAGE; - } - reset_labor(labor); - print_labor(labor, out); - return CR_OK; - } - else if (parameters.size() == 1 && (parameters[0] == "allow-fishing" || parameters[0] == "forbid-fishing")) - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - setOptionEnabled(CF_ALLOW_FISHING, (parameters[0] == "allow-fishing")); - return CR_OK; - } - else if (parameters.size() == 1 && (parameters[0] == "allow-hunting" || parameters[0] == "forbid-hunting")) - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - setOptionEnabled(CF_ALLOW_HUNTING, (parameters[0] == "allow-hunting")); - return CR_OK; - } - else if (parameters.size() == 1 && parameters[0] == "reset-all") - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - for (size_t i = 0; i < labor_infos.size(); i++) - { - reset_labor((df::unit_labor) i); - } - out << "All labors reset." << endl; - return CR_OK; - } - else if (parameters.size() == 1 && (parameters[0] == "list" || parameters[0] == "status")) - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - bool need_comma = 0; - for (int i = 0; i < NUM_STATE; i++) - { - if (state_count[i] == 0) - continue; - if (need_comma) - out << ", "; - out << state_count[i] << ' ' << state_names[i]; - need_comma = 1; - } - out << endl; - - if (parameters[0] == "list") - { - FOR_ENUM_ITEMS(unit_labor, labor) - { - if (labor == unit_labor::NONE) - continue; - - print_labor(labor, out); - } - } - - return CR_OK; - } - else if (parameters.size() == 2 && parameters[0] == "pause-on-error") - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - pause_on_error = parameters[1] == "yes" || parameters[1] == "true"; - - return CR_OK; - } - else if (parameters.size() == 1 && parameters[0] == "debug") - { - if (!enable_labormanager) - { - out << "Error: The plugin is not enabled." << endl; - return CR_FAILURE; - } - - print_debug = 1; - - return CR_OK; - } - else - { - out.print("Automatically assigns labors to dwarves.\n" - "Activate with 'labormanager enable', deactivate with 'labormanager disable'.\n" - "Current state: %s.\n", enable_labormanager ? "enabled" : "disabled"); - - return CR_OK; - } -} diff --git a/plugins/autolabor/labormanager.h b/plugins/autolabor/labormanager.h deleted file mode 100644 index c958900b1a..0000000000 --- a/plugins/autolabor/labormanager.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -void debug(const char* fmt, ...); -void debug_pause(); diff --git a/plugins/autolabor/laborstatemap.h b/plugins/autolabor/laborstatemap.h index 3757011d63..e944c815a4 100644 --- a/plugins/autolabor/laborstatemap.h +++ b/plugins/autolabor/laborstatemap.h @@ -30,7 +30,7 @@ using namespace df::enums; const int NUM_STATE = 6; -char const* state_names[] { +static char const* const state_names[] { "IDLE", "BUSY", "EXCLUSIVE", diff --git a/plugins/autolabor/legacyengine.cpp b/plugins/autolabor/legacyengine.cpp new file mode 100644 index 0000000000..9451d2708b --- /dev/null +++ b/plugins/autolabor/legacyengine.cpp @@ -0,0 +1,949 @@ +/* + * Legacy engine for autolabor: direct labor matrix manipulation. + * + * This preserves the pre-v50 behavior of the autolabor plugin. It bypasses + * the work detail system by setting automatic_professions_disabled and + * writes unit->status.labors directly each cycle. + */ + +#include "laborcommon.h" +#include "legacyengine.h" + +#include +#include +#include +#include +#include + +#include "Debug.h" +#include "PluginManager.h" + +#include "modules/Units.h" +#include "modules/World.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +using df::global::plotinfo; +using df::global::world; +using df::global::game; + +#define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) + +namespace DFHack { + DBG_DECLARE(autolabor, legacy_cycle, DebugCategory::LINFO); +} + +using autolabor::plugin_config; +using autolabor::config_flag; +using autolabor::labor_to_skill; + +static std::vector state_count(NUM_STATE); + +enum labor_mode { + DISABLE, + HAULERS, + AUTOMATIC, +}; + +struct labor_info +{ + PersistentDataItem config; + + bool is_exclusive; + int active_dwarfs; + + labor_mode mode() { return (labor_mode) config.ival(0); } + void set_mode(labor_mode mode) { config.ival(0) = mode; } + + int minimum_dwarfs() { return config.ival(1); } + void set_minimum_dwarfs(int minimum_dwarfs) { config.ival(1) = minimum_dwarfs; } + + int maximum_dwarfs() { return config.ival(2); } + void set_maximum_dwarfs(int maximum_dwarfs) { config.ival(2) = maximum_dwarfs; } + + int talent_pool() { return config.ival(3); } + void set_talent_pool(int talent_pool) { config.ival(3) = talent_pool; } +}; + +struct labor_default +{ + labor_mode mode; + bool is_exclusive; + int minimum_dwarfs; + int maximum_dwarfs; + int active_dwarfs; +}; + +// The percentage of the dwarves assigned as haulers at any one time. +static int hauler_pct = 33; + +// The maximum percentage of dwarves who will be allowed to be idle. +// Decreasing this will encourage autolabor to keep dwarves busy, +// at the expense of making it harder for dwarves to specialize in +// specific skills. +static int idler_pct = 10; + +static std::vector labor_infos; + +static const struct labor_default default_labor_infos[] = { + /* MINE */ {AUTOMATIC, true, 2, 200, 0}, + /* HAUL_STONE */ {HAULERS, false, 1, 200, 0}, + /* HAUL_WOOD */ {HAULERS, false, 1, 200, 0}, + /* HAUL_BODY */ {HAULERS, false, 1, 200, 0}, + /* HAUL_FOOD */ {HAULERS, false, 1, 200, 0}, + /* HAUL_REFUSE */ {HAULERS, false, 1, 200, 0}, + /* HAUL_ITEM */ {HAULERS, false, 1, 200, 0}, + /* HAUL_FURNITURE */ {HAULERS, false, 1, 200, 0}, + /* HAUL_ANIMAL */ {HAULERS, false, 1, 200, 0}, + /* CLEAN */ {HAULERS, false, 1, 200, 0}, + /* CUTWOOD */ {AUTOMATIC, true, 1, 200, 0}, + /* CARPENTER */ {AUTOMATIC, false, 1, 200, 0}, + /* STONECUTTER */ {AUTOMATIC, false, 1, 200, 0}, + /* STONE_CARVER */ {AUTOMATIC, false, 1, 200, 0}, + /* MASON */ {AUTOMATIC, false, 1, 200, 0}, + /* ANIMALTRAIN */ {AUTOMATIC, false, 1, 200, 0}, + /* ANIMALCARE */ {AUTOMATIC, false, 1, 200, 0}, + /* DIAGNOSE */ {AUTOMATIC, false, 1, 200, 0}, + /* SURGERY */ {AUTOMATIC, false, 1, 200, 0}, + /* BONE_SETTING */ {AUTOMATIC, false, 1, 200, 0}, + /* SUTURING */ {AUTOMATIC, false, 1, 200, 0}, + /* DRESSING_WOUNDS */ {AUTOMATIC, false, 1, 200, 0}, + /* FEED_WATER_CIVILIANS */ {AUTOMATIC, false, 200, 200, 0}, + /* RECOVER_WOUNDED */ {HAULERS, false, 1, 200, 0}, + /* BUTCHER */ {AUTOMATIC, false, 1, 200, 0}, + /* TRAPPER */ {AUTOMATIC, false, 1, 200, 0}, + /* DISSECT_VERMIN */ {AUTOMATIC, false, 1, 200, 0}, + /* LEATHER */ {AUTOMATIC, false, 1, 200, 0}, + /* TANNER */ {AUTOMATIC, false, 1, 200, 0}, + /* BREWER */ {AUTOMATIC, false, 1, 200, 0}, + /* ALCHEMIST */ {AUTOMATIC, false, 1, 200, 0}, + /* SOAP_MAKER */ {AUTOMATIC, false, 1, 200, 0}, + /* WEAVER */ {AUTOMATIC, false, 1, 200, 0}, + /* CLOTHESMAKER */ {AUTOMATIC, false, 1, 200, 0}, + /* MILLER */ {AUTOMATIC, false, 1, 200, 0}, + /* PROCESS_PLANT */ {AUTOMATIC, false, 1, 200, 0}, + /* MAKE_CHEESE */ {AUTOMATIC, false, 1, 200, 0}, + /* MILK */ {AUTOMATIC, false, 1, 200, 0}, + /* COOK */ {AUTOMATIC, false, 1, 200, 0}, + /* PLANT */ {AUTOMATIC, false, 1, 200, 0}, + /* HERBALIST */ {AUTOMATIC, false, 1, 200, 0}, + /* FISH */ {AUTOMATIC, false, 1, 1, 0}, + /* CLEAN_FISH */ {AUTOMATIC, false, 1, 200, 0}, + /* DISSECT_FISH */ {AUTOMATIC, false, 1, 200, 0}, + /* HUNT */ {AUTOMATIC, true, 1, 1, 0}, + /* SMELT */ {AUTOMATIC, false, 1, 200, 0}, + /* FORGE_WEAPON */ {AUTOMATIC, false, 1, 200, 0}, + /* FORGE_ARMOR */ {AUTOMATIC, false, 1, 200, 0}, + /* FORGE_FURNITURE */ {AUTOMATIC, false, 1, 200, 0}, + /* METAL_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* CUT_GEM */ {AUTOMATIC, false, 1, 200, 0}, + /* ENCRUST_GEM */ {AUTOMATIC, false, 1, 200, 0}, + /* WOOD_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* STONE_CRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* BONE_CARVE */ {AUTOMATIC, false, 1, 200, 0}, + /* GLASSMAKER */ {AUTOMATIC, false, 1, 200, 0}, + /* EXTRACT_STRAND */ {AUTOMATIC, false, 1, 200, 0}, + /* SIEGECRAFT */ {AUTOMATIC, false, 1, 200, 0}, + /* SIEGEOPERATE */ {AUTOMATIC, false, 1, 200, 0}, + /* BOWYER */ {AUTOMATIC, false, 1, 200, 0}, + /* MECHANIC */ {AUTOMATIC, false, 1, 200, 0}, + /* POTASH_MAKING */ {AUTOMATIC, false, 1, 200, 0}, + /* LYE_MAKING */ {AUTOMATIC, false, 1, 200, 0}, + /* DYER */ {AUTOMATIC, false, 1, 200, 0}, + /* BURN_WOOD */ {AUTOMATIC, false, 1, 200, 0}, + /* OPERATE_PUMP */ {AUTOMATIC, false, 1, 200, 0}, + /* SHEARER */ {AUTOMATIC, false, 1, 200, 0}, + /* SPINNER */ {AUTOMATIC, false, 1, 200, 0}, + /* POTTERY */ {AUTOMATIC, false, 1, 200, 0}, + /* GLAZING */ {AUTOMATIC, false, 1, 200, 0}, + /* PRESSING */ {AUTOMATIC, false, 1, 200, 0}, + /* BEEKEEPING */ {AUTOMATIC, false, 1, 200, 0}, + /* WAX_WORKING */ {AUTOMATIC, false, 1, 200, 0}, + /* HANDLE_VEHICLES */ {HAULERS, false, 1, 200, 0}, + /* HAUL_TRADE */ {HAULERS, false, 1, 200, 0}, + /* PULL_LEVER */ {HAULERS, false, 1, 200, 0}, + /* UNUSED_13 */ {DISABLE, false, 1, 0, 0}, + /* HAUL_WATER */ {HAULERS, false, 1, 200, 0}, + /* GELD */ {AUTOMATIC, false, 1, 200, 0}, + /* BUILD_ROAD */ {AUTOMATIC, false, 1, 200, 0}, + /* BUILD_CONSTRUCTION */ {AUTOMATIC, false, 1, 200, 0}, + /* PAPERMAKING */ {AUTOMATIC, false, 1, 200, 0}, + /* BOOKBINDING */ {AUTOMATIC, false, 1, 200, 0}, + /* UNUSED_20 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_21 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_22 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_23 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_24 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_25 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_26 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_27 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_28 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_29 */ {DISABLE, false, 0, 0, 0}, + /* UNUSED_30 */ {DISABLE, false, 0, 0, 0}, +}; + +// the table is indexed by labor enum value: one entry per item from 0 to +// the last labor (NONE is -1 and has no entry). a DF release that adds or +// removes labor types breaks the build here until the table is updated. +static_assert(ARRAY_COUNT(default_labor_infos) == + (int)ENUM_LAST_ITEM(unit_labor) + 1, + "default_labor_infos must have one entry per unit_labor enum item"); + +struct dwarf_info_t +{ + int highest_skill; + int total_skill; + int mastery_penalty; + int assigned_jobs; + dwarf_state state; + bool has_exclusive_labor; + int noble_penalty; // penalty for assignment due to noble status + bool medical; // this dwarf has medical responsibility + bool trader; // this dwarf has trade responsibility + bool diplomacy; // this dwarf meets with diplomats +}; + +static const int32_t CYCLE_TICKS = 61; +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle + +static void reset_labor(df::unit_labor labor) +{ + labor_infos[labor].set_minimum_dwarfs(default_labor_infos[labor].minimum_dwarfs); + labor_infos[labor].set_maximum_dwarfs(default_labor_infos[labor].maximum_dwarfs); + labor_infos[labor].set_talent_pool(200); + labor_infos[labor].set_mode(default_labor_infos[labor].mode); +} + +namespace autolabor { + +void legacy_labor_coverage(std::vector &missing) +{ + int enum_count = 0; + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE) + continue; + enum_count++; + // default_labor_infos is indexed by labor enum value; a labor added + // by a new DF release has no default and would be invisible here + if ((size_t)l >= ARRAY_COUNT(default_labor_infos)) + missing.push_back(ENUM_KEY_STR(unit_labor, l) + + ": no entry in legacy labor defaults"); + } + // a labor removed from the enum leaves stale entries behind + if (enum_count != (int)ARRAY_COUNT(default_labor_infos)) + missing.push_back("labor defaults table has " + + std::to_string(ARRAY_COUNT(default_labor_infos)) + + " entries for " + std::to_string(enum_count) + + " labor types"); +} + +} // namespace autolabor + +void LegacyEngine::enable(color_ostream &out) +{ + // purge any state left behind by the modern engine + wdm->reset(); + wdm->shutdown(); + + auto cfg_haulpct = World::GetPersistentSiteData("autolabor/haulpct"); + hauler_pct = cfg_haulpct.isValid() ? cfg_haulpct.ival(0) : 33; + + // Load labors from save + labor_infos.resize(ARRAY_COUNT(default_labor_infos)); + + std::vector items; + World::GetPersistentSiteData(&items, "autolabor/labors/", true); + + for (auto& p : items) + { + std::string key = p.key(); + df::unit_labor labor = (df::unit_labor) atoi(key.substr(strlen("autolabor/labors/")).c_str()); + if (labor >= 0 && size_t(labor) < labor_infos.size()) + { + labor_infos[labor].config = p; + labor_infos[labor].is_exclusive = default_labor_infos[labor].is_exclusive; + labor_infos[labor].active_dwarfs = 0; + } + } + + // Add default labors for those not in save + for (size_t i = 0; i < ARRAY_COUNT(default_labor_infos); i++) { + if (labor_infos[i].config.isValid()) + continue; + + std::stringstream name; + name << "autolabor/labors/" << i; + + labor_infos[i].config = World::AddPersistentSiteData(name.str()); + + labor_infos[i].is_exclusive = default_labor_infos[i].is_exclusive; + labor_infos[i].active_dwarfs = 0; + reset_labor((df::unit_labor) i); + } + + // bypass DF's work detail system + game->external_flag.bits.automatic_professions_disabled = true; + + cycle_timestamp = 0; + initialized = true; + out << "Enabling autolabor (legacy mode)." << std::endl; +} + +void LegacyEngine::disable(color_ostream &out) +{ + initialized = false; + labor_infos.clear(); + + // reinstate DF's work detail system and force a recompute so the + // labor matrix is rebuilt from the player's work details immediately + game->external_flag.bits.automatic_professions_disabled = false; + for (auto u : world->units.active) + if (Units::isCitizen(u)) + Units::setAutomaticProfessions(u); + + out << "Disabling autolabor." << std::endl; +} + +void LegacyEngine::map_unload() +{ + initialized = false; + labor_infos.clear(); + if (game) + game->external_flag.bits.automatic_professions_disabled = false; +} + +// sorting objects +struct dwarfinfo_sorter +{ + dwarfinfo_sorter(std::vector & info):dwarf_info(info){}; + bool operator() (int i,int j) + { + if (dwarf_info[i].state == IDLE && dwarf_info[j].state != IDLE) + return true; + if (dwarf_info[i].state != IDLE && dwarf_info[j].state == IDLE) + return false; + return dwarf_info[i].mastery_penalty > dwarf_info[j].mastery_penalty; + }; + std::vector & dwarf_info; +}; +struct laborinfo_sorter +{ + bool operator() (int i,int j) + { + if (labor_infos[i].mode() != labor_infos[j].mode()) + return labor_infos[i].mode() < labor_infos[j].mode(); + if (labor_infos[i].is_exclusive != labor_infos[j].is_exclusive) + return labor_infos[i].is_exclusive; + if (labor_infos[i].maximum_dwarfs() != labor_infos[j].maximum_dwarfs()) + return labor_infos[i].maximum_dwarfs() < labor_infos[j].maximum_dwarfs(); + return false; + }; +}; + +struct values_sorter +{ + values_sorter(std::vector & values):values(values){}; + bool operator() (int i,int j) + { + return values[i] > values[j]; + }; + std::vector & values; +}; + + +static void assign_labor(unit_labor::unit_labor labor, + int n_dwarfs, + std::vector& dwarf_info, + bool trader_requested, + std::vector& dwarfs, + bool has_butchers, + bool has_fishery, + color_ostream& out) +{ + df::job_skill skill = labor_to_skill[labor]; + + if (labor_infos[labor].mode() != AUTOMATIC) + return; + + int best_skill = 0; + + std::vector values(n_dwarfs); + std::vector candidates; + std::map dwarf_skill; + std::map dwarf_skillxp; + std::vector previously_enabled(n_dwarfs); + + // Find candidate dwarfs, and calculate a preference value for each dwarf + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + if (dwarf_info[dwarf].state == CHILD) + continue; + if (dwarf_info[dwarf].state == MILITARY) + continue; + if (dwarf_info[dwarf].trader && trader_requested) + continue; + if (dwarf_info[dwarf].diplomacy) + continue; + + if (labor_infos[labor].is_exclusive && dwarf_info[dwarf].has_exclusive_labor) + continue; + + int value = dwarf_info[dwarf].mastery_penalty; + + if (skill != job_skill::NONE) + { + int skill_level = 0; + int skill_experience = 0; + + for (auto s : dwarfs[dwarf]->status.souls[0]->skills) + { + if (s->id == skill) + { + skill_level = s->rating; + skill_experience = s->experience; + break; + } + } + + dwarf_skill[dwarf] = skill_level; + dwarf_skillxp[dwarf] = skill_experience; + + if (best_skill < skill_level) + best_skill = skill_level; + + value += skill_level * 100; + value += skill_experience / 20; + if (skill_level > 0 || skill_experience > 0) + value += 200; + if (skill_level >= 15) + value += 1000 * (skill_level - 14); + } + else + { + dwarf_skill[dwarf] = 0; + } + + if (dwarfs[dwarf]->status.labors[labor]) + { + value += 5; + if (labor_infos[labor].is_exclusive) + value += 350; + } + + if (dwarf_info[dwarf].has_exclusive_labor) + value -= 500; + + // bias by happiness + + //value += dwarfs[dwarf]->status.happiness; + + values[dwarf] = value; + + candidates.push_back(dwarf); + + } + + int pool = labor_infos[labor].talent_pool(); + if (pool < 200 && candidates.size() > 1 && size_t(abs(pool)) < candidates.size()) + { + // Sort in descending order + std::sort(candidates.begin(), candidates.end(), [&](const int lhs, const int rhs) -> bool { + if (dwarf_skill[lhs] == dwarf_skill[rhs]) + if (pool > 0) + return dwarf_skillxp[lhs] > dwarf_skillxp[rhs]; + else + return dwarf_skillxp[lhs] < dwarf_skillxp[rhs]; + else + if (pool > 0) + return dwarf_skill[lhs] > dwarf_skill[rhs]; + else + return dwarf_skill[lhs] < dwarf_skill[rhs]; + }); + + // Check if all dwarves have equivalent skills, usually zero + int first_dwarf = candidates[0]; + int last_dwarf = candidates[candidates.size() - 1]; + if (dwarf_skill[first_dwarf] == dwarf_skill[last_dwarf] && + dwarf_skillxp[first_dwarf] == dwarf_skillxp[last_dwarf]) + { + // There's no difference in skill, so change nothing + } + else + { + // Trim down to our top (or not) talents + candidates.resize(abs(pool)); + } + } + + // Sort candidates by preference value + values_sorter ivs(values); + std::sort(candidates.begin(), candidates.end(), ivs); + + // Disable the labor on everyone + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + if (dwarf_info[dwarf].state == CHILD) + continue; + + previously_enabled[dwarf] = dwarfs[dwarf]->status.labors[labor]; + dwarfs[dwarf]->status.labors[labor] = false; + } + + int min_dwarfs = labor_infos[labor].minimum_dwarfs(); + int max_dwarfs = labor_infos[labor].maximum_dwarfs(); + + // Special - don't assign hunt without a butchers, or fish without a fishery + if (unit_labor::HUNT == labor && !has_butchers) + min_dwarfs = max_dwarfs = 0; + if (unit_labor::FISH == labor && !has_fishery) + min_dwarfs = max_dwarfs = 0; + + // If there are enough idle dwarves to choose from, enter an aggressive assignment + // mode. "Enough" idle dwarves is defined as 2 or 10% of the total number of dwarves, + // whichever is higher. + // + // In aggressive mode, we will always pick at least one idle dwarf for each skill, + // in order to try to get the idle dwarves to start doing something. We also pick + // any dwarf more preferable to the idle dwarf, since we'd rather have a more + // preferable dwarf do a new job if one becomes available (probably because that + // dwarf just finished a job). + // + // In non-aggressive mode, only dwarves that are good at a labor will be assigned + // to it. Dwarves good at nothing, or nothing that needs doing, will tend to get + // assigned to hauling by the hauler code. If there are no hauling jobs to do, + // they will sit around idle and when enough build up they will trigger aggressive + // mode again. + bool aggressive_mode = state_count[IDLE] >= 2 && state_count[IDLE] >= n_dwarfs * idler_pct / 100; + + /* + * Assign dwarfs to this labor. We assign at least the minimum number of dwarfs, in + * order of preference, and then assign additional dwarfs that meet any of these conditions: + * - We are in aggressive mode and have not yet assigned an idle dwarf + * - The dwarf is good at this skill + * - The labor is mining, hunting, or woodcutting and the dwarf currently has it enabled. + * We stop assigning dwarfs when we reach the maximum allowed. + * Note that only idle and busy dwarfs count towards the number of dwarfs. "Other" dwarfs + * (sleeping, eating, on break, etc.) will have labors assigned, but will not be counted. + * Military and children/nobles will not have labors assigned. + * Dwarfs with the "health management" responsibility are always assigned DIAGNOSIS. + */ + for (size_t i = 0; i < candidates.size() && labor_infos[labor].active_dwarfs < max_dwarfs; i++) + { + int dwarf = candidates[i]; + + if (dwarf_info[dwarf].trader && trader_requested) + continue; + if (dwarf_info[dwarf].diplomacy) + continue; + + bool preferred_dwarf = false; + if (dwarf_skillxp[dwarf] > 0 && dwarf_skill[dwarf] >= best_skill / 2) + preferred_dwarf = true; + if (previously_enabled[dwarf] && labor_infos[labor].is_exclusive && dwarf_info[dwarf].state == EXCLUSIVE) + preferred_dwarf = true; + if (dwarf_info[dwarf].medical && labor == df::unit_labor::DIAGNOSE) + preferred_dwarf = true; + + if (labor_infos[labor].active_dwarfs >= min_dwarfs && !preferred_dwarf && !aggressive_mode) + continue; + + if (!dwarfs[dwarf]->status.labors[labor]) + dwarf_info[dwarf].assigned_jobs++; + + if (Units::isValidLabor(dwarfs[dwarf], labor)) + dwarfs[dwarf]->status.labors[labor] = true; + + if (labor_infos[labor].is_exclusive) + { + dwarf_info[dwarf].has_exclusive_labor = true; + // all the exclusive labors require equipment so this should force the dorf to reequip if needed + dwarfs[dwarf]->uniform.pickup_flags.bits.update = 1; + } + + TRACE(legacy_cycle, out).print("Dwarf {} \"{}\" assigned {}: value {} {} {}\n", + dwarf, dwarfs[dwarf]->name.first_name, ENUM_KEY_STR(unit_labor, labor), values[dwarf], + dwarf_info[dwarf].trader ? "(trader)" : "", + dwarf_info[dwarf].diplomacy ? "(diplomacy)" : ""); + + if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) + labor_infos[labor].active_dwarfs++; + + if (dwarf_info[dwarf].state == IDLE) + aggressive_mode = false; + } +} + +void LegacyEngine::update(color_ostream &out) +{ + if (!world || !world->map.block_index || !initialized) + return; + + if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) + return; + + cycle_timestamp = world->frame_counter; + + std::vector dwarfs; + + auto bscan = autolabor::scan_buildings(); + bool has_butchers = bscan.has_butchers; + bool has_fishery = bscan.has_fishery; + bool trader_requested = bscan.trader_requested; + + for (auto& cre : world->units.active) + { + if (Units::isCitizen(cre)) + { + if (cre->burrows.size() > 0) + continue; // dwarfs assigned to active burrows are skipped entirely + dwarfs.push_back(cre); + } + } + + int n_dwarfs = dwarfs.size(); + + if (n_dwarfs == 0) + return; + + std::vector dwarf_info(n_dwarfs); + + // Find total skill and highest skill for each dwarf. More skilled dwarves shouldn't be used for minor tasks. + + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + if (dwarfs[dwarf]->status.souls.size() <= 0) + continue; + + // compute noble penalty + + auto pos = autolabor::unit_position_info(dwarfs[dwarf]); + dwarf_info[dwarf].noble_penalty = pos.noble_penalty; + dwarf_info[dwarf].medical = pos.medical; + dwarf_info[dwarf].trader = pos.trader; + + // identify dwarfs who are needed for meetings and mark them for exclusion + + if (autolabor::in_diplomacy_meeting(dwarfs[dwarf])) + { + dwarf_info[dwarf].diplomacy = true; + DEBUG(legacy_cycle, out).print("Dwarf {} \"{}\" has a meeting, will be cleared of all labors\n", + dwarf, dwarfs[dwarf]->name.first_name); + } + + dwarf_info[dwarf].total_skill = autolabor::total_skill(dwarfs[dwarf]); + dwarf_info[dwarf].highest_skill = autolabor::top_skill(dwarfs[dwarf]); + } + + // Calculate a base penalty for using each dwarf for a task he isn't good at. + + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + dwarf_info[dwarf].mastery_penalty -= 40 * dwarf_info[dwarf].highest_skill; + dwarf_info[dwarf].mastery_penalty -= 10 * dwarf_info[dwarf].total_skill; + dwarf_info[dwarf].mastery_penalty -= dwarf_info[dwarf].noble_penalty; + + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE) + continue; + + if (labor_infos[labor].is_exclusive && dwarfs[dwarf]->status.labors[labor]) + dwarf_info[dwarf].mastery_penalty -= 100; + } + } + + // Find the activity state for each dwarf. It's important to get this right - a dwarf who we think is IDLE but + // can't work will gum everything up. In the future I might add code to auto-detect slacker dwarves. + + state_count.clear(); + state_count.resize(NUM_STATE); + + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + dwarf_info[dwarf].state = autolabor::get_dwarf_state(dwarfs[dwarf]); + if (dwarf_info[dwarf].state == OTHER && + dwarfs[dwarf]->job.current_job && + (dwarfs[dwarf]->job.current_job->job_type < 0 || + size_t(dwarfs[dwarf]->job.current_job->job_type) >= dwarf_state_count)) + { + WARN(legacy_cycle, out).print("Dwarf {} \"{}\" has unknown job {}\n", + dwarf, dwarfs[dwarf]->name.first_name, int(dwarfs[dwarf]->job.current_job->job_type)); + } + + state_count[dwarf_info[dwarf].state]++; + + TRACE(legacy_cycle, out).print("Dwarf {} \"{}\": penalty {}, state {}\n", + dwarf, dwarfs[dwarf]->name.first_name, dwarf_info[dwarf].mastery_penalty, state_names[dwarf_info[dwarf].state]); + } + + std::vector labors; + + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE) + continue; + + labor_infos[labor].active_dwarfs = 0; + + labors.push_back(labor); + } + laborinfo_sorter lasorter; + std::sort(labors.begin(), labors.end(), lasorter); + + // Handle DISABLED skills (just bookkeeping). + // Note that autolabor should *NEVER* enable or disable a skill that has been marked as DISABLED, for any reason. + // The user has told us that they want manage this skill manually, and we must respect that. + for (auto& labor: labors) + { + if (labor_infos[labor].mode() != DISABLE) + continue; + + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + if (dwarfs[dwarf]->status.labors[labor]) + { + if (labor_infos[labor].is_exclusive) + dwarf_info[dwarf].has_exclusive_labor = true; + + dwarf_info[dwarf].assigned_jobs++; + } + } + } + + // Handle all skills except those marked HAULERS + + for (auto& labor : labors) + { + assign_labor(labor, n_dwarfs, dwarf_info, trader_requested, dwarfs, has_butchers, has_fishery, out); + } + + // Set about 1/3 of the dwarfs as haulers. The haulers have all HAULER labors enabled. Having a lot of haulers helps + // make sure that hauling jobs are handled quickly rather than building up. + + int num_haulers = state_count[IDLE] + (state_count[BUSY] + state_count[EXCLUSIVE]) * hauler_pct / 100; + + if (num_haulers < 1) + num_haulers = 1; + + std::vector hauler_ids; + for (int dwarf = 0; dwarf < n_dwarfs; dwarf++) + { + if ((dwarf_info[dwarf].trader && trader_requested) || + dwarf_info[dwarf].diplomacy) + { + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE) + continue; + if (labor_infos[labor].mode() != HAULERS) + continue; + dwarfs[dwarf]->status.labors[labor] = false; + } + if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) + { + num_haulers--; + } + continue; + } + + if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) + hauler_ids.push_back(dwarf); + } + dwarfinfo_sorter sorter(dwarf_info); + // Idle dwarves come first, then we sort from least-skilled to most-skilled. + std::sort(hauler_ids.begin(), hauler_ids.end(), sorter); + + // don't set any haulers if everyone is off drinking or something + if (hauler_ids.size() == 0) { + num_haulers = 0; + } + + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE) + continue; + + if (labor_infos[labor].mode() != HAULERS) + continue; + + for (int i = 0; i < num_haulers; i++) + { + int dwarf = hauler_ids[i]; + + dwarfs[dwarf]->status.labors[labor] = true; + dwarf_info[dwarf].assigned_jobs++; + + if (dwarf_info[dwarf].state == IDLE || dwarf_info[dwarf].state == BUSY || dwarf_info[dwarf].state == EXCLUSIVE) + labor_infos[labor].active_dwarfs++; + + TRACE(legacy_cycle, out).print("Dwarf {} \"{}\" assigned {}: hauler\n", + dwarf, dwarfs[dwarf]->name.first_name, ENUM_KEY_STR(unit_labor, labor)); + } + + for (size_t i = num_haulers; i < hauler_ids.size(); i++) + { + int dwarf = hauler_ids[i]; + + dwarfs[dwarf]->status.labors[labor] = false; + } + } +} + +static void print_labor (df::unit_labor labor, color_ostream &out) +{ + std::string labor_name = ENUM_KEY_STR(unit_labor, labor); + out << labor_name << ": "; + for (int i = 0; i < 20 - (int)labor_name.length(); i++) + out << ' '; + if (labor_infos[labor].mode() == DISABLE) + out << "disabled" << std::endl; + else + { + if (labor_infos[labor].mode() == HAULERS) + out << "haulers"; + else + out << "minimum " << labor_infos[labor].minimum_dwarfs() << ", maximum " << labor_infos[labor].maximum_dwarfs() + << ", pool " << labor_infos[labor].talent_pool(); + out << ", currently " << labor_infos[labor].active_dwarfs << " dwarfs" << std::endl; + } +} + +bool LegacyEngine::command(color_ostream &out, std::vector & parameters) +{ + if (parameters.empty()) + return false; + if (!initialized) + { + out << "Error: The plugin is not enabled in legacy mode." << std::endl; + return true; + } + + if (parameters.size() == 2 && parameters[0] == "haulpct") + { + int pct = atoi (parameters[1].c_str()); + hauler_pct = pct; + return true; + } + else if (parameters.size() >= 2 && parameters.size() <= 4) + { + df::unit_labor labor = unit_labor::NONE; + + FOR_ENUM_ITEMS(unit_labor, test_labor) + { + if (parameters[0] == ENUM_KEY_STR(unit_labor, test_labor)) + labor = test_labor; + } + + if (labor == unit_labor::NONE) + { + out.printerr("Could not find labor {}.\n", parameters[0]); + return true; + } + + if (parameters[1] == "haulers") + { + labor_infos[labor].set_mode(HAULERS); + print_labor(labor, out); + return true; + } + if (parameters[1] == "disable") + { + labor_infos[labor].set_mode(DISABLE); + print_labor(labor, out); + return true; + } + if (parameters[1] == "reset") + { + reset_labor(labor); + print_labor(labor, out); + return true; + } + + int minimum = atoi (parameters[1].c_str()); + int maximum = 200; + int pool = 200; + + if (parameters.size() >= 3) + maximum = atoi (parameters[2].c_str()); + if (parameters.size() == 4) + pool = std::stoi(parameters[3]); + + if (maximum < minimum || maximum < 0 || minimum < 0) + { + out.printerr("Syntax: autolabor [] [], {} > {}\n", maximum, minimum); + return true; + } + + labor_infos[labor].set_minimum_dwarfs(minimum); + labor_infos[labor].set_maximum_dwarfs(maximum); + labor_infos[labor].set_talent_pool(pool); + labor_infos[labor].set_mode(AUTOMATIC); + print_labor(labor, out); + + return true; + } + else if (parameters.size() == 1 && parameters[0] == "reset-all") + { + for (size_t i = 0; i < labor_infos.size(); i++) + { + reset_labor((df::unit_labor) i); + } + out << "All labors reset." << std::endl; + return true; + } + else if (parameters.size() == 1 && (parameters[0] == "list" || parameters[0] == "status")) + { + bool need_comma = 0; + for (int i = 0; i < NUM_STATE; i++) + { + if (state_count[i] == 0) + continue; + if (need_comma) + out << ", "; + out << state_count[i] << ' ' << state_names[i]; + need_comma = 1; + } + out << std::endl; + + if (parameters[0] == "list") + { + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE) + continue; + + print_labor(labor, out); + } + } + + return true; + } + + return false; +} diff --git a/plugins/autolabor/legacyengine.h b/plugins/autolabor/legacyengine.h new file mode 100644 index 0000000000..28336cf135 --- /dev/null +++ b/plugins/autolabor/legacyengine.h @@ -0,0 +1,24 @@ +#pragma once + +#include "engines.h" +#include "workdetails.h" + +// The classic autolabor engine: decides which units should have each labor +// and writes unit->status.labors directly. While enabled it sets +// game->external_flag.automatic_professions_disabled so the vanilla work +// detail system does not fight the plugin's writes. +class LegacyEngine : public LaborEngine { +public: + LegacyEngine(WorkDetailManager *wdm) : wdm(wdm) {} + + void enable(color_ostream &out) override; + void disable(color_ostream &out) override; + void update(color_ostream &out) override; + bool command(color_ostream &out, std::vector ¶meters) override; + + void map_unload(); + +private: + WorkDetailManager *wdm; + bool initialized = false; +}; diff --git a/plugins/autolabor/modernengine.cpp b/plugins/autolabor/modernengine.cpp new file mode 100644 index 0000000000..f4edde356b --- /dev/null +++ b/plugins/autolabor/modernengine.cpp @@ -0,0 +1,1457 @@ +/* + * Modern engine for the autolabor plugin ("labormanager" command). + * + * Expresses labor policy through DF's work detail system rather than writing + * the labor matrix directly. The v50+ job auction already prefers skilled + * workers for skilled jobs, so this engine focuses on what the auction does + * badly: starving jobs that require no skill, and giving skilled dwarves + * room to practice and teach. + */ + +#include "joblabormapper.h" +#include "modernengine.h" +#include "monitorengine.h" + +#include +#include +#include +#include +#include +#include + +#include "Debug.h" +#include "MiscUtils.h" +#include "PluginManager.h" + +#include "modules/Units.h" +#include "modules/World.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +using df::global::plotinfo; +using df::global::world; +using df::global::game; + +namespace DFHack { + DBG_DECLARE(autolabor, modern_cycle, DebugCategory::LINFO); +} + +using namespace autolabor; + +static const int32_t CYCLE_TICKS = 61; +static int32_t cycle_timestamp = 0; + +// a posting that sits unclaimed for this many cycles counts as starving +static const int STARVE_CYCLES = 5; +// a posting on the board longer than this (one game day) triggers a +// task starvation warning in the DFHack notification panel +static const int32_t STARVE_TICKS = 1200; +// specialists work this many consecutive cycles before earning rest +static const int BUSY_LIMIT = 10; + +static const std::string LABORER_DETAIL = "auto:Laborers"; +static const std::string CARE_DETAIL = "auto:Care"; +static const std::string SKILL_PREFIX = "auto:Skill:"; + +// labors that should stay available to every unit even when specialized; +// these are covered by the Care detail so flag-restricted units keep them +static bool is_care_labor(df::unit_labor l) +{ + switch (l) { + case unit_labor::RECOVER_WOUNDED: + case unit_labor::FEED_WATER_CIVILIANS: + case unit_labor::HAUL_WATER: + return true; + default: + return false; + } +} + +enum tools_enum { + TOOL_NONE, TOOL_PICK, TOOL_AXE, TOOL_CROSSBOW, + TOOLS_MAX +}; + +static tools_enum labor_tool(df::unit_labor l) +{ + switch (l) { + case unit_labor::MINE: return TOOL_PICK; + case unit_labor::CUTWOOD: return TOOL_AXE; + case unit_labor::HUNT: return TOOL_CROSSBOW; + default: return TOOL_NONE; + } +} + +namespace autolabor { + +void modern_labor_coverage(std::vector &missing) +{ + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE) + continue; + // the exclusive-labor check and the tool-cap mapping must agree; + // a labor that needs a tool without being flagged would let the + // engine oversubscribe equipment + if (is_exclusive_labor(l) != (labor_tool(l) != TOOL_NONE)) + missing.push_back(ENUM_KEY_STR(unit_labor, l) + + ": is_exclusive_labor/labor_tool disagree"); + } +} + +} // namespace autolabor + +// --------------------------------------------------------------------------- +// per-labor managed flag ("autolabor/modern/labors/", ival(0) != 0 managed) +// --------------------------------------------------------------------------- + +static bool labor_managed(df::unit_labor l) +{ + std::stringstream key; + key << "autolabor/modern/labors/" << int(l); + auto item = World::GetPersistentSiteData(key.str()); + return !item.isValid() || item.ival(0) != 0; +} + +static void set_labor_managed(df::unit_labor l, bool managed) +{ + std::stringstream key; + key << "autolabor/modern/labors/" << int(l); + auto item = World::GetPersistentSiteData(key.str(), true); + if (item.isValid()) + item.ival(0) = managed ? 1 : 0; +} + +// migrate "unmanaged" marks from the old labormanager/2.0 config once +static void migrate_old_config() +{ + auto marker = World::GetPersistentSiteData("autolabor/modern/migrated", true); + if (!marker.isValid() || marker.ival(0) == 1) + return; + marker.ival(0) = 1; + + std::vector items; + World::GetPersistentWorldData(&items, "labormanager/2.0/labors/", true); + for (auto &p : items) + { + std::string key = p.key(); + int labor = atoi(key.substr(strlen("labormanager/2.0/labors/")).c_str()); + // old config: maximum_dwarfs in ival(2), -1 meant unmanaged + if (labor >= 0 && labor < NUM_LABORS && p.ival(2) == -1) + set_labor_managed((df::unit_labor)labor, false); + } +} + +// --------------------------------------------------------------------------- +// per-unit scan info +// --------------------------------------------------------------------------- + +// how a unit's unmet needs (personality.needs, focus_level < 0) bias the +// role assignment. pull values are scaled 0..50. +struct NeedBias { + int laborer_pull = 0; // hauling/care work satisfies the need + int idle_pull = 0; // needs downtime (socialize, rest) + std::map favored; // skills that satisfy unmet needs +}; + +struct UnitInfo { + df::unit *u = nullptr; + dwarf_state state = OTHER; + df::job_skill skill = job_skill::NONE; // top Normal/Medical skill + int rating = 0; + int total = 0; + int noble_penalty = 0; + bool trader = false; + bool diplomacy = false; + bool excluded = false; // cannot take assignments at all + NeedBias needs; +}; + +// per-unit persistent tracking for guild-hall rest rotation +struct SpecTrack { + int busy_streak = 0; + int rest_left = 0; +}; + +static JobLaborMapper *labor_mapper = nullptr; + +// per-posting tracking: how long it's been on the board and which skill +// the job exercises (job pointers go bad once a posting is dead, so the +// skill is recorded up front) +struct PostingTrack { + int32_t first_seen = 0; + df::job_skill skill = df::job_skill::NONE; +}; +static std::map posting_track; // posting idx -> info + +// job-history skill usage, decayed per cycle so recently-used skills +// outweigh ones the fort abandoned +static std::map skill_usage; +static std::map unclaimed_streak; // labor -> cycles +static std::map unit_track; // unit id -> tracking +static std::vector managed_labor_cache; + +// status for the overlay +static int stat_laborers = 0; +static int stat_specialists = 0; +static int stat_resting = 0; +static int stat_generalists = 0; + +// job-board starvation stats (since the engine was last enabled) +static int stat_starving = 0; // live postings older than STARVE_TICKS +static int32_t stat_oldest_wait = 0; // age of the oldest live posting +static df::job_type stat_oldest_job = df::job_type::NONE; // oldest posting's job +static std::string stat_oldest_reaction; // custom reaction code, if any +static df::coord stat_oldest_pos; // oldest posting's job location +static int stat_resolved = 0; // postings that left the board +static int64_t stat_resolved_ticks = 0; // total ticks they spent on the board +static int32_t stat_max_wait = 0; // longest time on the board + +static bool has_worker(df::job *j) +{ + for (auto ref : j->general_refs) + if (ref->getType() == df::general_ref_type::UNIT_WORKER) + return true; + return false; +} + +// --------------------------------------------------------------------------- +// working-state persistence +// --------------------------------------------------------------------------- + +// The engine's accumulated state (posting ages, skill usage history, +// starvation streaks, guild rest rotation, stats) is written to site data +// every cycle so it's included whenever the fort is saved, then restored +// on enable after a load. + +static std::vector split_str(const std::string &s, char delim) +{ + std::vector out; + std::string cur; + for (char c : s) + { + if (c == delim) + { + out.push_back(cur); + cur.clear(); + } + else + cur += c; + } + out.push_back(cur); + return out; +} + +static void save_state() +{ + auto p = World::GetPersistentSiteData("autolabor/engine-state", true); + if (!p.isValid()) + return; + + std::stringstream ss; + ss << "stats:" << stat_resolved << ',' << stat_resolved_ticks << ',' + << stat_max_wait << ';'; + ss << "post:"; + for (auto &kv : posting_track) + ss << kv.first << ',' << kv.second.first_seen << ',' + << (int)kv.second.skill << '|'; + ss << ";usage:"; + for (auto &kv : skill_usage) + ss << (int)kv.first << ',' << int(kv.second * 1000) << '|'; + ss << ";streak:"; + for (auto &kv : unclaimed_streak) + ss << kv.first << ',' << kv.second << '|'; + ss << ";track:"; + for (auto &kv : unit_track) + ss << kv.first << ',' << kv.second.busy_streak << ',' + << kv.second.rest_left << '|'; + p.val() = ss.str(); +} + +static void restore_state() +{ + auto p = World::GetPersistentSiteData("autolabor/engine-state"); + if (!p.isValid()) + return; + + for (auto &sec : split_str(p.val(), ';')) + { + size_t colon = sec.find(':'); + if (colon == std::string::npos) + continue; + std::string name = sec.substr(0, colon); + std::string body = sec.substr(colon + 1); + + if (name == "stats") + { + auto f = split_str(body, ','); + if (f.size() == 3) + { + stat_resolved = atoi(f[0].c_str()); + stat_resolved_ticks = atoll(f[1].c_str()); + stat_max_wait = atoi(f[2].c_str()); + } + } + else if (name == "post") + { + for (auto &e : split_str(body, '|')) + { + auto f = split_str(e, ','); + if (f.size() == 3) + { + PostingTrack t; + t.first_seen = atoi(f[1].c_str()); + t.skill = (df::job_skill)atoi(f[2].c_str()); + posting_track[atoi(f[0].c_str())] = t; + } + } + } + else if (name == "usage") + { + for (auto &e : split_str(body, '|')) + { + auto f = split_str(e, ','); + if (f.size() == 2) + skill_usage[(df::job_skill)atoi(f[0].c_str())] = + atoi(f[1].c_str()) / 1000.0; + } + } + else if (name == "streak") + { + for (auto &e : split_str(body, '|')) + { + auto f = split_str(e, ','); + if (f.size() == 2) + unclaimed_streak[atoi(f[0].c_str())] = atoi(f[1].c_str()); + } + } + else if (name == "track") + { + for (auto &e : split_str(body, '|')) + { + auto f = split_str(e, ','); + if (f.size() == 3) + { + SpecTrack t; + t.busy_streak = atoi(f[1].c_str()); + t.rest_left = atoi(f[2].c_str()); + unit_track[atoi(f[0].c_str())] = t; + } + } + } + } +} + +void ModernEngine::enable(color_ostream &out) +{ + game->external_flag.bits.automatic_professions_disabled = false; + + if (!labor_mapper) + labor_mapper = new JobLaborMapper(); + init_labor_to_skill(); + migrate_old_config(); + + managed_labor_cache.assign(NUM_LABORS, true); + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE) + managed_labor_cache[l] = labor_managed(l); + + wdm->reset(); + posting_track.clear(); + skill_usage.clear(); + unclaimed_streak.clear(); + unit_track.clear(); + cycle_timestamp = 0; + stat_starving = 0; + stat_oldest_wait = 0; + stat_oldest_job = df::job_type::NONE; + stat_oldest_reaction.clear(); + stat_resolved = 0; + stat_resolved_ticks = 0; + stat_max_wait = 0; + restore_state(); + initialized = true; + + out << "Enabling labormanager (work detail mode)." << std::endl; +} + +void ModernEngine::disable(color_ostream &out) +{ + initialized = false; + wdm->shutdown(); + out << "Disabling labormanager." << std::endl; +} + +void ModernEngine::map_unload() +{ + initialized = false; + posting_track.clear(); + skill_usage.clear(); + unclaimed_streak.clear(); + unit_track.clear(); + stat_starving = 0; + stat_oldest_wait = 0; + stat_oldest_job = df::job_type::NONE; + stat_oldest_reaction.clear(); + stat_resolved = 0; + stat_resolved_ticks = 0; + stat_max_wait = 0; +} + +int ModernEngine::starving_jobs() +{ + return initialized ? stat_starving : 0; +} + +df::job_type ModernEngine::oldest_starving_job() +{ + return initialized && stat_starving > 0 ? stat_oldest_job + : df::job_type::NONE; +} + +int32_t ModernEngine::oldest_starving_wait() +{ + return initialized && stat_starving > 0 ? stat_oldest_wait : 0; +} + +df::coord ModernEngine::oldest_starving_pos() +{ + return initialized && stat_starving > 0 ? stat_oldest_pos + : df::coord(); +} + +// display name for the oldest starving job: for custom reactions, resolve +// the job's reaction code to the reaction's own name +static std::string oldest_starving_name() +{ + if (stat_oldest_job == df::job_type::CustomReaction && + !stat_oldest_reaction.empty()) + for (auto r : df::reaction::get_vector()) + if (r->code == stat_oldest_reaction && !r->name.empty()) + return r->name; + const char *cap = ENUM_ATTR(job_type, caption, stat_oldest_job); + return cap ? cap : ENUM_KEY_STR(job_type, stat_oldest_job); +} + +std::string ModernEngine::oldest_starving_name() +{ + return initialized && stat_starving > 0 ? ::oldest_starving_name() : ""; +} + +std::string ModernEngine::status_line() +{ + std::stringstream ss; + ss << stat_laborers << " laborers, " << stat_specialists << " specialists"; + if (stat_resting > 0) + ss << " (" << stat_resting << " on guild rest)"; + ss << ", " << stat_generalists << " generalists"; + return ss.str(); +} + +// --------------------------------------------------------------------------- +// demand scan +// --------------------------------------------------------------------------- + +static void reconcile(WorkDetailManager *wdm, df::work_detail *wd, + const std::set &desired); +static df::work_detail_icon_type skill_icon(df::job_skill skill); + +static void scan_demand(std::map &backlog) +{ + backlog.clear(); + stat_starving = 0; + stat_oldest_wait = 0; + stat_oldest_job = df::job_type::NONE; + stat_oldest_reaction.clear(); + + std::set live_postings; + + for (auto jp : world->jobs.postings) + { + if (jp->flags.bits.dead || !jp->job) + continue; + df::job *j = jp->job; + if (j->flags.bits.suspend || j->flags.bits.item_lost) + continue; + if (has_worker(j)) + continue; + + live_postings.insert(jp->idx); + + df::unit_labor labor = labor_mapper->find_job_labor(j); + + if (!posting_track.count(jp->idx)) + { + PostingTrack t; + t.first_seen = world->frame_counter; + if (labor >= 0 && labor < NUM_LABORS) + t.skill = labor_to_skill[labor]; + posting_track[jp->idx] = t; + } + + // starvation stats: how long has this posting been on the board + int32_t age = world->frame_counter - posting_track[jp->idx].first_seen; + if (age > stat_oldest_wait) + { + stat_oldest_wait = age; + stat_oldest_job = j->job_type; + stat_oldest_reaction = j->reaction_name; + stat_oldest_pos = j->pos; + } + if (age >= STARVE_TICKS) + stat_starving++; + + if (labor < 0 || labor >= NUM_LABORS) + continue; + backlog[labor]++; + } + + // postings that left the board (assigned, cancelled, or suspended): + // record how long they spent on the board and the skill they exercised + for (auto it = posting_track.begin(); it != posting_track.end();) + { + if (!live_postings.count(it->first)) + { + int32_t wait = world->frame_counter - it->second.first_seen; + stat_resolved++; + stat_resolved_ticks += wait; + stat_max_wait = std::max(stat_max_wait, wait); + if (it->second.skill != df::job_skill::NONE) + skill_usage[it->second.skill] += 1.0; + it = posting_track.erase(it); + } + else + ++it; + } + + // decay usage so recently-used skills outweigh abandoned ones + for (auto &kv : skill_usage) + kv.second *= 0.995; + + // stockpile job counters catch hauling demand that hasn't spawned jobs yet + backlog[unit_labor::HAUL_STONE] += world->stockpile.num_jobs[1]; + backlog[unit_labor::HAUL_WOOD] += world->stockpile.num_jobs[2]; + backlog[unit_labor::HAUL_ITEM] += world->stockpile.num_jobs[3]; + backlog[unit_labor::HAUL_ITEM] += world->stockpile.num_jobs[4]; + backlog[unit_labor::HAUL_BODY] += world->stockpile.num_jobs[5]; + backlog[unit_labor::HAUL_FOOD] += world->stockpile.num_jobs[6]; + backlog[unit_labor::HAUL_REFUSE] += world->stockpile.num_jobs[7]; + backlog[unit_labor::HAUL_FURNITURE] += world->stockpile.num_jobs[8]; + backlog[unit_labor::HAUL_ANIMALS] += world->stockpile.num_jobs[9]; + + for (auto v : world->vehicles.all) + if (v->route_id != -1) + backlog[unit_labor::HANDLE_VEHICLES]++; + + // track how many consecutive cycles each labor has had unclaimed work + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE) + continue; + if (backlog[l] > 0) + unclaimed_streak[l]++; + else + unclaimed_streak[l] = 0; + } +} + +// --------------------------------------------------------------------------- +// guild halls +// --------------------------------------------------------------------------- + +static std::set guilded_professions() +{ + std::set profs; + + auto site = df::world_site::find(plotinfo->site_id); + if (!site) + return profs; + + for (auto ab : site->buildings) + { + if (auto gh = virtual_cast(ab)) + { + df::profession p = gh->contents.profession; + if (p != profession::NONE) + profs.insert(p); + } + } + + return profs; +} + +// does this skill's profession (or an ancestor profession) have a guild hall? +static bool is_guilded(df::job_skill skill, const std::set &guilded) +{ + df::profession p = (df::profession)ENUM_ATTR(job_skill, profession, skill); + while (p != profession::NONE) + { + if (guilded.count(p)) + return true; + p = (df::profession)ENUM_ATTR(profession, parent, p); + } + return false; +} + +// --------------------------------------------------------------------------- +// tools +// --------------------------------------------------------------------------- + +static void count_tools(int tool_count[TOOLS_MAX]) +{ + for (int e = 0; e < TOOLS_MAX; e++) + tool_count[e] = 0; + + df::item_flags bad_flags; + bad_flags.whole = 0; +#define F(x) bad_flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); +#undef F + + for (auto item : world->items.other.IN_PLAY) + { + if (item->flags.whole & bad_flags.whole) + continue; + if (!item->isWeapon()) + continue; + + df::itemdef_weaponst* weapondef = ((df::item_weaponst*)item)->subtype; + if ((df::job_skill)weapondef->skill_melee == df::job_skill::MINING) + tool_count[TOOL_PICK]++; + else if ((df::job_skill)weapondef->skill_melee == df::job_skill::AXE) + tool_count[TOOL_AXE]++; + else if ((df::job_skill)weapondef->skill_ranged == df::job_skill::CROSSBOW) + tool_count[TOOL_CROSSBOW]++; + } +} + +// --------------------------------------------------------------------------- +// needs +// --------------------------------------------------------------------------- + +static int rating_in_skill(df::unit *u, df::job_skill skill) +{ + if (u->status.current_soul) + for (auto s : u->status.current_soul->skills) + if (s->id == skill) + return s->rating; + return 0; +} + +// craft labors satisfy the BeCreative / CraftObject / ThinkAbstractly needs +static const df::unit_labor craft_labors[] = { + unit_labor::WOOD_CRAFT, unit_labor::STONE_CRAFT, unit_labor::BONE_CARVE, + unit_labor::METAL_CRAFT, unit_labor::GLASSMAKER, unit_labor::POTTERY, + unit_labor::BOWYER, unit_labor::CLOTHESMAKER, unit_labor::WEAVER, + unit_labor::LEATHER, unit_labor::CUT_GEM, unit_labor::ENCRUST_GEM, + unit_labor::PAPERMAKING, unit_labor::BOOKBINDING, unit_labor::WAX_WORKING, + unit_labor::EXTRACT_STRAND, unit_labor::SPINNER, unit_labor::DYER, +}; + +// labors whose products carry a quality modifier that scales with the +// worker's skill -- skilled specialists matter most for these +static const df::unit_labor quality_labors[] = { + unit_labor::CARPENTER, unit_labor::STONECUTTER, unit_labor::STONE_CARVER, + unit_labor::ENGRAVER, unit_labor::MASON, unit_labor::MECHANIC, + unit_labor::WOOD_CRAFT, unit_labor::STONE_CRAFT, unit_labor::BONE_CARVE, + unit_labor::METAL_CRAFT, unit_labor::FORGE_WEAPON, unit_labor::FORGE_ARMOR, + unit_labor::FORGE_FURNITURE, unit_labor::GLASSMAKER, unit_labor::POTTERY, + unit_labor::GLAZING, unit_labor::BOWYER, unit_labor::CLOTHESMAKER, + unit_labor::LEATHER, unit_labor::CUT_GEM, unit_labor::ENCRUST_GEM, + unit_labor::SIEGECRAFT, unit_labor::COOK, unit_labor::PAPERMAKING, + unit_labor::BOOKBINDING, +}; + +// labors whose skills a strange mood can raise to legendary +static const df::unit_labor moodable_labors[] = { + unit_labor::CARPENTER, unit_labor::STONECUTTER, unit_labor::STONE_CARVER, + unit_labor::WOOD_CRAFT, unit_labor::BONE_CARVE, unit_labor::METAL_CRAFT, + unit_labor::FORGE_WEAPON, unit_labor::FORGE_ARMOR, + unit_labor::FORGE_FURNITURE, unit_labor::BOWYER, unit_labor::MECHANIC, + unit_labor::MASON, unit_labor::CLOTHESMAKER, unit_labor::WEAVER, + unit_labor::LEATHER, unit_labor::TANNER, unit_labor::CUT_GEM, + unit_labor::ENCRUST_GEM, unit_labor::GLASSMAKER, unit_labor::POTTERY, + unit_labor::GLAZING, unit_labor::WAX_WORKING, unit_labor::SMELT, + unit_labor::SIEGECRAFT, +}; + +static bool is_labor_skill(const df::unit_labor *labors, size_t count, + df::job_skill skill) +{ + for (size_t i = 0; i < count; i++) + if (labor_to_skill[labors[i]] == skill) + return true; + return false; +} + +// products of these skills carry skill-scaled quality modifiers +static bool is_quality_skill(df::job_skill skill) +{ + return is_labor_skill(quality_labors, + sizeof(quality_labors)/sizeof(df::unit_labor), skill); +} + +// strange moods can advance these skills to legendary +static bool is_moodable_skill(df::job_skill skill) +{ + return is_labor_skill(moodable_labors, + sizeof(moodable_labors)/sizeof(df::unit_labor), skill); +} + +static bool has_moodable_skill(df::unit *u) +{ + for (auto l : moodable_labors) + if (rating_in_skill(u, labor_to_skill[l]) > 0) + return true; + return false; +} + +// frequently-used skills are worth specializing more aggressively +static int usage_bonus(df::job_skill skill) +{ + auto it = skill_usage.find(skill); + if (it == skill_usage.end()) + return 0; + return int(std::min(it->second, 20.0) * 5); // 0..100 +} + +static NeedBias scan_needs(df::unit *u) +{ + NeedBias bias; + if (!u->status.current_soul) + return bias; + + for (auto need : u->status.current_soul->personality.needs) + { + // focus_level runs -999 (desperate) to 400 (satisfied) + if (need->focus_level >= 0) + continue; + int urgency = std::min(-need->focus_level, 400) / 8; // 0..50 + if (!urgency) + continue; + + switch (need->id) + { + // staying occupied, acquiring things (stockpiling, hauling to the + // depot), and helping (patients, prisoners, wounded) are all + // satisfied by unskilled laborer work + case need_type::StayOccupied: + case need_type::AcquireObject: + bias.laborer_pull += urgency; + break; + // helping: patient care is laborer work, and leading guildhall + // demonstrations needs idle time + case need_type::HelpSomebody: + bias.laborer_pull += urgency; + bias.idle_pull += urgency / 2; + break; + // social needs are satisfied by free time; keep these units out of + // the restrictive pools so they can idle and socialize, and let + // guilded ones earn rest (and teach) sooner + case need_type::Socialize: + case need_type::MakeMerry: + case need_type::TakeItEasy: + case need_type::BeWithFamily: + case need_type::BeWithFriends: + case need_type::HearEloquence: + case need_type::LearnSomething: // watching demos is guild idle time + bias.idle_pull += urgency; + break; + // wandering satisfied by fishing, hunting, plant gathering + case need_type::Wander: + bias.favored[labor_to_skill[unit_labor::FISH]] += urgency; + bias.favored[labor_to_skill[unit_labor::HUNT]] += urgency; + bias.favored[labor_to_skill[unit_labor::HERBALIST]] += urgency; + break; + // excitement: catching vermin, danger (hunting); performing and + // socializing are downtime activities + case need_type::Excitement: + bias.favored[labor_to_skill[unit_labor::TRAPPER]] += urgency; + bias.favored[labor_to_skill[unit_labor::HUNT]] += urgency; + bias.idle_pull += urgency / 2; + break; + // creativity: artwork and crafts + case need_type::BeCreative: + case need_type::CraftObject: + case need_type::ThinkAbstractly: + for (auto l : craft_labors) + bias.favored[labor_to_skill[l]] += urgency; + break; + default: + break; + } + } + return bias; +} + +// --------------------------------------------------------------------------- +// main cycle +// --------------------------------------------------------------------------- + +void ModernEngine::update(color_ostream &out) +{ + if (!world || !world->map.block_index || !initialized) + return; + if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) + return; + cycle_timestamp = world->frame_counter; + + auto bscan = scan_buildings(); + int tool_count[TOOLS_MAX]; + count_tools(tool_count); + + std::map backlog; + scan_demand(backlog); + + auto guilded = guilded_professions(); + + // --- collect citizens --------------------------------------------------- + + std::vector units; + for (auto cre : world->units.active) + { + if (!is_assignable(cre) || cre->burrows.size() > 0) + continue; + + UnitInfo info; + info.u = cre; + info.state = get_dwarf_state(cre); + info.rating = top_skill(cre, &info.skill); + info.total = total_skill(cre); + + auto pos = unit_position_info(cre); + info.noble_penalty = pos.noble_penalty; + info.trader = pos.trader && bscan.trader_requested; + info.diplomacy = in_diplomacy_meeting(cre); + + info.excluded = info.diplomacy || info.trader || + info.state == CHILD || info.state == MILITARY || + info.state == OTHER || cre->status2.limbs_grasp_count == 0; + + info.needs = scan_needs(cre); + + units.push_back(info); + } + + if (units.empty()) + return; + + int n = units.size(); + + // --- priority weights from the balance slider --------------------------- + + double starve_w = double(NUM_BALANCE_STOPS - 1 - balance()) / (NUM_BALANCE_STOPS - 1); + double skill_w = double(balance()) / (NUM_BALANCE_STOPS - 1); + + // --- laborer pool sizing ------------------------------------------------ + + int unskilled_backlog = 0; + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE || !is_unskilled(l)) + continue; + if (!managed_labor_cache[l]) + continue; + int count = backlog[l]; + if (unclaimed_streak[l] >= STARVE_CYCLES) + count *= 2; // starving labors get doubled weight + unskilled_backlog += count; + } + + int laborer_target = 1 + unskilled_backlog / 2 + + int(std::round(n * 0.25 * starve_w)); + laborer_target = std::min(laborer_target, n); + + // --- pick laborers ------------------------------------------------------ + // pool score: lower is a better laborer; skilled dwarves are protected in + // proportion to the skill-priority weight + + std::vector order; + std::map pool_score; + for (int i = 0; i < n; i++) + { + if (units[i].excluded) + continue; + int score = 0; + switch (units[i].state) { + case IDLE: score = 0; break; + case BUSY: score = 200; break; + case EXCLUSIVE: score = 400; break; + default: score = 800; break; + } + score += units[i].total * 10; + // skilled specialists are protected in proportion to the skill + // weight; quality-affecting and frequently-used skills count extra + // since losing their time costs the fort more + int prot = units[i].rating * 40; + if (is_quality_skill(units[i].skill)) + prot = prot * 3 / 2; + prot += usage_bonus(units[i].skill); + score += int(prot * skill_w * 4); + score += units[i].noble_penalty / 100; + // units whose unmet needs are satisfied by hauling/patient care get + // pulled in; units needing social downtime get pushed out + score -= units[i].needs.laborer_pull * 4; + score += units[i].needs.idle_pull * 4; + pool_score[i] = score; + order.push_back(i); + } + std::sort(order.begin(), order.end(), + [&](int a, int b) { return pool_score[a] < pool_score[b]; }); + + std::set laborer_ids; + for (int i = 0; i < (int)order.size() && (int)laborer_ids.size() < laborer_target; i++) + laborer_ids.insert(units[order[i]].u->id); + + // --- specialists -------------------------------------------------------- + // every remaining citizen with a usable skill gets pinned to that skill's + // detail; guilded specialists rotate through rest periods + + // skills that map to a managed labor AND are actually used by the fort + // (per posting history); a citizen whose top skill has no labor (e.g. + // teaching) or whose skill is never exercised (gelders, animal + // dissectors in most forts) stays a generalist rather than getting a + // reserved detail + std::set viable_skills; + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && managed_labor_cache[l] && !is_unskilled(l) && + skill_usage[labor_to_skill[l]] >= 0.5) + viable_skills.insert(labor_to_skill[l]); + + std::map> skill_members; // skill -> unit ids + std::set specialist_ids; + stat_resting = 0; + + auto skill_allowed = [&](df::job_skill s) { + if (s == labor_to_skill[unit_labor::FISH]) + return config_flag(CF_ALLOW_FISHING) && bscan.has_fishery; + if (s == labor_to_skill[unit_labor::HUNT]) + return config_flag(CF_ALLOW_HUNTING) && bscan.has_butchers; + return true; + }; + + for (int i = 0; i < n; i++) + { + UnitInfo &info = units[i]; + if (info.excluded || laborer_ids.count(info.u->id)) + continue; + + // pick the specialty. normally the unit's top skill; need-favored + // skills and fort usage stats can pull the pick elsewhere. + // mood-shaping: a unit that can still have a strange mood and has + // started a moodable craft gets steered toward moodable skills -- + // preferably valuable ones -- so a mood pays out in a skill the + // fort wants. needs a caste STRANGE_MOODS token and no spent mood. + bool mood_prot = + Units::casteFlagSet(info.u->race, info.u->caste, + df::caste_raw_flags::STRANGE_MOODS) && + !info.u->flags1.bits.had_mood && has_moodable_skill(info.u); + + df::job_skill chosen = job_skill::NONE; + int best_score = -1; + auto consider = [&](df::job_skill want, int bonus) { + if (want == job_skill::NONE || !viable_skills.count(want) || + !skill_allowed(want)) + return; + int r = rating_in_skill(info.u, want); + int score = r * 100 + bonus + usage_bonus(want) + + (is_quality_skill(want) ? r * 20 : 0); + if (score > best_score) + { + best_score = score; + chosen = want; + } + }; + + if (mood_prot) + { + // candidates restricted to moodable skills; quality-affecting + // skills get a large bonus so the pick trends toward valuable + // mood outcomes, and need-favored moodable skills still count + for (auto l : moodable_labors) + { + df::job_skill want = labor_to_skill[l]; + int bonus = is_quality_skill(want) ? 300 : 100; + auto it = info.needs.favored.find(want); + if (it != info.needs.favored.end()) + bonus += it->second * 20; + consider(want, bonus); + } + } + else + { + if (info.skill != job_skill::NONE && info.rating > 0) + consider(info.skill, 0); + for (auto &kv : info.needs.favored) + consider(kv.first, kv.second * 20); + } + if (chosen == job_skill::NONE) + continue; + + specialist_ids.insert(info.u->id); + + // guild-hall rest rotation; units with unmet social/helping needs + // earn rest (and the chance to teach or learn) sooner + SpecTrack &track = unit_track[info.u->id]; + bool guilded_unit = is_guilded(chosen, guilded); + if (guilded_unit) + { + int busy_limit = std::max(4, BUSY_LIMIT - info.needs.idle_pull / 5); + if (info.u->job.current_job) + track.busy_streak++; + else + track.busy_streak = 0; + + if (track.rest_left > 0) + { + track.rest_left--; + stat_resting++; + continue; // resting: keep flag, join no skill detail + } + if (track.busy_streak >= busy_limit) + { + int reserve = idle_reserve(); + track.rest_left = std::max(1, + busy_limit * reserve / std::max(1, 100 - reserve)); + track.busy_streak = 0; + stat_resting++; + continue; + } + } + + skill_members[chosen].push_back(info.u->id); + } + + // --- apprentices -------------------------------------------------------- + // sustained backlog on a skilled labor: pull in unskilled generalists so + // the job gets done and they train up + + for (int i = 0; i < n; i++) + { + UnitInfo &info = units[i]; + if (info.excluded || laborer_ids.count(info.u->id) || + specialist_ids.count(info.u->id)) + continue; + if (info.state != IDLE && info.state != BUSY) + continue; + + // find the most-starving skilled labor this unit could learn + df::job_skill want = job_skill::NONE; + int worst_streak = STARVE_CYCLES; + FOR_ENUM_ITEMS(unit_labor, labor) + { + if (labor == unit_labor::NONE || !managed_labor_cache[labor] || + is_unskilled(labor)) + continue; + if (unclaimed_streak[labor] < worst_streak) + continue; + df::job_skill skill = labor_to_skill[labor]; + tools_enum tool = labor_tool(labor); + if (tool != TOOL_NONE && + (int)skill_members[skill].size() >= tool_count[tool]) + continue; + if (!Units::isValidLabor(info.u, labor)) + continue; + want = skill; + worst_streak = unclaimed_streak[labor]; + } + if (want != job_skill::NONE) + { + skill_members[want].push_back(info.u->id); + specialist_ids.insert(info.u->id); // restrict them to the detail + } + } + + // --- apply -------------------------------------------------------------- + + std::set specialized_ids = laborer_ids; + specialized_ids.insert(specialist_ids.begin(), specialist_ids.end()); + + // Laborers: unskilled labors for the pool (Everybody so generalists keep + // them too; membership is what matters for flag-restricted units) + auto *wd_laborers = wdm->ensure_detail(LABORER_DETAIL, + work_detail_icon_type::HAULERS, work_detail_mode::EverybodyDoesThis); + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && is_unskilled(l) && managed_labor_cache[l]) + wdm->cover(wd_laborers, l, true); + reconcile(wdm, wd_laborers, laborer_ids); + + // Care: safety labors for all specialized units + auto *wd_care = wdm->ensure_detail(CARE_DETAIL, + work_detail_icon_type::ORDERLIES, work_detail_mode::EverybodyDoesThis); + for (auto l : {unit_labor::RECOVER_WOUNDED, unit_labor::FEED_WATER_CIVILIANS, + unit_labor::HAUL_WATER}) + if (managed_labor_cache[l]) + wdm->cover(wd_care, l, true); + reconcile(wdm, wd_care, specialized_ids); + + // per-skill details + std::set live_skill_details; + for (auto &kv : skill_members) + { + df::job_skill skill = kv.first; + std::set members(kv.second.begin(), kv.second.end()); + if (members.empty()) + continue; + + const char *cap = ENUM_ATTR(job_skill, caption_noun, skill); + std::string name = SKILL_PREFIX + (cap ? cap : ENUM_KEY_STR(job_skill, skill)); + live_skill_details.insert(name); + + auto *wd = wdm->ensure_detail(name, skill_icon(skill), + work_detail_mode::OnlySelectedDoesThis); + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && managed_labor_cache[l] && + labor_to_skill[l] == skill) + wdm->cover(wd, l, true); + reconcile(wdm, wd, members); + } + + // delete skill details that no longer have members + for (auto wd : wdm->managed_details()) + if (wd->name.compare(0, SKILL_PREFIX.size(), SKILL_PREFIX) == 0 && + !live_skill_details.count(wd->name)) + wdm->delete_detail(wd); + + // specialization flags + for (int i = 0; i < n; i++) + wdm->set_specialized(units[i].u, specialized_ids.count(units[i].u->id)); + // also clear flags on units no longer assignable (burrows, gone military) + for (int32_t id : std::set(wdm->flagged_units())) + if (auto u = df::unit::find(id)) + if (!is_assignable(u) || u->burrows.size() > 0) + wdm->set_specialized(u, false); + + wdm->commit(); + + save_state(); + + stat_laborers = laborer_ids.size(); + stat_specialists = specialist_ids.size(); + stat_generalists = n - stat_laborers - stat_specialists; + + DEBUG(modern_cycle, out).print( + "cycle: {} units, {} laborers, {} specialists ({} resting), backlog={}\n", + n, stat_laborers, stat_specialists, stat_resting, unskilled_backlog); +} + +// reconcile a detail's membership with the desired id set +static void reconcile(WorkDetailManager *wdm, df::work_detail *wd, + const std::set &desired) +{ + // remove stale members + for (auto it = wd->assigned_units.begin(); it != wd->assigned_units.end();) + { + if (!desired.count(*it)) + { + int32_t id = *it; + it = wd->assigned_units.erase(it); + if (auto u = df::unit::find(id)) + wdm->touch(u); + } + else + ++it; + } + // add new members + for (int32_t id : desired) + wdm->set_membership(wd, id, true); +} + +static df::work_detail_icon_type skill_icon(df::job_skill skill) +{ + df::unit_labor l = (df::unit_labor)ENUM_ATTR(job_skill, labor, skill); + switch (l) { + case unit_labor::MINE: return work_detail_icon_type::MINERS; + case unit_labor::CUTWOOD: return work_detail_icon_type::WOODCUTTERS; + case unit_labor::HUNT: return work_detail_icon_type::HUNTERS; + case unit_labor::PLANT: return work_detail_icon_type::PLANTERS; + case unit_labor::FISH: return work_detail_icon_type::FISHERMEN; + case unit_labor::HERBALIST: return work_detail_icon_type::PLANT_GATHERERS; + case unit_labor::SIEGEOPERATE: return work_detail_icon_type::SIEGE_OPERATORS; + default: return work_detail_icon_type::CUSTOM_1; + } +} + +// --------------------------------------------------------------------------- +// command dialect +// --------------------------------------------------------------------------- + +bool ModernEngine::command(color_ostream &out, std::vector ¶meters) +{ + if (parameters.empty()) + return false; + + if (managed_labor_cache.empty()) + { + managed_labor_cache.assign(NUM_LABORS, true); + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE) + managed_labor_cache[l] = labor_managed(l); + } + + if (parameters.size() == 2 && parameters[0] == "balance") + { + bool ok = false; + for (int i = 0; i < NUM_BALANCE_STOPS; i++) + if (parameters[1] == balance_stop_name(i)) + { + store_balance(i); + ok = true; + } + if (!ok) + { + int v = atoi(parameters[1].c_str()); + if (v >= 0 && v < NUM_BALANCE_STOPS) + { + store_balance(v); + ok = true; + } + } + if (!ok) + { + out.printerr("Syntax: labormanager balance <0-{}|{}|...|{}>\n", + NUM_BALANCE_STOPS - 1, balance_stop_name(0), + balance_stop_name(NUM_BALANCE_STOPS - 1)); + return true; + } + out << "Balance: " << balance_stop_name(balance()) << std::endl; + return true; + } + else if (parameters.size() == 2 && parameters[0] == "idle-reserve") + { + int v = atoi(parameters[1].c_str()); + if (v < 0 || v > 100) + { + out.printerr("Syntax: labormanager idle-reserve <0-100>\n"); + return true; + } + store_idle_reserve(v); + out << "Idle reserve: " << v << "%" << std::endl; + return true; + } + else if (parameters.size() == 3 && parameters[0] == "labor") + { + df::unit_labor labor = unit_labor::NONE; + FOR_ENUM_ITEMS(unit_labor, test) + if (parameters[1] == ENUM_KEY_STR(unit_labor, test)) + labor = test; + if (labor == unit_labor::NONE) + { + out.printerr("Could not find labor {}.\n", parameters[1]); + return true; + } + bool managed = parameters[2] == "managed"; + if (!managed && parameters[2] != "unmanaged") + { + out.printerr("Syntax: labormanager labor managed|unmanaged\n"); + return true; + } + set_labor_managed(labor, managed); + managed_labor_cache[labor] = managed; + wdm->touch_all(); + wdm->commit(); + out << ENUM_KEY_STR(unit_labor, labor) << ": " + << (managed ? "managed" : "unmanaged") << std::endl; + return true; + } + else if (parameters.size() == 1 && + (parameters[0] == "allow-fishing" || parameters[0] == "forbid-fishing")) + { + set_config_flag(CF_ALLOW_FISHING, parameters[0] == "allow-fishing"); + return true; + } + else if (parameters.size() == 1 && + (parameters[0] == "allow-hunting" || parameters[0] == "forbid-hunting")) + { + set_config_flag(CF_ALLOW_HUNTING, parameters[0] == "allow-hunting"); + return true; + } + else if (parameters.size() == 1 && (parameters[0] == "list" || parameters[0] == "status")) + { + out << "Balance: " << balance_stop_name(balance()) + << ", idle reserve " << idle_reserve() << "%" << std::endl; + out << status_line() << std::endl; + out << "job board: " << stat_resolved << " postings resolved"; + if (stat_resolved > 0) + out << ", avg wait " << (stat_resolved_ticks / stat_resolved) + << " ticks, longest " << stat_max_wait << " ticks"; + if (stat_starving > 0) + out << "; " << stat_starving << " starving (> " << STARVE_TICKS + << " ticks, oldest " << stat_oldest_wait << ")"; + out << std::endl; + // most-used skills drive specialization priorities + std::vector> top_used; + for (auto &kv : skill_usage) + if (kv.second >= 0.5) + top_used.push_back(kv); + std::sort(top_used.begin(), top_used.end(), + [](const std::pair &a, + const std::pair &b) { + return a.second > b.second; + }); + if (!top_used.empty()) + { + out << "top skills in use:"; + for (size_t i = 0; i < top_used.size() && i < 5; i++) + out << " " << ENUM_KEY_STR(job_skill, top_used[i].first) + << "(" << int(top_used[i].second) << ")"; + out << std::endl; + } + if (parameters[0] == "list") + { + FOR_ENUM_ITEMS(unit_labor, l) + { + if (l == unit_labor::NONE) + continue; + out << ENUM_KEY_STR(unit_labor, l) << ": " + << (managed_labor_cache[l] ? "managed" : "unmanaged"); + if (unclaimed_streak[l] >= STARVE_CYCLES) + out << " (starved " << unclaimed_streak[l] << " cycles)"; + out << std::endl; + } + } + return true; + } + + return false; +} + +// --------------------------------------------------------------------------- +// monitor mode +// --------------------------------------------------------------------------- +// shares the posting-tracking machinery and persisted state above, but +// performs no labor management -- it just watches the job board and feeds +// the task starvation notification. + +void MonitorEngine::enable(color_ostream &out) +{ + // undo the legacy engine's flag if switching straight from it, so + // vanilla work details govern labors again + game->external_flag.bits.automatic_professions_disabled = false; + + if (!labor_mapper) + labor_mapper = new JobLaborMapper(); + init_labor_to_skill(); + + posting_track.clear(); + skill_usage.clear(); + stat_starving = 0; + stat_oldest_wait = 0; + stat_oldest_job = df::job_type::NONE; + stat_oldest_reaction.clear(); + stat_resolved = 0; + stat_resolved_ticks = 0; + stat_max_wait = 0; + restore_state(); + initialized = true; + + out << "Enabling labor monitor (starvation warnings only)." << std::endl; +} + +void MonitorEngine::disable(color_ostream &out) +{ + initialized = false; + out << "Disabling labor monitor." << std::endl; +} + +void MonitorEngine::map_unload() +{ + initialized = false; + posting_track.clear(); + skill_usage.clear(); + stat_starving = 0; + stat_oldest_wait = 0; + stat_oldest_job = df::job_type::NONE; + stat_oldest_reaction.clear(); + stat_resolved = 0; + stat_resolved_ticks = 0; + stat_max_wait = 0; +} + +void MonitorEngine::update(color_ostream &out) +{ + if (!world || !world->map.block_index || !initialized) + return; + if (world->frame_counter - cycle_timestamp < CYCLE_TICKS) + return; + cycle_timestamp = world->frame_counter; + + // the demand scan does the posting-age tracking and starvation stats; + // the per-labor backlog it also fills is unused here + std::map backlog; + scan_demand(backlog); + save_state(); +} + +int MonitorEngine::starving_jobs() +{ + return initialized ? stat_starving : 0; +} + +df::job_type MonitorEngine::oldest_starving_job() +{ + return initialized && stat_starving > 0 ? stat_oldest_job + : df::job_type::NONE; +} + +int32_t MonitorEngine::oldest_starving_wait() +{ + return initialized && stat_starving > 0 ? stat_oldest_wait : 0; +} + +df::coord MonitorEngine::oldest_starving_pos() +{ + return initialized && stat_starving > 0 ? stat_oldest_pos + : df::coord(); +} + +std::string MonitorEngine::oldest_starving_name() +{ + return initialized && stat_starving > 0 ? ::oldest_starving_name() : ""; +} + +bool MonitorEngine::command(color_ostream &out, std::vector ¶meters) +{ + if (parameters.size() == 1 && + (parameters[0] == "list" || parameters[0] == "status")) + { + out << "labor monitor: " << stat_starving << " starving postings (> " + << STARVE_TICKS << " ticks"; + if (stat_starving > 0 && stat_oldest_job != df::job_type::NONE) + out << ", oldest " << ::oldest_starving_name() + << " at " << stat_oldest_wait << " ticks"; + out << ")" << std::endl; + out << stat_resolved << " postings resolved"; + if (stat_resolved > 0) + out << ", avg wait " << (stat_resolved_ticks / stat_resolved) + << " ticks, longest " << stat_max_wait << " ticks"; + out << std::endl; + return true; + } + return false; +} diff --git a/plugins/autolabor/modernengine.h b/plugins/autolabor/modernengine.h new file mode 100644 index 0000000000..ef0705d6ef --- /dev/null +++ b/plugins/autolabor/modernengine.h @@ -0,0 +1,50 @@ +#pragma once + +#include "engines.h" +#include "laborcommon.h" +#include "workdetails.h" + +#include + +// The modern engine: works through DF's work detail system instead of +// writing the labor matrix directly. It maintains plugin-owned work details +// (named "auto: ...", visible and editable in the vanilla UI) and uses the +// per-unit only_do_assigned_jobs flag to create dedicated pools: +// +// - "auto: Laborers": unskilled labors only; members are specialized so +// the game's job auction always has units that can only take unskilled +// work, preventing starvation of hauling/cleaning jobs. +// - "auto: ": one per exercised skill; members are specialized to +// jobs that train that skill. Specialists whose profession has a guild +// hall get protected idle time to participate in guild activities. +// +// The balance slider trades laborer coverage against specialist protection. +class ModernEngine : public LaborEngine { +public: + ModernEngine(WorkDetailManager *wdm) : wdm(wdm) {} + + void enable(color_ostream &out) override; + void disable(color_ostream &out) override; + void update(color_ostream &out) override; + bool command(color_ostream &out, std::vector ¶meters) override; + + void map_unload(); + + // status summary for the overlay, e.g. "3 laborers, 5 specialists" + std::string status_line(); + + // number of live job postings that have been on the board longer than + // one game day (drives the task starvation notification) + int starving_jobs(); + + // the job type, age (ticks), location, and display name of the + // longest-starving posting, for the starvation notification + df::job_type oldest_starving_job(); + int32_t oldest_starving_wait(); + df::coord oldest_starving_pos(); + std::string oldest_starving_name(); + +private: + WorkDetailManager *wdm; + bool initialized = false; +}; diff --git a/plugins/autolabor/monitorengine.h b/plugins/autolabor/monitorengine.h new file mode 100644 index 0000000000..3b0717e649 --- /dev/null +++ b/plugins/autolabor/monitorengine.h @@ -0,0 +1,34 @@ +#pragma once + +#include "engines.h" +#include "laborcommon.h" + +#include +#include + +// Monitor mode: watches the job board and feeds the task starvation +// notification, but performs no labor management at all -- everything is +// left to the player's own work details and the labor matrix. +// +// The implementation lives in modernengine.cpp so it shares the +// posting-tracking machinery (posting ages, starvation stats, skill usage +// history, persisted state) with the modern engine; switching between +// modes keeps the accumulated history. +class MonitorEngine : public LaborEngine { +public: + void enable(color_ostream &out) override; + void disable(color_ostream &out) override; + void update(color_ostream &out) override; + bool command(color_ostream &out, std::vector ¶meters) override; + + void map_unload(); + + int starving_jobs(); + df::job_type oldest_starving_job(); + int32_t oldest_starving_wait(); + df::coord oldest_starving_pos(); + std::string oldest_starving_name(); + +private: + bool initialized = false; +}; diff --git a/plugins/autolabor/workdetails.cpp b/plugins/autolabor/workdetails.cpp new file mode 100644 index 0000000000..36a7a02cfc --- /dev/null +++ b/plugins/autolabor/workdetails.cpp @@ -0,0 +1,234 @@ +#include "workdetails.h" + +#include + +#include + +#include "modules/Units.h" +#include "modules/World.h" + +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +using df::global::plotinfo; +using df::global::world; + +const char * const WorkDetailManager::NAME_PREFIX = "auto:"; + +static std::vector &details() +{ + return plotinfo->labor_info.work_details; +} + +bool WorkDetailManager::is_managed(df::work_detail *wd) +{ + return wd && wd->name.compare(0, strlen(NAME_PREFIX), NAME_PREFIX) == 0; +} + +void WorkDetailManager::reset() +{ + dirty.clear(); + flagged.clear(); + recompute_all = false; + flagged_cfg = World::GetPersistentSiteData("autolabor/flagged"); + load_flagged(); +} + +void WorkDetailManager::load_flagged() +{ + flagged.clear(); + if (!flagged_cfg.isValid()) + return; + size_t count = flagged_cfg.data_size() / PersistentDataItem::int28_size; + for (size_t i = 0; i < count; i++) + flagged.insert(flagged_cfg.get_int28(i * PersistentDataItem::int28_size)); +} + +void WorkDetailManager::save_flagged() +{ + if (!flagged_cfg.isValid()) + flagged_cfg = World::AddPersistentSiteData("autolabor/flagged"); + if (!flagged_cfg.isValid()) + return; + + size_t off = 0; + for (int32_t id : flagged) + { + flagged_cfg.set_int28(off, id); + off += PersistentDataItem::int28_size; + } + flagged_cfg.val().resize(off); +} + +std::vector WorkDetailManager::managed_details() +{ + std::vector out; + for (auto wd : details()) + if (is_managed(wd)) + out.push_back(wd); + return out; +} + +df::work_detail *WorkDetailManager::find_detail(const std::string &name) +{ + for (auto wd : details()) + if (is_managed(wd) && wd->name == name) + return wd; + return NULL; +} + +df::work_detail *WorkDetailManager::ensure_detail(const std::string &name, + df::work_detail_icon_type icon, df::work_detail_mode mode) +{ + if (auto wd = find_detail(name)) + return wd; + + auto wd = df::allocate(); + wd->name = name; + wd->icon = icon; + wd->flags.bits.mode = mode; + details().push_back(wd); + + // a new non-Everybody detail clears its covered labors globally + if (mode != work_detail_mode::EverybodyDoesThis) + touch_all(); + else + recompute_all = true; + + return wd; +} + +void WorkDetailManager::delete_detail(df::work_detail *wd) +{ + auto &vec = details(); + for (auto it = vec.begin(); it != vec.end(); ++it) + { + if (*it == wd) + { + for (int32_t id : wd->assigned_units) + if (auto u = df::unit::find(id)) + touch(u); + vec.erase(it); + delete wd; + touch_all(); + return; + } + } +} + +void WorkDetailManager::cover(df::work_detail *wd, df::unit_labor labor, bool on) +{ + if (labor < 0 || labor > ENUM_LAST_ITEM(unit_labor)) + return; + if (wd->allowed_labors[labor] != on) + { + wd->allowed_labors[labor] = on; + // coverage changes affect every unit, not just members + recompute_all = true; + } +} + +void WorkDetailManager::set_membership(df::work_detail *wd, int32_t unit_id, bool want) +{ + auto &vec = wd->assigned_units; + auto it = std::lower_bound(vec.begin(), vec.end(), unit_id); + bool has = (it != vec.end() && *it == unit_id); + + if (want && !has) + { + vec.insert(it, unit_id); + if (auto u = df::unit::find(unit_id)) + touch(u); + } + else if (!want && has) + { + vec.erase(it); + if (auto u = df::unit::find(unit_id)) + touch(u); + } +} + +void WorkDetailManager::set_specialized(df::unit *u, bool on) +{ + if (!u) + return; + + if (on) + { + if (!u->flags4.bits.only_do_assigned_jobs) + { + u->flags4.bits.only_do_assigned_jobs = true; + touch(u); + } + flagged.insert(u->id); + } + else + { + // only strip flags we set ourselves + if (flagged.count(u->id)) + { + flagged.erase(u->id); + if (u->flags4.bits.only_do_assigned_jobs) + { + u->flags4.bits.only_do_assigned_jobs = false; + touch(u); + } + } + } +} + +void WorkDetailManager::touch(df::unit *u) +{ + if (u) + dirty.insert(u->id); +} + +void WorkDetailManager::touch_all() +{ + recompute_all = true; +} + +void WorkDetailManager::commit() +{ + if (recompute_all) + { + dirty.clear(); + for (auto u : world->units.active) + if (Units::isCitizen(u)) + touch(u); + recompute_all = false; + } + + for (int32_t id : dirty) + if (auto u = df::unit::find(id)) + Units::setAutomaticProfessions(u); + dirty.clear(); + + save_flagged(); +} + +void WorkDetailManager::shutdown() +{ + // remove all managed details + for (auto wd : managed_details()) + delete_detail(wd); + + // clear specialization flags we set on any still-living unit + for (int32_t id : flagged) + if (auto u = df::unit::find(id)) + if (u->flags4.bits.only_do_assigned_jobs) + { + u->flags4.bits.only_do_assigned_jobs = false; + touch(u); + } + flagged.clear(); + save_flagged(); + + // detail removal affects everyone + touch_all(); + commit(); +} diff --git a/plugins/autolabor/workdetails.h b/plugins/autolabor/workdetails.h new file mode 100644 index 0000000000..00ea8f9e70 --- /dev/null +++ b/plugins/autolabor/workdetails.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include + +#include "modules/Persistence.h" + +#include +#include +#include +#include + +using namespace DFHack; +using namespace df::enums; + +// Manages the plugin-owned work details through which the modern engine +// expresses assignments. Managed details are identified by an "auto:" name +// prefix so the player can see (and, with caveats, edit) what the tool is +// doing in the vanilla work details screen. +// +// The plugin never writes to unit->status.labors directly; all changes go +// through work detail membership and df::unit flags4.only_do_assigned_jobs, +// followed by a call to Units::setAutomaticProfessions() which asks the game +// to recompute the labor matrix. + +class WorkDetailManager { +public: + static const char * const NAME_PREFIX; // "auto:" + + // Rescan labor_info.work_details, adopting any surviving managed details + // and reloading the flagged-unit set. Call on map load before use. + void reset(); + + // Remove all managed details, clear specialization flags we set, and + // recompute every citizen. Call when the modern engine is disabled. + void shutdown(); + + // Find a managed detail by exact name (including prefix). + df::work_detail *find_detail(const std::string &name); + + // Find or create a managed detail with the given name, icon, and mode. + // Coverage (allowed_labors) is only set on creation; callers that change + // coverage or mode afterwards must call touch_all() since pass 1 of the + // game's labor computation clears covered labors for every unit. + df::work_detail *ensure_detail(const std::string &name, + df::work_detail_icon_type icon, df::work_detail_mode mode); + + // Delete a managed detail entirely (labor becomes unrestricted again). + void delete_detail(df::work_detail *wd); + + // Set the detail's coverage of a labor. Requires touch_all() semantics; + // we just mark every citizen dirty. + void cover(df::work_detail *wd, df::unit_labor labor, bool on); + + // Add/remove a unit from a detail's assigned_units (kept sorted). + void set_membership(df::work_detail *wd, int32_t unit_id, bool want); + + // Mark a unit as only doing assigned work (the game's "specialized" + // flag). We track which units we flagged so shutdown() doesn't strip + // flags the player set themselves. + void set_specialized(df::unit *u, bool on); + + // Queue a unit for labor recomputation at commit(). + void touch(df::unit *u); + + // Queue every citizen for recomputation; needed when detail coverage or + // mode changes since non-Everybody details clear their labors globally. + void touch_all(); + + // Apply queued recomputations via Units::setAutomaticProfessions(). + void commit(); + + // Enumerate details whose name starts with the managed prefix. + std::vector managed_details(); + + // Unit ids we have flagged as only_do_assigned_jobs. + const std::set &flagged_units() const { return flagged; } + + bool is_managed(df::work_detail *wd); + +private: + void load_flagged(); + void save_flagged(); + + std::set dirty; + std::set flagged; + bool recompute_all = false; + PersistentDataItem flagged_cfg; +}; diff --git a/plugins/lua/autolabor.lua b/plugins/lua/autolabor.lua index 8cedf0bb19..7f3116291e 100644 --- a/plugins/lua/autolabor.lua +++ b/plugins/lua/autolabor.lua @@ -1,52 +1,6 @@ local _ENV = mkmodule('plugins.autolabor') -local gui = require('gui') -local overlay = require('plugins.overlay') -local widgets = require('gui.widgets') - -AutolaborOverlay = defclass(AutolaborOverlay, overlay.OverlayWidget) -AutolaborOverlay.ATTRS{ - desc='Adds information to the work details screen about whether work details are enabled.', - default_pos={x=7,y=-13}, - default_enabled=true, - viewscreens='dwarfmode/Info/LABOR/WORK_DETAILS', - frame={w=29, h=5}, - frame_style=gui.MEDIUM_FRAME, - frame_background=gui.CLEAR_PEN, -} - -function AutolaborOverlay:init() - self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - text_pen=COLOR_LIGHTRED, - text='DFHack autolabor is active!', - visible=isEnabled, - }, - widgets.Label{ - frame={t=0, l=0}, - text_pen=COLOR_LIGHTRED, - text='Dwarf Therapist is active!', - visible=function() return not isEnabled() end, - }, - widgets.Label{ - frame={t=1, l=0}, - text_pen=COLOR_WHITE, - text={ - 'Any changes made on this', NEWLINE, - 'screen will have no effect.' - }, - }, - } -end - -function AutolaborOverlay:render(dc) - if not df.global.game.external_flag.automatic_professions_disabled then return end - AutolaborOverlay.super.render(self, dc) -end - -OVERLAY_WIDGETS = { - overlay=AutolaborOverlay, -} +-- The labor automation controls are part of the work details overlay in +-- scripts/gui/settings-manager.lua. return _ENV From f7d8dd42269a5f9da7bcc50c4a7fd738e6ecfe0f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 21 Sep 2026 16:49:31 -0500 Subject: [PATCH 2/2] improve moodable skill detection in a way that will make gcc happier --- plugins/autolabor/modernengine.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/plugins/autolabor/modernengine.cpp b/plugins/autolabor/modernengine.cpp index f4edde356b..e52b0722bf 100644 --- a/plugins/autolabor/modernengine.cpp +++ b/plugins/autolabor/modernengine.cpp @@ -716,13 +716,6 @@ static bool is_quality_skill(df::job_skill skill) sizeof(quality_labors)/sizeof(df::unit_labor), skill); } -// strange moods can advance these skills to legendary -static bool is_moodable_skill(df::job_skill skill) -{ - return is_labor_skill(moodable_labors, - sizeof(moodable_labors)/sizeof(df::unit_labor), skill); -} - static bool has_moodable_skill(df::unit *u) { for (auto l : moodable_labors) @@ -1104,9 +1097,8 @@ void ModernEngine::update(color_ostream &out) // Care: safety labors for all specialized units auto *wd_care = wdm->ensure_detail(CARE_DETAIL, work_detail_icon_type::ORDERLIES, work_detail_mode::EverybodyDoesThis); - for (auto l : {unit_labor::RECOVER_WOUNDED, unit_labor::FEED_WATER_CIVILIANS, - unit_labor::HAUL_WATER}) - if (managed_labor_cache[l]) + FOR_ENUM_ITEMS(unit_labor, l) + if (l != unit_labor::NONE && is_care_labor(l) && managed_labor_cache[l]) wdm->cover(wd_care, l, true); reconcile(wdm, wd_care, specialized_ids);