diff --git a/CMakeLists.txt b/CMakeLists.txt index 38eb6c92b..56c81ba72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,8 +61,10 @@ if(UNIX) endif() if(WIN32) - if((NOT MSVC) OR (MSVC_VERSION LESS 1930) OR (MSVC_VERSION GREATER 1934)) - message(SEND_ERROR "MSVC 2022 is required") + if(NOT MSVC) + message(SEND_ERROR "No MSVC found! MSVC 2022 version 1930 to 1935 is required.") + elseif((MSVC_VERSION LESS 1930) OR (MSVC_VERSION GREATER 1935)) + message(SEND_ERROR "MSVC 2022 version 1930 to 1935 is required, Version Found: ${MSVC_VERSION}") endif() endif() @@ -190,7 +192,7 @@ endif() # set up versioning. set(DF_VERSION "50.07") -set(DFHACK_RELEASE "alpha1") +set(DFHACK_RELEASE "alpha2") set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") diff --git a/depends/md5/md5.cpp b/depends/md5/md5.cpp index 044df259e..8aa9ba38c 100644 --- a/depends/md5/md5.cpp +++ b/depends/md5/md5.cpp @@ -158,7 +158,7 @@ void MD5Final(unsigned char digest[16], MD5Context *ctx) */ void MD5Transform(uint32_t buf[4], uint32_t in[16]) { - register uint32_t a, b, c, d; + uint32_t a, b, c, d; a = buf[0]; b = buf[1]; diff --git a/docs/changelog.txt b/docs/changelog.txt index 51b81de0e..4a3428031 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -35,39 +35,64 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## New Plugins +## Fixes +-@ ``widgets.HotkeyLabel``: don't trigger on click if the widget is disabled +- ``dfhack.job.isSuitableMaterial``: now properly detects lack of fire and magma safety for vulnerable materials with high melting points + +## Misc Improvements + +## Documentation + +## API +- Gui focus strings will no longer get the "dfhack/" prefix if the string "dfhack/" already exists in the focus string + +## Lua +- ``dfhack.job.attachJobItem()``: allows you to attach specific items to a job +- ``dfhack.screen.paintTile()``: you can now explicitly clear the interface cursor from a map tile by passing ``0`` as the tile value +- ``widgets.Label``: token ``tile`` properties can now be functions that return a value +- ``widgets.CycleHotkeyLabel``: add ``label_below`` attribute for compact 2-line output +-@ ``widgets.FilteredList``: search key matching is now case insensitive by default +-@ ``gui.INTERIOR_FRAME``: a panel frame style for use in highlighting off interior areas of a UI + +## Removed +-@ ``gui.THIN_FRAME``: replaced by ``gui.INTERIOR_FRAME`` + +# 50.07-alpha2 + ## Fixes -@ `nestboxes`: fixed bug causing nestboxes themselves to be forbidden, which prevented citizens from using them to lay eggs. Now only eggs are forbidden. - `autobutcher`: implemented work-around for Dwarf Fortress not setting nicknames properly, so that nicknames created in the in-game interface are detected & protect animals from being butchered properly. Note that nicknames for unnamed units are not currently saved by dwarf fortress - use ``enable fix/protect-nicks`` to fix any nicknames created/removed within dwarf fortress so they can be saved/reloaded when you reload the game. -@ `seedwatch`: fix saving and loading of seed stock targets - `autodump`: changed behaviour to only change ``dump`` and ``forbid`` flags if an item is successfully dumped. -@ `autochop`: generate default names for burrows with no assigned names -- ``Buildings::StockpileIterator``: check for stockpile items on block boundary. -- `dig-now`: fixed multi-layer channel designations only channeling every second layer -- `tailor`: block making clothing sized for toads; make replacement clothing orders use the size of the wearer, not the size of the garment; add support for adamantine cloth (off by default); improve logging +- ``Buildings::StockpileIterator``: fix check for stockpile items on block boundary. +- `tailor`: block making clothing sized for toads; make replacement clothing orders use the size of the wearer, not the size of the garment -@ `confirm`: fix fps drop when enabled +- `channel-safely`: fix an out of bounds error regarding the REPORT event listener receiving (presumably) stale id's ## Misc Improvements +- `autobutcher`: logs activity to the console terminal instead of making disruptive in-game announcements - DFHack tool windows that capture mouse clicks (and therefore prevent you from clicking on the "pause" button) now unconditionally pause the game when they open (but you can still unpause with the keyboard if you want to). Examples of this behavior: `gui/quickfort`, `gui/blueprint`, `gui/liquids` - `showmood`: now shows the number of items needed for cloth and bars in addition to the technically correct but always confusing "total dimension" (150 per bar or 10,000 per cloth) -@ Stopped mouse clicks from affecting the map when a click on a DFHack screen dismisses the window - `confirm`: configuration data is now persisted globally. -- `dig-now`: added handling of dig designations that have been converted into active jobs - -## Documentation +- `tailor`: add support for adamantine cloth (off by default); improve logging ## API - ``Gui::any_civzone_hotkey``, ``Gui::getAnyCivZone``, ``Gui::getSelectedCivZone``: new functions to operate on the new zone system -- Units module: added new predicates for: - - ``isGeldable()`` - - ``isMarkedForGelding()`` - - ``isPet()`` +- Units module: added new predicates for ``isGeldable()``, ``isMarkedForGelding()``, and ``isPet()`` +- ``Military``: New module for military functionality +- ``Military``: new ``makeSquad`` to create a squad +- ``Military``: changed ``getSquadName`` to take a squad identifier +- ``Military``: new ``updateRoomAssignments`` for assigning a squad to a barracks and archery range +- ``Maps::GetBiomeType`` renamed to ``Maps::getBiomeType`` for consistency +- ``Maps::GetBiomeTypeRef`` renamed to ``Maps::getBiomeTypeRef`` for consistency ## Lua - ``dfhack.gui.getSelectedCivZone``: returns the Zone that the user has selected currently - ``widgets.FilteredList``: Added ``edit_on_change`` optional parameter to allow a custom callback on filter edit change. -- Added ``widgets.TabBar`` and ``widgets.Tab`` (migrated from control-panel.lua) - -## Removed +- ``widgets.TabBar``: new library widget (migrated from control-panel.lua) +- ``maps.getBiomeType``: exposed preexisting function to Lua # 50.07-alpha1 diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 02260513f..9f2386660 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1219,6 +1219,15 @@ Job module if there are any jobs with ``first_id <= id < job_next_id``, a lua list containing them. +* ``dfhack.job.attachJobItem(job, item, role, filter_idx, insert_idx)`` + + Attach a real item to this job. If the item is intended to satisfy a job_item + filter, the index of that filter should be passed in ``filter_idx``; otherwise, + pass ``-1``. Similarly, if you don't care where the item is inserted, pass + ``-1`` for ``insert_idx``. The ``role`` param is a ``df.job_item_ref.T_role``. + If the item needs to be brought to the job site, then the value should be + ``df.job_item_ref.T_role.Hauled``. + * ``dfhack.job.isSuitableItem(job_item, item_type, item_subtype)`` Does basic sanity checks to verify if the suggested item type matches @@ -1580,6 +1589,24 @@ Units module Returns a table of the cutoffs used by the above stress level functions. +Military module +~~~~~~~~~~~~~~~~~~~ + +* ``dfhack.military.makeSquad(assignment_id)`` + + Creates a new squad associated with the assignment (ie ``df::entity_position_assignment``, via ``id``) and returns it. + Fails if a squad already exists that is associated with that assignment, or if the assignment is not a fort mode player controlled squad. + Note: This function does not name the squad: consider setting a nickname (under ``squad.name.nickname``), and/or filling out the ``language_name`` object at ``squad.name``. + The returned squad is otherwise complete and requires no more setup to work correctly. + +* ``dfhack.military.updateRoomAssignments(squad_id, assignment_id, squad_use_flags)`` + + Sets the sleep, train, indiv_eq, and squad_eq flags when training at a barracks. + +* ``dfhack.military.getSquadName(squad_id)`` + + Returns the name of a squad as a string. + Action Timer API ~~~~~~~~~~~~~~~~ @@ -3757,6 +3784,12 @@ Misc Wraps ``dfhack.screen.getKeyDisplay`` in order to allow using strings for the keycode argument. +* ``invert_color(color, bold)`` + + This inverts the brightness of ``color``. If this color is coming from a pen's + foreground color, include ``pen.bold`` in ``bold`` for this to work properly. + + ViewRect class -------------- @@ -4319,9 +4352,11 @@ There are the following predefined frame style tables: A frame suitable for overlay widget panels. -* ``THIN_FRAME`` +* ``INTERIOR_FRAME`` - A frame suitable for light accent elements. + A frame suitable for light interior accent elements. This frame does *not* have + a visible ``DFHack`` signature on it, so it must not be used as the most external + frame for a DFHack-owned UI. gui.widgets =========== @@ -4662,7 +4697,9 @@ It has the following attributes: :text_pen: Specifies the pen for active text. :text_dpen: Specifies the pen for disabled text. -:text_hpen: Specifies the pen for text hovered over by the mouse, if a click handler is registered. +:text_hpen: Specifies the pen for text hovered over by the mouse, if a click + handler is registered. By default, this will invert the foreground + and background colors. :disabled: Boolean or a callback; if true, the label is disabled. :enabled: Boolean or a callback; if false, the label is disabled. :auto_height: Sets self.frame.h from the text height. @@ -4696,8 +4733,8 @@ containing newlines, or a table with the following possible fields: * ``token.tile = pen`` - Specifies a pen or texture index to paint as one tile before the main part of - the token. + Specifies a pen or texture index (or a function that returns a pen or texture + index) to paint as one tile before the main part of the token. * ``token.width = ...`` @@ -4769,6 +4806,18 @@ The Label widget implements the following methods: ``+halfpage``, ``-halfpage``, ``home``, or ``end``. It returns the number of lines that were actually scrolled (negative for scrolling up). +* ``label:shouldHover()`` + + This method returns whether or not this widget should show a hover effect, + generally you want to return ``true`` if there is some type of mouse handler + present. For example, for a ``HotKeyLabel``:: + + function HotkeyLabel:shouldHover() + -- When on_activate is set, text should also hover on mouseover + return HotkeyLabel.super.shouldHover(self) or self.on_activate + end + + WrappedLabel class ------------------ @@ -4847,6 +4896,8 @@ It has the following attributes: hotkey. :label_width: The number of spaces to allocate to the ``label`` (for use in aligning a column of ``CycleHotkeyLabel`` labels). +:label_below: If ``true``, then the option value will apear below the label + instead of to the right of it. Defaults to ``false``. :options: A list of strings or tables of ``{label=string, value=string[, pen=pen]}``. String options use the same string for the label and value and the default pen. The optional ``pen`` @@ -4903,6 +4954,8 @@ item to call the ``on_submit`` callback for that item. It has the following attributes: :text_pen: Specifies the pen for deselected list entries. +:text_hpen: Specifies the pen for entries that the mouse is hovered over. + Defaults to swapping the background/foreground colors. :cursor_pen: Specifies the pen for the selected entry. :inactive_pen: If specified, used for the cursor when the widget is not active. :icon_pen: Default pen for icons. @@ -4983,7 +5036,7 @@ construction that allows filtering the list by subwords of its items. In addition to passing through all attributes supported by List, it supports: -:case_sensitive: If true, matching is case sensitive. Defaults to true. +:case_sensitive: If ``true``, matching is case sensitive. Defaults to ``false``. :edit_pen: If specified, used instead of ``cursor_pen`` for the edit field. :edit_below: If true, the edit field is placed below the list instead of above. :edit_key: If specified, the edit field is disabled until this key is pressed. diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index 6f606c43a..ecee46918 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -1308,7 +1308,7 @@ legacy Python Quickfort. This setting has no effect on DFHack Quickfort, which will use buildingplan to manage everything designated in a ``#build`` blueprint regardless of the buildingplan UI settings. -However, quickfort *does* use `buildingplan's filters ` +However, quickfort *does* use `buildingplan's filters ` for each building type. For example, you can use the buildingplan UI to set the type of stone you want your walls made out of. Or you can specify that all buildingplan-managed chairs and tables must be of Masterful quality. The current diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index 1eb18b1d5..f77e5b590 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -2,91 +2,93 @@ buildingplan ============ .. dfhack-tool:: - :summary: Plan building construction before you have materials. - :tags: untested fort design buildings - -This plugin adds a planning mode for building placement. You can then place -furniture, constructions, and other buildings before the required materials are -available, and they will be created in a suspended state. Buildingplan will -periodically scan for appropriate items, and the jobs will be unsuspended when -the items are available. - -This is very useful when combined with manager work orders or `workflow` -- you -can set a constraint to always have one or two doors/beds/tables/chairs/etc. -available, and place as many as you like. Materials are used to build the -planned buildings as they are produced, with minimal space dedicated to -stockpiles. + :summary: Plan building layouts with or without materials. + :tags: fort design buildings + +Buildingplan allows you to place furniture, constructions, and other buildings, +regardless of whether the required materials are available. This allows you to +focus purely on design elements when you are laying out your fort, and defers +item production concerns to a more convenient time. + +Buildingplan is as an alternative to the vanilla building placement UI. It +appears after you have selected the type of building, furniture, or construction +that you want to place in the vanilla build menu. Buildingplan then takes over +for the actual placement step. If any building materials are not available yet +for the placed building, it will be created in a suspended state. Buildingplan +will periodically scan for appropriate items and attach them. Once all items are +attached, the construction job will be unsuspended and a dwarf will come and +build the building. If you have the `unsuspend` overlay enabled (it is enabled +by default), then buildingplan-suspended buildings will appear with a ``P`` marker +on the main map, as opposed to the usual ``x`` marker for "regular" suspended +buildings. + +If you want to impose restrictions on which items are chosen for the buildings, +buildingplan has full support for quality and material filters. Before you place +a building, you can select a component item in the list and hit ``f`` or click on +the ``filter`` button next to the item description. This will let you choose your +desired item quality range, whether the item must be decorated, and even which +specific materials the item must be made out of. This lets you create layouts +with a consistent color, if that is part of your design. + +If you just care about the heat sensitivity of the building, you can set the +building to be fire- or magma-proof in the placement UI screen or in any item +filter screen, and the restriction will apply to all building items. This makes it +very easy to create magma-safe pump stacks, for example. + +Buildingplan works very well in conjuction with other design tools like +`gui/quickfort`, which allow you to apply a building layout from a blueprint. You +can apply very large, complicated layouts, and the buildings will simply be built +when your dwarves get around to producing the needed materials. If you set filters +in the buildingplan UI before applying the blueprint, the filters will be applied +to the blueprint buildings, just as if you had planned them from the buildingplan +placement UI. + +One way to integrate buildingplan into your gameplay is to create manager +workorders to ensure you always have a few blocks/doors/beds/etc. available. You +can then place as many of each building as you like. Produced items will be used +to build the planned buildings as they are produced, with minimal space dedicated +to stockpiles. The DFHack `orders` library can help with setting up these manager +workorders for you. + +If you do not wish to use the ``buildingplan`` interface, you can turn off the +``buildingplan.planner`` overlay in `gui/overlay`. You should not disable the +``buildingplan`` service entirely in `gui/control-panel` since then existing +planned buildings in loaded forts will stop functioning. Usage ----- :: - enable buildingplan - buildingplan set - buildingplan set true|false + buildingplan [status] + buildingplan set (true|false) + +Examples +-------- -Running ``buildingplan set`` without parameters displays the current settings. +``buildingplan`` + Print a report of current settings, which kinds of buildings are planned, + and what kinds of materials the buildings are waiting for. .. _buildingplan-settings: Global settings --------------- -The buildingplan plugin has global settings that can be set from the UI -(:kbd:`G` from any building placement screen, for example: -:kbd:`b`:kbd:`a`:kbd:`G`). These settings can also be set via the -``buildingplan set`` command. The available settings are: +The buildingplan plugin has several global settings that affect what materials +can be chosen when attaching items to planned buildings: -``all_enabled`` (default: false) - Enable planning mode for all building types. ``blocks``, ``boulders``, ``logs``, ``bars`` (defaults: true, true, true, false) Allow blocks, boulders, logs, or bars to be matched for generic "building material" items. -``quickfort_mode`` (default: false) - Enable compatibility mode for the legacy Python Quickfort (this setting is - not required for DFHack `quickfort`) -The settings for ``blocks``, ``boulders``, ``logs``, and ``bars`` are saved with -your fort, so you only have to set them once and they will be persisted in your -save. +These settings are saved with your fort, so you only have to set them once and +they will be persisted in your save. If you normally embark with some blocks on hand for early workshops, you might want to add this line to your ``dfhack-config/init/onMapLoad.init`` file to -always configure buildingplan to just use blocks for buildings and +always configure `buildingplan` to just use blocks for buildings and constructions:: - on-new-fortress buildingplan set boulders false; buildingplan set logs false - -.. _buildingplan-filters: - -Item filtering --------------- - -While placing a building, you can set filters for what materials you want the -building made out of, what quality you want the component items to be, and -whether you want the items to be decorated. - -If a building type takes more than one item to construct, use -:kbd:`Ctrl`:kbd:`Left` and :kbd:`Ctrl`:kbd:`Right` to select the item that you -want to set filters for. Any filters that you set will be used for all buildings -of the selected type placed from that point onward (until you set a new filter -or clear the current one). Buildings placed before the filters were changed will -keep the filter values that were set when the building was placed. - -For example, you can be sure that all your constructed walls are the same color -by setting a filter to accept only certain types of stone. - -Quickfort mode --------------- - -If you use the external Python Quickfort to apply building blueprints instead of -the native DFHack `quickfort` script, you must enable Quickfort mode. This -temporarily enables buildingplan for all building types and adds an extra blank -screen after every building placement. This "dummy" screen is needed for Python -Quickfort to interact successfully with Dwarf Fortress. - -Note that Quickfort mode is only for compatibility with the legacy Python -Quickfort. The DFHack `quickfort` script does not need this Quickfort mode to be -enabled. The `quickfort` script will successfully integrate with buildingplan as -long as the buildingplan plugin itself is enabled. + on-new-fortress buildingplan set boulders false + on-new-fortress buildingplan set logs false diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index c5dbc37f6..3acbe66cd 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -3,7 +3,7 @@ channel-safely .. dfhack-tool:: :summary: Auto-manage channel designations to keep dwarves safe. - :tags: fort auto + :tags: untested fort auto Multi-level channel projects can be dangerous, and managing the safety of your dwarves throughout the completion of such projects can be difficult and time diff --git a/docs/plugins/strangemood.rst b/docs/plugins/strangemood.rst index def862a4b..a863943e9 100644 --- a/docs/plugins/strangemood.rst +++ b/docs/plugins/strangemood.rst @@ -3,7 +3,7 @@ strangemood .. dfhack-tool:: :summary: Trigger a strange mood. - :tags: untested fort armok units + :tags: fort armok units Usage ----- diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 87f869493..92db43563 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -137,6 +137,7 @@ set(MODULE_HEADERS include/modules/MapCache.h include/modules/Maps.h include/modules/Materials.h + include/modules/Military.h include/modules/Once.h include/modules/Persistence.h include/modules/Random.h @@ -164,6 +165,7 @@ set(MODULE_SOURCES modules/MapCache.cpp modules/Maps.cpp modules/Materials.cpp + modules/Military.cpp modules/Once.cpp modules/Persistence.cpp modules/Random.cpp diff --git a/library/Core.cpp b/library/Core.cpp index 5375daa3c..01bc89480 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -35,7 +35,6 @@ distribution. #include #include #include -using namespace std; #include "Error.h" #include "MemAccess.h" @@ -61,8 +60,6 @@ using namespace std; #include "LuaTools.h" #include "DFHackVersion.h" -#include "MiscUtils.h" - using namespace DFHack; #include "df/plotinfost.h" @@ -99,7 +96,7 @@ using df::global::world; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. static bool parseKeySpec(std::string keyspec, int *psym, int *pmod, std::string *pfocus = NULL); -size_t loadScriptFiles(Core* core, color_ostream& out, const vector& prefix, const std::string& folder); +size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::string& folder); namespace DFHack { @@ -160,9 +157,9 @@ struct CommandDepthCounter }; thread_local int CommandDepthCounter::depth = 0; -void Core::cheap_tokenise(string const& input, vector &output) +void Core::cheap_tokenise(std::string const& input, std::vector& output) { - string *cur = NULL; + std::string *cur = NULL; size_t i = 0; // Check the first non-space character @@ -234,7 +231,7 @@ void fHKthread(void * iodata) PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; if(plug_mgr == 0 || core == 0) { - cerr << "Hotkey thread has croaked." << endl; + std::cerr << "Hotkey thread has croaked." << std::endl; return; } bool keep_going = true; @@ -256,10 +253,10 @@ void fHKthread(void * iodata) struct sortable { bool recolor; - string name; - string description; + std::string name; + std::string description; //FIXME: Nuke when MSVC stops failing at being C++11 compliant - sortable(bool recolor_,const string& name_,const string & description_): recolor(recolor_), name(name_), description(description_){}; + sortable(bool recolor_,const std::string& name_,const std::string & description_): recolor(recolor_), name(name_), description(description_){}; bool operator <(const sortable & rhs) const { if( name < rhs.name ) @@ -268,9 +265,9 @@ struct sortable }; }; -static string dfhack_version_desc() +static std::string dfhack_version_desc() { - stringstream s; + std::stringstream s; s << Version::dfhack_version() << " "; if (Version::is_release()) s << "(release)"; @@ -284,11 +281,11 @@ static string dfhack_version_desc() namespace { struct ScriptArgs { - const string *pcmd; - vector *pargs; + const std::string *pcmd; + std::vector *pargs; }; struct ScriptEnableState { - const string *pcmd; + const std::string *pcmd; bool pstate; }; } @@ -307,7 +304,7 @@ static bool init_run_script(color_ostream &out, lua_State *state, void *info) return true; } -static command_result runLuaScript(color_ostream &out, std::string name, vector &args) +static command_result runLuaScript(color_ostream &out, std::string name, std::vector &args) { ScriptArgs data; data.pcmd = &name; @@ -346,25 +343,25 @@ command_result Core::runCommand(color_ostream &out, const std::string &command) { if (!command.empty()) { - vector parts; + std::vector parts; Core::cheap_tokenise(command,parts); if(parts.size() == 0) return CR_NOT_IMPLEMENTED; - string first = parts[0]; + std::string first = parts[0]; parts.erase(parts.begin()); if (first[0] == '#') return CR_OK; - cerr << "Invoking: " << command << endl; + std::cerr << "Invoking: " << command << std::endl; return runCommand(out, first, parts); } else return CR_NOT_IMPLEMENTED; } -bool is_builtin(color_ostream &con, const string &command) { +bool is_builtin(color_ostream &con, const std::string &command) { CoreSuspender suspend; auto L = Lua::Core::State; Lua::StackUnwinder top(L); @@ -385,7 +382,7 @@ bool is_builtin(color_ostream &con, const string &command) { return lua_toboolean(L, -1); } -void get_commands(color_ostream &con, vector &commands) { +void get_commands(color_ostream &con, std::vector &commands) { CoreSuspender suspend; auto L = Lua::Core::State; Lua::StackUnwinder top(L); @@ -431,10 +428,10 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std:: return false; } -bool Core::addScriptPath(string path, bool search_before) +bool Core::addScriptPath(std::string path, bool search_before) { - lock_guard lock(script_path_mutex); - vector &vec = script_paths[search_before ? 0 : 1]; + std::lock_guard lock(script_path_mutex); + std::vector &vec = script_paths[search_before ? 0 : 1]; if (std::find(vec.begin(), vec.end(), path) != vec.end()) return false; if (!Filesystem::isdir(path)) @@ -443,13 +440,13 @@ bool Core::addScriptPath(string path, bool search_before) return true; } -bool Core::removeScriptPath(string path) +bool Core::removeScriptPath(std::string path) { - lock_guard lock(script_path_mutex); + std::lock_guard lock(script_path_mutex); bool found = false; for (int i = 0; i < 2; i++) { - vector &vec = script_paths[i]; + std::vector &vec = script_paths[i]; while (1) { auto it = std::find(vec.begin(), vec.end(), path); @@ -464,14 +461,14 @@ bool Core::removeScriptPath(string path) void Core::getScriptPaths(std::vector *dest) { - lock_guard lock(script_path_mutex); + std::lock_guard lock(script_path_mutex); dest->clear(); - string df_path = this->p->getPath() + "/"; + std::string df_path = this->p->getPath() + "/"; for (auto it = script_paths[0].begin(); it != script_paths[0].end(); ++it) dest->push_back(*it); dest->push_back(df_path + CONFIG_PATH + "scripts"); if (df::global::world && isWorldLoaded()) { - string save = World::ReadWorldFolder(); + std::string save = World::ReadWorldFolder(); if (save.size()) dest->push_back(df_path + "/save/" + save + "/scripts"); } @@ -481,13 +478,13 @@ void Core::getScriptPaths(std::vector *dest) } -string Core::findScript(string name) +std::string Core::findScript(std::string name) { - vector paths; + std::vector paths; getScriptPaths(&paths); for (auto it = paths.begin(); it != paths.end(); ++it) { - string path = *it + "/" + name; + std::string path = *it + "/" + name; if (Filesystem::isfile(path)) return path; } @@ -497,7 +494,7 @@ string Core::findScript(string name) bool loadScriptPaths(color_ostream &out, bool silent = false) { using namespace std; - string filename(CONFIG_PATH + "script-paths.txt"); + std::string filename(CONFIG_PATH + "script-paths.txt"); ifstream file(filename); if (!file) { @@ -505,7 +502,7 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) out.printerr("Could not load %s\n", filename.c_str()); return false; } - string raw; + std::string raw; int line = 0; while (getline(file, raw)) { @@ -516,7 +513,7 @@ bool loadScriptPaths(color_ostream &out, bool silent = false) if (!(ss >> ch) || ch == '#') continue; ss >> ws; // discard whitespace - string path; + std::string path; getline(ss, path); if (ch == '+' || ch == '-') { @@ -565,7 +562,7 @@ static std::string sc_event_name (state_change_event id) { return "SC_UNKNOWN"; } -void help_helper(color_ostream &con, const string &entry_name) { +void help_helper(color_ostream &con, const std::string &entry_name) { CoreSuspender suspend; auto L = Lua::Core::State; Lua::StackUnwinder top(L); @@ -583,7 +580,7 @@ void help_helper(color_ostream &con, const string &entry_name) { } } -void tags_helper(color_ostream &con, const string &tag) { +void tags_helper(color_ostream &con, const std::string &tag) { CoreSuspender suspend; auto L = Lua::Core::State; Lua::StackUnwinder top(L); @@ -601,11 +598,11 @@ void tags_helper(color_ostream &con, const string &tag) { } } -void ls_helper(color_ostream &con, const vector ¶ms) { - vector filter; +void ls_helper(color_ostream &con, const std::vector ¶ms) { + std::vector filter; bool skip_tags = false; bool show_dev_commands = false; - string exclude_strs = ""; + std::string exclude_strs = ""; bool in_exclude = false; for (auto str : params) { @@ -641,7 +638,7 @@ void ls_helper(color_ostream &con, const vector ¶ms) { } } -command_result Core::runCommand(color_ostream &con, const std::string &first_, vector &parts) +command_result Core::runCommand(color_ostream &con, const std::string &first_, std::vector &parts) { std::string first = first_; CommandDepthCounter counter; @@ -717,7 +714,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { if (p->size() && (*p)[0] == '-') { - if (p->find('a') != string::npos) + if (p->find('a') != std::string::npos) all = true; } } @@ -876,7 +873,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v } con << parts[0]; bool builtin = is_builtin(con, parts[0]); - string lua_path = findScript(parts[0] + ".lua"); + std::string lua_path = findScript(parts[0] + ".lua"); Plugin *plug = plug_mgr->getPluginByCommand(parts[0]); if (builtin) { @@ -933,31 +930,31 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { std::vector list = ListKeyBindings(parts[1]); if (list.empty()) - con << "No bindings." << endl; + con << "No bindings." << std::endl; for (size_t i = 0; i < list.size(); i++) - con << " " << list[i] << endl; + con << " " << list[i] << std::endl; } else { - con << "Usage:" << endl - << " keybinding list " << endl - << " keybinding clear [@context]..." << endl - << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << endl - << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << endl - << "Later adds, and earlier items within one command have priority." << endl - << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << endl - << "Context may be used to limit the scope of the binding, by" << endl - << "requiring the current context to have a certain prefix." << endl - << "Current UI context is: " << endl - << join_strings("\n", Gui::getCurFocus(true)) << endl; + con << "Usage:" << std::endl + << " keybinding list " << std::endl + << " keybinding clear [@context]..." << std::endl + << " keybinding set [@context] \"cmdline\" \"cmdline\"..." << std::endl + << " keybinding add [@context] \"cmdline\" \"cmdline\"..." << std::endl + << "Later adds, and earlier items within one command have priority." << std::endl + << "Supported keys: [Ctrl-][Alt-][Shift-](A-Z, 0-9, F1-F12, `, or Enter)." << std::endl + << "Context may be used to limit the scope of the binding, by" << std::endl + << "requiring the current context to have a certain prefix." << std::endl + << "Current UI context is: " << std::endl + << join_strings("\n", Gui::getCurFocus(true)) << std::endl; } } else if (first == "alias") { if (parts.size() >= 3 && (parts[0] == "add" || parts[0] == "replace")) { - const string &name = parts[1]; - vector cmd(parts.begin() + 2, parts.end()); + const std::string &name = parts[1]; + std::vector cmd(parts.begin() + 2, parts.end()); if (!AddAlias(name, cmd, parts[0] == "replace")) { con.printerr("Could not add alias %s - already exists\n", name.c_str()); @@ -977,15 +974,15 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v auto aliases = ListAliases(); for (auto p : aliases) { - con << p.first << ": " << join_strings(" ", p.second) << endl; + con << p.first << ": " << join_strings(" ", p.second) << std::endl; } } else { - con << "Usage: " << endl - << " alias add|replace " << endl - << " alias delete|clear " << endl - << " alias list" << endl; + con << "Usage: " << std::endl + << " alias add|replace " << std::endl + << " alias delete|clear " << std::endl + << " alias list" << std::endl; } } else if (first == "fpause") @@ -1038,8 +1035,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v } else { - con << "Usage:" << endl - << " script " << endl; + con << "Usage:" << std::endl + << " script " << std::endl; return CR_WRONG_USAGE; } } @@ -1065,13 +1062,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { if (parts.empty() || parts[0] == "help" || parts[0] == "?") { - con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << endl; - con << "Valid event names (SC_ prefix is optional):" << endl; + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; + con << "Valid event names (SC_ prefix is optional):" << std::endl; for (int i = SC_WORLD_LOADED; i <= SC_UNPAUSED; i++) { std::string name = sc_event_name((state_change_event)i); if (name != "SC_UNKNOWN") - con << " " << name << endl; + con << " " << name << std::endl; } return CR_OK; } @@ -1081,7 +1078,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v parts.push_back(""); if (parts[1].size() && sc_event_id(parts[1]) == SC_UNKNOWN) { - con << "Unrecognized event name: " << parts[1] << endl; + con << "Unrecognized event name: " << parts[1] << std::endl; return CR_WRONG_USAGE; } for (auto it = state_change_scripts.begin(); it != state_change_scripts.end(); ++it) @@ -1100,13 +1097,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) { - con << "Usage: sc-script add EVENT path-to-script [-save]" << endl; + con << "Usage: sc-script add EVENT path-to-script [-save]" << std::endl; return CR_WRONG_USAGE; } state_change_event evt = sc_event_id(parts[1]); if (evt == SC_UNKNOWN) { - con << "Unrecognized event: " << parts[1] << endl; + con << "Unrecognized event: " << parts[1] << std::endl; return CR_FAILURE; } bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); @@ -1115,7 +1112,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { if (script == *it) { - con << "Script already registered" << endl; + con << "Script already registered" << std::endl; return CR_FAILURE; } } @@ -1126,13 +1123,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v { if (parts.size() < 3 || (parts.size() >= 4 && parts[3] != "-save")) { - con << "Usage: sc-script remove EVENT path-to-script [-save]" << endl; + con << "Usage: sc-script remove EVENT path-to-script [-save]" << std::endl; return CR_WRONG_USAGE; } state_change_event evt = sc_event_id(parts[1]); if (evt == SC_UNKNOWN) { - con << "Unrecognized event: " << parts[1] << endl; + con << "Unrecognized event: " << parts[1] << std::endl; return CR_FAILURE; } bool save_specific = (parts.size() >= 4 && parts[3] == "-save"); @@ -1145,13 +1142,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v } else { - con << "Unrecognized script" << endl; + con << "Unrecognized script" << std::endl; return CR_FAILURE; } } else { - con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << endl; + con << "Usage: sc-script add|remove|list|help SC_EVENT [path-to-script] [...]" << std::endl; return CR_WRONG_USAGE; } } @@ -1173,13 +1170,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v if (!svc) continue; - file << "// Plugin: " << plug->getName() << endl; + file << "// Plugin: " << plug->getName() << std::endl; svc->dumpMethods(file); } } else { - con << "Usage: devel/dump-rpc \"filename\"" << endl; + con << "Usage: devel/dump-rpc \"filename\"" << std::endl; return CR_WRONG_USAGE; } } @@ -1196,8 +1193,8 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v } else if (res == CR_NOT_IMPLEMENTED) { - string completed; - string filename = findScript(first + ".lua"); + std::string completed; + std::string filename = findScript(first + ".lua"); bool lua = filename != ""; if ( !lua ) { filename = findScript(first + ".rb"); @@ -1234,22 +1231,22 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v return CR_OK; } -bool Core::loadScriptFile(color_ostream &out, string fname, bool silent) +bool Core::loadScriptFile(color_ostream &out, std::string fname, bool silent) { if(!silent) { INFO(script,out) << "Loading script: " << fname << std::endl; - cerr << "Loading script: " << fname << std::endl; + std::cerr << "Loading script: " << fname << std::endl; } - ifstream script(fname.c_str()); + std::ifstream script(fname.c_str()); if ( !script.good() ) { if(!silent) out.printerr("Error loading script: %s\n", fname.c_str()); return false; } - string command; + std::string command; while(script.good()) { - string temp; + std::string temp; getline(script,temp); bool doMore = false; if ( temp.length() > 0 ) { @@ -1333,18 +1330,18 @@ void fIOthread(void * iodata) while (true) { - string command = ""; + std::string command = ""; int ret; while ((ret = con.lineedit("[DFHack]# ",command, main_history)) == Console::RETRY); if(ret == Console::SHUTDOWN) { - cerr << "Console is shutting down properly." << endl; + std::cerr << "Console is shutting down properly." << std::endl; return; } else if(ret == Console::FAILURE) { - cerr << "Console caught an unspecified error." << endl; + std::cerr << "Console caught an unspecified error." << std::endl; continue; } else if(ret) @@ -1405,7 +1402,7 @@ Core::Core() : void Core::fatal (std::string output) { errorstate = true; - stringstream out; + std::stringstream out; out << output ; if (output[output.size() - 1] != '\n') out << '\n'; @@ -1421,7 +1418,7 @@ void Core::fatal (std::string output) out << "Check file stderr.log for details\n"; MessageBox(0,out.str().c_str(),"DFHack error!", MB_OK | MB_ICONERROR); #else - cout << "DFHack fatal error: " << out.str() << std::endl; + std::cout << "DFHack fatal error: " << out.str() << std::endl; #endif bool is_headless = bool(getenv("DFHACK_HEADLESS")); @@ -1459,16 +1456,16 @@ bool Core::Init() // this is handled as appropriate in Console-posix.cpp fprintf(stdout, "dfhack: redirecting stdout to stdout.log (again)\n"); if (!freopen("stdout.log", "w", stdout)) - cerr << "Could not redirect stdout to stdout.log" << endl; + std::cerr << "Could not redirect stdout to stdout.log" << std::endl; #endif fprintf(stderr, "dfhack: redirecting stderr to stderr.log\n"); if (!freopen("stderr.log", "w", stderr)) - cerr << "Could not redirect stderr to stderr.log" << endl; + std::cerr << "Could not redirect stderr to stderr.log" << std::endl; Filesystem::init(); - cerr << "DFHack build: " << Version::git_description() << "\n" - << "Starting with working directory: " << Filesystem::getcwd() << endl; + std::cerr << "DFHack build: " << Version::git_description() << "\n" + << "Starting with working directory: " << Filesystem::getcwd() << std::endl; // find out what we are... #ifdef LINUX_BUILD @@ -1477,7 +1474,7 @@ bool Core::Init() const char * path = "hack\\symbols.xml"; #endif auto local_vif = dts::make_unique(); - cerr << "Identifying DF version.\n"; + std::cerr << "Identifying DF version.\n"; try { local_vif->loadFile(path); @@ -1518,8 +1515,8 @@ bool Core::Init() "recompile.\n" "More details can be found in stderr.log in this folder.\n" ); - cout << msg << endl; - cerr << msg << endl; + std::cout << msg << std::endl; + std::cerr << msg << std::endl; fatal("Not a known DF version - XML version mismatch (see console or stderr.log)"); } else @@ -1529,13 +1526,13 @@ bool Core::Init() errorstate = true; return false; } - cerr << "Version: " << vinfo->getVersion() << endl; + std::cerr << "Version: " << vinfo->getVersion() << std::endl; p = std::move(local_p); // Init global object pointers df::global::InitGlobals(); - cerr << "Initializing Console.\n"; + std::cerr << "Initializing Console.\n"; // init the console. bool is_text_mode = (init && init->display.flag.is_set(init_display_flags::TEXT)); bool is_headless = bool(getenv("DFHACK_HEADLESS")); @@ -1551,29 +1548,29 @@ bool Core::Init() } else { - cerr << "endwin(): bind failed" << endl; + std::cerr << "endwin(): bind failed" << std::endl; } } else { - cerr << "Headless mode requires PRINT_MODE:TEXT" << endl; + std::cerr << "Headless mode requires PRINT_MODE:TEXT" << std::endl; } #else - cerr << "Headless mode not supported on Windows" << endl; + std::cerr << "Headless mode not supported on Windows" << std::endl; #endif } if (is_text_mode && !is_headless) { - cerr << "Console is not available. Use dfhack-run to send commands.\n"; + std::cerr << "Console is not available. Use dfhack-run to send commands.\n"; if (!is_text_mode) { - cout << "Console disabled.\n"; + std::cout << "Console disabled.\n"; } } else if(con.init(false)) - cerr << "Console is running.\n"; + std::cerr << "Console is running.\n"; else - cerr << "Console has failed to initialize!\n"; + std::cerr << "Console has failed to initialize!\n"; /* // dump offsets to a file std::ofstream dump("offsets.log"); @@ -1642,19 +1639,19 @@ bool Core::Init() return false; } - cerr << "Binding to SDL.\n"; + std::cerr << "Binding to SDL.\n"; if (!DFSDL::init(con)) { fatal("cannot bind SDL libraries"); return false; } - cerr << "Initializing textures.\n"; + std::cerr << "Initializing textures.\n"; Textures::init(con); // create mutex for syncing with interactive tasks - cerr << "Initializing plugins.\n"; + std::cerr << "Initializing plugins.\n"; // create plugin manager plug_mgr = new PluginManager(this); plug_mgr->init(); - cerr << "Starting the TCP listener.\n"; + std::cerr << "Starting the TCP listener.\n"; auto listen = ServerMain::listen(RemoteClient::GetDefaultPort()); IODATA *temp = new IODATA; temp->core = this; @@ -1662,7 +1659,7 @@ bool Core::Init() if (!is_text_mode || is_headless) { - cerr << "Starting IO thread.\n"; + std::cerr << "Starting IO thread.\n"; // create IO thread d->iothread = std::thread{fIOthread, (void*)temp}; } @@ -1672,19 +1669,19 @@ bool Core::Init() d->iothread = std::thread{fInitthread, (void*)temp}; } - cerr << "Starting DF input capture thread.\n"; + std::cerr << "Starting DF input capture thread.\n"; // set up hotkey capture d->hotkeythread = std::thread(fHKthread, (void *) temp); started = true; modstate = 0; if (!listen.get()) - cerr << "TCP listen failed.\n"; + std::cerr << "TCP listen failed.\n"; if (df::global::game) { - vector args; - const string & raw = df::global::game->command_line.original; + std::vector args; + const std::string & raw = df::global::game->command_line.original; size_t offset = 0; while (offset < raw.size()) { @@ -1698,7 +1695,7 @@ bool Core::Init() else { size_t next = raw.find(" ", offset); - if (next == string::npos) + if (next == std::string::npos) { args.push_back(raw.substr(offset)); offset = raw.size(); @@ -1712,12 +1709,12 @@ bool Core::Init() } for (auto it = args.begin(); it != args.end(); ) { - const string & first = *it; + const std::string & first = *it; if (first.length() > 0 && first[0] == '+') { - vector cmd; + std::vector cmd; for (it++; it != args.end(); it++) { - const string & arg = *it; + const std::string & arg = *it; if (arg.length() > 0 && arg[0] == '+') { break; @@ -1727,12 +1724,12 @@ bool Core::Init() if (runCommand(con, first.substr(1), cmd) != CR_OK) { - cerr << "Error running command: " << first.substr(1); + std::cerr << "Error running command: " << first.substr(1); for (auto it2 = cmd.begin(); it2 != cmd.end(); it2++) { - cerr << " \"" << *it2 << "\""; + std::cerr << " \"" << *it2 << "\""; } - cerr << "\n"; + std::cerr << "\n"; } } else @@ -1742,7 +1739,7 @@ bool Core::Init() } } - cerr << "DFHack is running.\n"; + std::cerr << "DFHack is running.\n"; onStateChange(con, SC_CORE_INITIALIZED); @@ -1761,7 +1758,7 @@ bool Core::setHotkeyCmd( std::string cmd ) /// removes the hotkey command and gives it to the caller thread std::string Core::getHotkeyCmd( bool &keep_going ) { - string returner; + std::string returner; std::unique_lock lock(HotkeyMutex); HotkeyCond.wait(lock, [this]() -> bool {return this->hotkey_set;}); if (hotkey_set == SHUTDOWN) { @@ -1986,22 +1983,22 @@ void getFilesWithPrefixAndSuffix(const std::string& folder, const std::string& p return; } -size_t loadScriptFiles(Core* core, color_ostream& out, const vector& prefix, const std::string& folder) { - static const string suffix = ".init"; - vector scriptFiles; +size_t loadScriptFiles(Core* core, color_ostream& out, const std::vector& prefix, const std::string& folder) { + static const std::string suffix = ".init"; + std::vector scriptFiles; for ( size_t a = 0; a < prefix.size(); a++ ) { getFilesWithPrefixAndSuffix(folder, prefix[a], ".init", scriptFiles); } std::sort(scriptFiles.begin(), scriptFiles.end(), - [&](const string &a, const string &b) { - string a_base = a.substr(0, a.size() - suffix.size()); - string b_base = b.substr(0, b.size() - suffix.size()); + [&](const std::string &a, const std::string &b) { + std::string a_base = a.substr(0, a.size() - suffix.size()); + std::string b_base = b.substr(0, b.size() - suffix.size()); return a_base < b_base; }); size_t result = 0; for ( size_t a = 0; a < scriptFiles.size(); a++ ) { result++; - string path = ""; + std::string path = ""; if (folder != ".") path = folder + "/"; core->loadScriptFile(out, path + scriptFiles[a], false); @@ -2012,10 +2009,10 @@ size_t loadScriptFiles(Core* core, color_ostream& out, const vector namespace DFHack { namespace X { typedef state_change_event Key; - typedef vector Val; - typedef pair Entry; - typedef vector EntryVector; - typedef map InitVariationTable; + typedef std::vector Val; + typedef std::pair Entry; + typedef std::vector EntryVector; + typedef std::map InitVariationTable; EntryVector computeInitVariationTable(void* none, ...) { va_list list; @@ -2030,7 +2027,7 @@ namespace DFHack { const char *v = va_arg(list, const char *); if (!v || !v[0]) break; - val.push_back(string(v)); + val.emplace_back(v); } result.push_back(Entry(key,val)); } @@ -2647,8 +2644,8 @@ bool Core::RunAlias(color_ostream &out, const std::string &name, return false; } - const string &first = aliases[name][0]; - vector parts(aliases[name].begin() + 1, aliases[name].end()); + const std::string &first = aliases[name][0]; + std::vector parts(aliases[name].begin() + 1, aliases[name].end()); parts.insert(parts.end(), parameters.begin(), parameters.end()); result = runCommand(out, first, parts); return true; diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 2dc2ddf8b..c9bdc3021 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -55,6 +55,7 @@ distribution. #include "modules/MapCache.h" #include "modules/Maps.h" #include "modules/Materials.h" +#include "modules/Military.h" #include "modules/Random.h" #include "modules/Screen.h" #include "modules/Textures.h" @@ -1659,6 +1660,7 @@ static bool jobItemEqual(const df::job_item *job1, const df::job_item *job2) } static const LuaWrapper::FunctionReg dfhack_job_module[] = { + WRAPM(Job,attachJobItem), WRAPM(Job,cloneJobStruct), WRAPM(Job,printItemDetails), WRAPM(Job,printJobDetails), @@ -1811,7 +1813,6 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getGoalType), WRAPM(Units, getGoalName), WRAPM(Units, isGoalAchieved), - WRAPM(Units, getSquadName), WRAPM(Units, getPhysicalDescription), WRAPM(Units, getRaceName), WRAPM(Units, getRaceNamePlural), @@ -1936,6 +1937,15 @@ static const luaL_Reg dfhack_units_funcs[] = { { NULL, NULL } }; +/***** Military Module *****/ + +static const LuaWrapper::FunctionReg dfhack_military_module[] = { + WRAPM(Military, makeSquad), + WRAPM(Military, updateRoomAssignments), + WRAPM(Military, getSquadName), + { NULL, NULL } +}; + /***** Items module *****/ static bool items_moveToGround(df::item *item, df::coord pos) @@ -3446,6 +3456,7 @@ void OpenDFHackApi(lua_State *state) OpenModule(state, "job", dfhack_job_module, dfhack_job_funcs); OpenModule(state, "textures", dfhack_textures_module); OpenModule(state, "units", dfhack_units_module, dfhack_units_funcs); + OpenModule(state, "military", dfhack_military_module); OpenModule(state, "items", dfhack_items_module, dfhack_items_funcs); OpenModule(state, "maps", dfhack_maps_module, dfhack_maps_funcs); OpenModule(state, "world", dfhack_world_module, dfhack_world_funcs); diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index c9a5f66d6..d14bdb6e9 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -404,6 +404,22 @@ DFHACK_EXPORT bool split_string(std::vector *out, bool squash_empty = false); DFHACK_EXPORT std::string join_strings(const std::string &separator, const std::vector &items); +template +inline std::string join_strings(const std::string &separator, T &items) { + std::stringstream ss; + + bool first = true; + for (auto &item : items) { + if (first) + first = false; + else + ss << separator; + ss << item; + } + + return ss.str(); +} + DFHACK_EXPORT std::string toUpper(const std::string &str); DFHACK_EXPORT std::string toLower(const std::string &str); DFHACK_EXPORT std::string to_search_normalized(const std::string &str); diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index b5fd119f7..9f07ea3db 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -8,6 +8,7 @@ namespace DFHack // SDL stand-in type definitions typedef signed short SINT16; typedef void DFSDL_sem; + typedef void DFSDL_Event; typedef struct { @@ -80,12 +81,15 @@ void cleanup(); DFHACK_EXPORT DFSDL_Surface * DFIMG_Load(const char *file); DFHACK_EXPORT int DFSDL_SetAlpha(DFSDL_Surface *surface, uint32_t flag, uint8_t alpha); +DFHACK_EXPORT DFSDL_Surface * DFSDL_GetVideoSurface(void); DFHACK_EXPORT DFSDL_Surface * DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); +DFHACK_EXPORT DFSDL_Surface * DFSDL_CreateRGBSurfaceFrom(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask); DFHACK_EXPORT int DFSDL_UpperBlit(DFSDL_Surface *src, const DFSDL_Rect *srcrect, DFSDL_Surface *dst, DFSDL_Rect *dstrect); DFHACK_EXPORT DFSDL_Surface * DFSDL_ConvertSurface(DFSDL_Surface *src, const DFSDL_PixelFormat *fmt, uint32_t flags); DFHACK_EXPORT void DFSDL_FreeSurface(DFSDL_Surface *surface); DFHACK_EXPORT int DFSDL_SemWait(DFSDL_sem *sem); DFHACK_EXPORT int DFSDL_SemPost(DFSDL_sem *sem); +DFHACK_EXPORT int DFSDL_PushEvent(DFSDL_Event *event); } diff --git a/library/include/modules/Military.h b/library/include/modules/Military.h new file mode 100644 index 000000000..8ceb987b5 --- /dev/null +++ b/library/include/modules/Military.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Export.h" +#include "DataDefs.h" + +#include "df/squad.h" + +namespace DFHack +{ +namespace Military +{ + +DFHACK_EXPORT std::string getSquadName(int32_t squad_id); +DFHACK_EXPORT df::squad* makeSquad(int32_t assignment_id); +DFHACK_EXPORT void updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags); + +} +} diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 0edebacdc..4fd9246aa 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -222,8 +222,6 @@ DFHACK_EXPORT df::goal_type getGoalType(df::unit *unit, size_t goalIndex = 0); DFHACK_EXPORT std::string getGoalName(df::unit *unit, size_t goalIndex = 0); DFHACK_EXPORT bool isGoalAchieved(df::unit *unit, size_t goalIndex = 0); -DFHACK_EXPORT std::string getSquadName(df::unit *unit); - DFHACK_EXPORT df::activity_entry *getMainSocialActivity(df::unit *unit); DFHACK_EXPORT df::activity_event *getMainSocialEvent(df::unit *unit); diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 4a30d947a..7791f3685 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -916,7 +916,8 @@ end WINDOW_FRAME = make_frame('Window', true) PANEL_FRAME = make_frame('Panel', false) MEDIUM_FRAME = make_frame('Medium', false) -THIN_FRAME = make_frame('Thin', false) +INTERIOR_FRAME = make_frame('Thin', false) +INTERIOR_FRAME.signature_pen = false -- for compatibility with pre-steam code GREY_LINE_FRAME = WINDOW_FRAME @@ -987,4 +988,10 @@ function FramedScreen:onRenderFrame(dc, rect) paint_frame(dc,rect,self.frame_style,self.frame_title) end +-- Inverts the brightness of the color, optionally taking a "bold" parameter, +-- which you should include if you're reading the fg color of a pen. +function invert_color(color, bold) + color = bold and (color + 8) or color + return (color + 8) % 16 +end return _ENV diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 77c6c2c28..b86e31710 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1080,7 +1080,26 @@ local function is_disabled(token) (token.enabled ~= nil and not getval(token.enabled)) end -function render_text(obj,dc,x0,y0,pen,dpen,disabled) +-- Make the hover pen -- that is a pen that should render elements that has the +-- mouse hovering over it. if hpen is specified, it just checks the fields and +-- returns it (in parsed pen form) +local function make_hpen(pen, hpen) + if not hpen then + pen = dfhack.pen.parse(pen) + + -- Swap the foreground and background + hpen = dfhack.pen.make(pen.bg, nil, pen.fg + (pen.bold and 8 or 0)) + end + + -- text_hpen needs a character in order to paint the background using + -- Painter:fill(), so let's make it paint a space to show the background + -- color + local hpen_parsed = dfhack.pen.parse(hpen) + hpen_parsed.ch = string.byte(' ') + return hpen_parsed +end + +function render_text(obj,dc,x0,y0,pen,dpen,disabled,hpen,hovered) local width = 0 for iline = dc and obj.start_line_num or 1, #obj.text_lines do local x, line = 0, obj.text_lines[iline] @@ -1105,8 +1124,8 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled) if token.tile then x = x + 1 if dc then - local tile_pen = tonumber(token.tile) and - to_pen{tile=token.tile} or token.tile + local tile = getval(token.tile) + local tile_pen = tonumber(tile) and to_pen{tile=tile} or tile dc:char(nil, tile_pen) if token.width then dc:advance(token.width-1) @@ -1120,16 +1139,25 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled) if dc then local tpen = getval(token.pen) + local dcpen = to_pen(tpen or pen) + + -- If disabled, figure out which dpen to use if disabled or is_disabled(token) then - dc:pen(getval(token.dpen) or tpen or dpen) + dcpen = to_pen(getval(token.dpen) or tpen or dpen) if keypen.fg ~= COLOR_BLACK then keypen.bold = false end - else - dc:pen(tpen or pen) + + -- if hovered *and* disabled, combine both effects + if hovered then + dcpen = make_hpen(dcpen) + end + elseif hovered then + dcpen = make_hpen(dcpen, getval(token.hpen) or hpen) end - end + dc:pen(dcpen) + end local width = getval(token.width) local padstr if width then @@ -1204,7 +1232,7 @@ Label = defclass(Label, Widget) Label.ATTRS{ text_pen = COLOR_WHITE, text_dpen = COLOR_DARKGREY, -- disabled - text_hpen = DEFAULT_NIL, -- highlight - default is text_pen with reversed brightness + text_hpen = DEFAULT_NIL, -- hover - default is to invert the fg/bg colors disabled = DEFAULT_NIL, enabled = DEFAULT_NIL, auto_height = true, @@ -1221,12 +1249,7 @@ function Label:init(args) self:addviews{self.scrollbar} - -- use existing saved text if no explicit text was specified. this avoids - -- overwriting pre-formatted text that subclasses may have already set self:setText(args.text or self.text) - if not self.text_hpen then - self.text_hpen = ((tonumber(self.text_pen) or tonumber(self.text_pen.fg) or 0) + 8) % 16 - end end local function update_label_scrollbar(label) @@ -1274,12 +1297,16 @@ function Label:getTextWidth() return self.text_width end +-- Overridden by subclasses that also want to add new mouse handlers, see +-- HotkeyLabel. +function Label:shouldHover() + return self.on_click or self.on_rclick +end + function Label:onRenderBody(dc) local text_pen = self.text_pen - if self:getMousePos() and (self.on_click or self.on_rclick) then - text_pen = self.text_hpen - end - render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self)) + local hovered = self:getMousePos() and self:shouldHover() + render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self), self.text_hpen, hovered) end function Label:on_scrollbar(scroll_spec) @@ -1432,6 +1459,11 @@ function HotkeyLabel:setLabel(label) self:initializeLabel() end +function HotkeyLabel:shouldHover() + -- When on_activate is set, text should also hover on mouseover + return self.on_activate or HotkeyLabel.super.shouldHover(self) +end + function HotkeyLabel:initializeLabel() self:setText{{key=self.key, key_sep=self.key_sep, text=self.label, on_activate=self.on_activate}} @@ -1440,7 +1472,8 @@ end function HotkeyLabel:onInput(keys) if HotkeyLabel.super.onInput(self, keys) then return true - elseif keys._MOUSE_L_DOWN and self:getMousePos() and self.on_activate then + elseif keys._MOUSE_L_DOWN and self:getMousePos() and self.on_activate + and not is_disabled(self) then self.on_activate() return true end @@ -1457,6 +1490,7 @@ CycleHotkeyLabel.ATTRS{ key_back=DEFAULT_NIL, label=DEFAULT_NIL, label_width=DEFAULT_NIL, + label_below=false, options=DEFAULT_NIL, initial_option=1, on_change=DEFAULT_NIL, @@ -1465,16 +1499,27 @@ CycleHotkeyLabel.ATTRS{ function CycleHotkeyLabel:init() self:setOption(self.initial_option) + local val_gap = 1 + if self.label_below then + val_gap = 0 + (self.key_back and 1 or 0) + (self.key and 3 or 0) + end + self:setText{ self.key_back ~= nil and {key=self.key_back, key_sep='', width=0, on_activate=self:callback('cycle', true)} or {}, {key=self.key, key_sep=': ', text=self.label, width=self.label_width, on_activate=self:callback('cycle')}, - ' ', - {text=self:callback('getOptionLabel'), + self.label_below and NEWLINE or '', + {gap=val_gap, text=self:callback('getOptionLabel'), pen=self:callback('getOptionPen')}, } end +-- CycleHotkeyLabels are always clickable and therefore should always change +-- color when hovered. +function CycleHotkeyLabel:shouldHover() + return true +end + function CycleHotkeyLabel:cycle(backwards) local old_option_idx = self.option_idx if self.option_idx == #self.options and not backwards then @@ -1541,7 +1586,7 @@ end function CycleHotkeyLabel:onInput(keys) if CycleHotkeyLabel.super.onInput(self, keys) then return true - elseif keys._MOUSE_L_DOWN and self:getMousePos() then + elseif keys._MOUSE_L_DOWN and self:getMousePos() and not is_disabled(self) then self:cycle() return true end @@ -1565,6 +1610,7 @@ List = defclass(List, Widget) List.ATTRS{ text_pen = COLOR_CYAN, + text_hpen = DEFAULT_NIL, -- hover color, defaults to inverting the FG/BG pens for each text object cursor_pen = COLOR_LIGHTCYAN, inactive_pen = DEFAULT_NIL, on_select = DEFAULT_NIL, @@ -1745,12 +1791,16 @@ function List:onRenderBody(dc) end end + local hoveridx = self:getIdxUnderMouse() for i = top,iend do local obj = choices[i] local current = (i == self.selected) - local cur_pen = self.cursor_pen - local cur_dpen = self.text_pen - local active_pen = current and cur_pen or cur_dpen + local hovered = (i == hoveridx) + -- cur_pen and cur_dpen can't be integers or background colors get + -- messed up in render_text for subsequent renders + local cur_pen = to_pen(self.cursor_pen) + local cur_dpen = to_pen(self.text_pen) + local active_pen = (current and cur_pen or cur_dpen) if not getval(self.active) then cur_pen = self.inactive_pen or self.cursor_pen @@ -1764,7 +1814,7 @@ function List:onRenderBody(dc) paint_icon(icon, obj) end - render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current) + render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current, self.text_hpen, hovered) local ip = dc.width @@ -1882,7 +1932,7 @@ end FilteredList = defclass(FilteredList, Widget) FilteredList.ATTRS { - case_sensitive = true, + case_sensitive = false, edit_below = false, edit_key = DEFAULT_NIL, edit_ignore_keys = DEFAULT_NIL, diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index a7847fdb5..6a3e6af2f 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -28,12 +28,15 @@ static const std::vector SDL_IMAGE_LIBS { DFSDL_Surface * (*g_IMG_Load)(const char *) = nullptr; int (*g_SDL_SetAlpha)(DFSDL_Surface *, uint32_t, uint8_t) = nullptr; -DFSDL_Surface * (*g_SDL_CreateRGBSurface)(uint32_t, int, int, int, uint32_t, uint32_t, uint32_t, uint32_t); -int (*g_SDL_UpperBlit)(DFSDL_Surface *, const DFSDL_Rect *, DFSDL_Surface *, DFSDL_Rect *); -DFSDL_Surface * (*g_SDL_ConvertSurface)(DFSDL_Surface *, const DFSDL_PixelFormat *, uint32_t); -void (*g_SDL_FreeSurface)(DFSDL_Surface *); -int (*g_SDL_SemWait)(DFSDL_sem *); -int (*g_SDL_SemPost)(DFSDL_sem *); +DFSDL_Surface * (*g_SDL_GetVideoSurface)(void) = nullptr; +DFSDL_Surface * (*g_SDL_CreateRGBSurface)(uint32_t, int, int, int, uint32_t, uint32_t, uint32_t, uint32_t) = nullptr; +DFSDL_Surface * (*g_SDL_CreateRGBSurfaceFrom)(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) = nullptr; +int (*g_SDL_UpperBlit)(DFSDL_Surface *, const DFSDL_Rect *, DFSDL_Surface *, DFSDL_Rect *) = nullptr; +DFSDL_Surface * (*g_SDL_ConvertSurface)(DFSDL_Surface *, const DFSDL_PixelFormat *, uint32_t) = nullptr; +void (*g_SDL_FreeSurface)(DFSDL_Surface *) = nullptr; +int (*g_SDL_SemWait)(DFSDL_sem *) = nullptr; +int (*g_SDL_SemPost)(DFSDL_sem *) = nullptr; +int (*g_SDL_PushEvent)(DFSDL_Event *) = nullptr; bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { @@ -63,12 +66,15 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_image_handle, IMG_Load); bind(g_sdl_handle, SDL_SetAlpha); + bind(g_sdl_handle, SDL_GetVideoSurface); bind(g_sdl_handle, SDL_CreateRGBSurface); + bind(g_sdl_handle, SDL_CreateRGBSurfaceFrom); bind(g_sdl_handle, SDL_UpperBlit); bind(g_sdl_handle, SDL_ConvertSurface); bind(g_sdl_handle, SDL_FreeSurface); bind(g_sdl_handle, SDL_SemWait); bind(g_sdl_handle, SDL_SemPost); + bind(g_sdl_handle, SDL_PushEvent); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -95,10 +101,18 @@ int DFSDL::DFSDL_SetAlpha(DFSDL_Surface *surface, uint32_t flag, uint8_t alpha) return g_SDL_SetAlpha(surface, flag, alpha); } +DFSDL_Surface * DFSDL::DFSDL_GetVideoSurface(void) { + return g_SDL_GetVideoSurface(); +} + DFSDL_Surface * DFSDL::DFSDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) { return g_SDL_CreateRGBSurface(flags, width, height, depth, Rmask, Gmask, Bmask, Amask); } +DFSDL_Surface * DFSDL::DFSDL_CreateRGBSurfaceFrom(void *pixels, int width, int height, int depth, int pitch, uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) { + return g_SDL_CreateRGBSurfaceFrom(pixels, width, height, depth, pitch, Rmask, Gmask, Bmask, Amask); +} + int DFSDL::DFSDL_UpperBlit(DFSDL_Surface *src, const DFSDL_Rect *srcrect, DFSDL_Surface *dst, DFSDL_Rect *dstrect) { return g_SDL_UpperBlit(src, srcrect, dst, dstrect); } @@ -118,3 +132,7 @@ int DFSDL::DFSDL_SemWait(DFSDL_sem *sem) { int DFSDL::DFSDL_SemPost(DFSDL_sem *sem) { return g_SDL_SemPost(sem); } + +int DFSDL::DFSDL_PushEvent(DFSDL_Event *event) { + return g_SDL_PushEvent(event); +} diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index e3a7ba983..6ea0a5ff0 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -491,7 +491,12 @@ bool Gui::matchFocusString(std::string focus_string, df::viewscreen *top) { static void push_dfhack_focus_string(dfhack_viewscreen *vs, std::vector &focusStrings) { auto name = vs->getFocusString(); - focusStrings.push_back(name.empty() ? "dfhack" : "dfhack/" + name); + if (name.empty()) + name = "dfhack"; + else if (string::npos == name.find("dfhack/")) + name = "dfhack/" + name; + + focusStrings.push_back(name); } std::vector Gui::getFocusStrings(df::viewscreen* top) diff --git a/library/modules/Items.cpp b/library/modules/Items.cpp index 11d170725..bfdd525be 100644 --- a/library/modules/Items.cpp +++ b/library/modules/Items.cpp @@ -1647,5 +1647,5 @@ bool Items::isSquadEquipment(df::item *item) return false; auto &vec = plotinfo->equipment.items_assigned[item->getType()]; - return binsearch_index(vec, &df::item::id, item->id) >= 0; + return binsearch_index(vec, item->id) >= 0; } diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index 0854a85ce..a6141f1d8 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -513,8 +513,14 @@ void MaterialInfo::getMatchBits(df::job_item_flags2 &ok, df::job_item_flags2 &ma TEST(sewn_imageless, is_cloth); TEST(glass_making, MAT_FLAG(CRYSTAL_GLASSABLE)); - TEST(fire_safe, material->heat.melting_point > 11000); - TEST(magma_safe, material->heat.melting_point > 12000); + TEST(fire_safe, material->heat.melting_point > 11000 + && material->heat.boiling_point > 11000 + && material->heat.ignite_point > 11000 + && material->heat.heatdam_point > 11000); + TEST(magma_safe, material->heat.melting_point > 12000 + && material->heat.boiling_point > 12000 + && material->heat.ignite_point > 12000 + && material->heat.heatdam_point > 12000); TEST(deep_material, FLAG(inorganic, inorganic_flags::SPECIAL)); TEST(non_economic, !inorganic || !(plotinfo && vector_get(plotinfo->economic_stone, index))); diff --git a/library/modules/Military.cpp b/library/modules/Military.cpp new file mode 100644 index 000000000..b402cc0fa --- /dev/null +++ b/library/modules/Military.cpp @@ -0,0 +1,291 @@ +#include +#include +#include +#include "MiscUtils.h" +#include "modules/Military.h" +#include "modules/Translation.h" +#include "df/building.h" +#include "df/building_civzonest.h" +#include "df/historical_figure.h" +#include "df/historical_entity.h" +#include "df/entity_position.h" +#include "df/entity_position_assignment.h" +#include "df/plotinfost.h" +#include "df/squad.h" +#include "df/squad_position.h" +#include "df/squad_schedule_order.h" +#include "df/squad_order.h" +#include "df/squad_order_trainst.h" +#include "df/world.h" + +using namespace DFHack; +using namespace df::enums; +using df::global::world; +using df::global::plotinfo; + +std::string Military::getSquadName(int32_t squad_id) +{ + df::squad *squad = df::squad::find(squad_id); + if (!squad) + return ""; + if (squad->alias.size() > 0) + return squad->alias; + return Translation::TranslateName(&squad->name, true); +} + +//only works for making squads for fort mode player controlled dwarf squads +//could be extended straightforwardly by passing in entity +df::squad* Military::makeSquad(int32_t assignment_id) +{ + if (df::global::squad_next_id == nullptr || df::global::plotinfo == nullptr) + return nullptr; + + df::language_name name; + name.type = df::language_name_type::Squad; + + for (int i=0; i < 7; i++) + { + name.words[i] = -1; + name.parts_of_speech[i] = df::part_of_speech::Noun; + } + + df::historical_entity* fort = df::historical_entity::find(df::global::plotinfo->group_id); + + if (fort == nullptr) + return nullptr; + + df::entity_position_assignment* found_assignment = nullptr; + + for (auto* assignment : fort->positions.assignments) + { + if (assignment->id == assignment_id) + { + found_assignment = assignment; + break; + } + } + + if (found_assignment == nullptr) + return nullptr; + + //this function does not attempt to delete or replace squads for assignments + if (found_assignment->squad_id != -1) + return nullptr; + + df::entity_position* corresponding_position = nullptr; + + for (auto* position : fort->positions.own) + { + if (position->id == found_assignment->position_id) + { + corresponding_position = position; + break; + } + } + + if (corresponding_position == nullptr) + return nullptr; + + df::squad* result = new df::squad(); + result->id = *df::global::squad_next_id; + result->uniform_priority = result->id + 1; //no idea why, but seems to hold + result->carry_food = 2; + result->carry_water = 1; + result->entity_id = df::global::plotinfo->group_id; + result->leader_position = corresponding_position->id; + result->leader_assignment = found_assignment->id; + result->name = name; + + int16_t squad_size = corresponding_position->squad_size; + + for (int i=0; i < squad_size; i++) + { + //construct for squad_position seems to set all the attributes correctly + df::squad_position* pos = new df::squad_position(); + + result->positions.push_back(pos); + } + + const auto& routines = df::global::plotinfo->alerts.routines; + + for (const auto& routine : routines) + { + df::squad_schedule_entry* asched = (df::squad_schedule_entry*)malloc(sizeof(df::squad_schedule_entry) * 12); + + for(int kk=0; kk < 12; kk++) + { + new (&asched[kk]) df::squad_schedule_entry; + + for(int jj=0; jj < squad_size; jj++) + { + int32_t* order_assignments = new int32_t(); + *order_assignments = -1; + + asched[kk].order_assignments.push_back(order_assignments); + } + } + + auto insert_training_order = [asched, squad_size](int month) + { + df::squad_schedule_order* order = new df::squad_schedule_order(); + order->min_count = squad_size; + //assumed + order->positions.resize(squad_size); + + df::squad_order* s_order = df::allocate(); + + s_order->year = *df::global::cur_year; + s_order->year_tick = *df::global::cur_year_tick; + + order->order = s_order; + + asched[month].orders.push_back(order); + //wear uniform while training + asched[month].uniform_mode = 0; + }; + + //Dwarf fortress does do this via a series of string comparisons + //Off duty: No orders, Sleep/room at will. Equip/orders only + if (routine->name == "Off duty") + { + for (int i=0; i < 12; i++) + { + asched[i].sleep_mode = 0; + asched[i].uniform_mode = 1; + } + } + //Staggered Training: Training orders at months 3 4 5 9 10 11, *or* 0 1 2 6 7 8, sleep/room at will. Equip/orders only, except train months which are equip/always + else if (routine->name == "Staggered training") + { + //this is semi randomised for different squads + //appears to be something like squad.id & 1, it isn't smart + //if you alternate squad creation, its 'correctly' staggered + //but it'll also happily not stagger them if you eg delete a squad and make another + std::array indices; + + if ((*df::global::squad_next_id) & 1) + { + indices = {3, 4, 5, 9, 10, 11}; + } + else + { + indices = {0, 1, 2, 6, 7, 8}; + } + + for (int index : indices) + { + insert_training_order(index); + //still sleep in room at will even when training + asched[index].sleep_mode = 0; + } + } + //see above, but with all indices + else if (routine->name == "Constant training") + { + for (int i=0; i < 12; i++) + { + insert_training_order(i); + //still sleep in room at will even when training + asched[i].sleep_mode = 0; + } + } + else if (routine->name == "Ready") + { + for (int i=0; i < 12; i++) + { + asched[i].sleep_mode = 2; + asched[i].uniform_mode = 0; + } + } + else + { + for (int i=0; i < 12; i++) + { + asched[i].sleep_mode = 0; + asched[i].uniform_mode = 0; + } + } + + result->schedule.push_back(reinterpret_cast(asched)); + } + + //Modify necessary world state + (*df::global::squad_next_id)++; + fort->squads.push_back(result->id); + df::global::world->squads.all.push_back(result); + found_assignment->squad_id = result->id; + + return result; +} + +void Military::updateRoomAssignments(int32_t squad_id, int32_t civzone_id, df::squad_use_flags flags) +{ + df::squad* squad = df::squad::find(squad_id); + df::building* bzone = df::building::find(civzone_id); + + df::building_civzonest* zone = strict_virtual_cast(bzone); + + if (squad == nullptr || zone == nullptr) + return; + + df::squad::T_rooms* room_from_squad = nullptr; + df::building_civzonest::T_squad_room_info* room_from_building = nullptr; + + for (auto room : squad->rooms) + { + if (room->building_id == civzone_id) + { + room_from_squad = room; + break; + } + } + + for (auto room : zone->squad_room_info) + { + if (room->squad_id == squad_id) + { + room_from_building = room; + break; + } + } + + if (flags.whole == 0 && room_from_squad == nullptr && room_from_building == nullptr) + return; + + //if we're setting 0 flags, and there's no room already, don't set a room + bool avoiding_squad_roundtrip = flags.whole == 0 && room_from_squad == nullptr; + + if (!avoiding_squad_roundtrip && room_from_squad == nullptr) + { + room_from_squad = new df::squad::T_rooms(); + room_from_squad->building_id = civzone_id; + + insert_into_vector(squad->rooms, &df::squad::T_rooms::building_id, room_from_squad); + } + + if (room_from_building == nullptr) + { + room_from_building = new df::building_civzonest::T_squad_room_info(); + room_from_building->squad_id = squad_id; + + insert_into_vector(zone->squad_room_info, &df::building_civzonest::T_squad_room_info::squad_id, room_from_building); + } + + if (room_from_squad) + room_from_squad->mode = flags; + + room_from_building->mode = flags; + + if (flags.whole == 0 && !avoiding_squad_roundtrip) + { + for (int i=0; i < (int)squad->rooms.size(); i++) + { + if (squad->rooms[i]->building_id == civzone_id) + { + delete squad->rooms[i]; + squad->rooms.erase(squad->rooms.begin() + i); + i--; + } + } + } +} diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index a78a36a3f..dcb18dd91 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -130,8 +130,7 @@ static bool doSetTile_map(const Pen &pen, int x, int y) { long texpos = pen.tile; if (!texpos && pen.ch) texpos = init->font.large_font_texpos[(uint8_t)pen.ch]; - if (texpos) - vp->screentexpos_interface[index] = texpos; + vp->screentexpos_interface[index] = texpos; return true; } @@ -877,7 +876,7 @@ void dfhack_lua_viewscreen::update_focus(lua_State *L, int idx) if (focus.empty()) focus = "lua"; - else + else if (string::npos == focus.find("lua/")) focus = "lua/"+focus; } diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 9a415fb34..533b40ca8 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -71,7 +71,6 @@ using namespace std; #include "df/identity.h" #include "df/job.h" #include "df/nemesis_record.h" -#include "df/squad.h" #include "df/tile_occupancy.h" #include "df/plotinfost.h" #include "df/unit_inventory_item.h" @@ -589,19 +588,7 @@ bool Units::isMischievous(df::unit *unit) bool Units::isAvailableForAdoption(df::unit* unit) { CHECK_NULL_POINTER(unit); - auto refs = unit->specific_refs; - for(size_t i=0; itype; - if( reftype == df::specific_ref_type::PETINFO_PET ) - { - //df::pet_info* pet = ref->pet; - return true; - } - } - - return false; + return unit->flags3.bits.available_for_adoption; } bool Units::isPet(df::unit* unit) @@ -1959,19 +1946,6 @@ bool Units::isGoalAchieved(df::unit *unit, size_t goalIndex) && unit->status.current_soul->personality.dreams[goalIndex]->flags.whole != 0; } -std::string Units::getSquadName(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - if (unit->military.squad_id == -1) - return ""; - df::squad *squad = df::squad::find(unit->military.squad_id); - if (!squad) - return ""; - if (squad->alias.size() > 0) - return squad->alias; - return Translation::TranslateName(&squad->name, true); -} - df::activity_entry *Units::getMainSocialActivity(df::unit *unit) { CHECK_NULL_POINTER(unit); diff --git a/library/xml b/library/xml index 7917f062c..8ae81f8d8 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7917f062c403a47d4d190bafc2470b247c8aa642 +Subproject commit 8ae81f8d8f1f96d82b9074b205073bb8e8d29f96 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 7940c3994..82722f16a 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -89,7 +89,7 @@ dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) #dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua) #dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) -dfhack_plugin(buildingplan buildingplan.cpp LINK_LIBRARIES lua) +add_subdirectory(buildingplan) #dfhack_plugin(changeitem changeitem.cpp) dfhack_plugin(changelayer changelayer.cpp) dfhack_plugin(changevein changevein.cpp) diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index bee3a4503..536c74f0f 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -19,8 +19,6 @@ #include "LuaTools.h" #include "PluginManager.h" -#include "modules/Gui.h" -#include "modules/Maps.h" #include "modules/Persistence.h" #include "modules/Units.h" #include "modules/World.h" @@ -805,8 +803,8 @@ static void autobutcher_cycle(color_ostream &out) { w->UpdateConfig(out); watched_races.emplace(unit->race, w); - string announce = "New race added to autobutcher watchlist: " + Units::getRaceNamePluralById(unit->race); - Gui::showAnnouncement(announce, 2, false); + INFO(cycle,out).print("New race added to autobutcher watchlist: %s\n", + Units::getRaceNamePluralById(unit->race).c_str()); } if (w->isWatched) { @@ -828,9 +826,8 @@ static void autobutcher_cycle(color_ostream &out) { if (slaughter_count) { std::stringstream ss; ss << slaughter_count; - string announce = Units::getRaceNamePluralById(w.first) + " marked for slaughter: " + ss.str(); - DEBUG(cycle,out).print("%s\n", announce.c_str()); - Gui::showAnnouncement(announce, 2, false); + INFO(cycle,out).print("%s marked for slaughter: %s\n", + Units::getRaceNamePluralById(w.first).c_str(), ss.str().c_str()); } } } @@ -954,10 +951,8 @@ static void autobutcher_setWatchListRace(color_ostream &out, unsigned id, unsign WatchedRace * w = new WatchedRace(out, id, watched, fk, mk, fa, ma); w->UpdateConfig(out); watched_races.emplace(id, w); - - string announce; - announce = "New race added to autobutcher watchlist: " + Units::getRaceNamePluralById(id); - Gui::showAnnouncement(announce, 2, false); + INFO(status,out).print("New race added to autobutcher watchlist: %s\n", + Units::getRaceNamePluralById(id).c_str()); } // remove entry from watchlist diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index 86fa9114b..53a01a6a6 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -841,7 +841,7 @@ DFhackCExport command_result plugin_onupdate ( color_ostream &out ) if (p1 || p2) { dwarf_info[dwarf].diplomacy = true; - INFO(cycle, out).print("Dwarf %i \"%s\" has a meeting, will be cleared of all labors\n", + DEBUG(cycle, out).print("Dwarf %i \"%s\" has a meeting, will be cleared of all labors\n", dwarf, dwarfs[dwarf]->name.first_name.c_str()); break; } diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp deleted file mode 100644 index 039c83b0f..000000000 --- a/plugins/buildingplan.cpp +++ /dev/null @@ -1,689 +0,0 @@ -#include "Core.h" -#include "Debug.h" -#include "LuaTools.h" -#include "PluginManager.h" - -#include "modules/Items.h" -#include "modules/Job.h" -#include "modules/Materials.h" -#include "modules/Persistence.h" -#include "modules/World.h" - -#include "df/building.h" -#include "df/building_design.h" -#include "df/item.h" -#include "df/job_item.h" -#include "df/world.h" - -#include -#include -#include -#include - -using std::map; -using std::pair; -using std::queue; -using std::string; -using std::unordered_map; -using std::vector; - -using namespace DFHack; - -DFHACK_PLUGIN("buildingplan"); -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -REQUIRE_GLOBAL(world); - -// logging levels can be dynamically controlled with the `debugfilter` command. -namespace DFHack { - // for configuration-related logging - DBG_DECLARE(buildingplan, status, DebugCategory::LINFO); - // for logging during the periodic scan - DBG_DECLARE(buildingplan, cycle, DebugCategory::LINFO); -} - -static const string CONFIG_KEY = string(plugin_name) + "/config"; -static const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; - -enum ConfigValues { - CONFIG_BLOCKS = 1, - CONFIG_BOULDERS = 2, - CONFIG_LOGS = 3, - CONFIG_BARS = 4, -}; - -enum BuildingConfigValues { - BLD_CONFIG_ID = 0, -}; - -static int get_config_val(PersistentDataItem &c, int index) { - if (!c.isValid()) - return -1; - return c.ival(index); -} -static bool get_config_bool(PersistentDataItem &c, int index) { - return get_config_val(c, index) == 1; -} -static void set_config_val(PersistentDataItem &c, int index, int value) { - if (c.isValid()) - c.ival(index) = value; -} -static void set_config_bool(PersistentDataItem &c, int index, bool value) { - set_config_val(c, index, value ? 1 : 0); -} - -class PlannedBuilding { -public: - const df::building::key_field_type id; - - PlannedBuilding(color_ostream &out, df::building *building) : id(building->id) { - DEBUG(status,out).print("creating persistent data for building %d\n", id); - bld_config = DFHack::World::AddPersistentData(BLD_CONFIG_KEY); - set_config_val(bld_config, BLD_CONFIG_ID, id); - } - - PlannedBuilding(DFHack::PersistentDataItem &bld_config) - : id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { } - - void remove(color_ostream &out); - - // Ensure the building still exists and is in a valid state. It can disappear - // for lots of reasons, such as running the game with the buildingplan plugin - // disabled, manually removing the building, modifying it via the API, etc. - df::building * getBuildingIfValidOrRemoveIfNot(color_ostream &out) { - auto bld = df::building::find(id); - bool valid = bld && bld->getBuildStage() == 0; - if (!valid) { - remove(out); - return NULL; - } - return bld; - } - -private: - DFHack::PersistentDataItem bld_config; -}; - -static PersistentDataItem config; -// building id -> PlannedBuilding -unordered_map planned_buildings; -// vector id -> filter bucket -> queue of (building id, job_item index) -map>>> tasks; - -// note that this just removes the PlannedBuilding. the tasks will get dropped -// as we discover them in the tasks queues and they fail to be found in planned_buildings. -// this "lazy" task cleaning algorithm works because there is no way to -// re-register a building once it has been removed -- if it has been booted out of -// planned_buildings, then it has either been built or desroyed. therefore there is -// no chance of duplicate tasks getting added to the tasks queues. -void PlannedBuilding::remove(color_ostream &out) { - DEBUG(status,out).print("removing persistent data for building %d\n", id); - DFHack::World::DeletePersistentData(config); - if (planned_buildings.count(id) > 0) - planned_buildings.erase(id); -} - -static const int32_t CYCLE_TICKS = 600; // twice per game day -static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - -static command_result do_command(color_ostream &out, vector ¶meters); -static void do_cycle(color_ostream &out); -static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb); - -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(status,out).print("initializing %s\n", plugin_name); - - // provide a configuration interface for the plugin - commands.push_back(PluginCommand( - plugin_name, - "Plan building placement before you have materials.", - do_command)); - - return CR_OK; -} - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - if (enable != is_enabled) { - is_enabled = enable; - DEBUG(status,out).print("%s from the API; persisting\n", - is_enabled ? "enabled" : "disabled"); - } else { - DEBUG(status,out).print("%s from the API, but already %s; no action\n", - is_enabled ? "enabled" : "disabled", - is_enabled ? "enabled" : "disabled"); - } - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown (color_ostream &out) { - DEBUG(status,out).print("shutting down %s\n", plugin_name); - - return CR_OK; -} - -DFhackCExport command_result plugin_load_data (color_ostream &out) { - cycle_timestamp = 0; - config = World::GetPersistentData(CONFIG_KEY); - - if (!config.isValid()) { - DEBUG(status,out).print("no config found in this save; initializing\n"); - config = World::AddPersistentData(CONFIG_KEY); - set_config_bool(config, CONFIG_BLOCKS, true); - set_config_bool(config, CONFIG_BOULDERS, true); - set_config_bool(config, CONFIG_LOGS, true); - set_config_bool(config, CONFIG_BARS, false); - } - - DEBUG(status,out).print("loading persisted state\n"); - planned_buildings.clear(); - tasks.clear(); - vector building_configs; - World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); - const size_t num_building_configs = building_configs.size(); - for (size_t idx = 0; idx < num_building_configs; ++idx) { - PlannedBuilding pb(building_configs[idx]); - registerPlannedBuilding(out, pb); - } - - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - if (event == DFHack::SC_WORLD_UNLOADED) { - DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); - planned_buildings.clear(); - tasks.clear(); - } - return CR_OK; -} - -static bool cycle_requested = false; - -DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (!Core::getInstance().isWorldLoaded()) - return CR_OK; - - if (is_enabled && - (cycle_requested || world->frame_counter - cycle_timestamp >= CYCLE_TICKS)) - do_cycle(out); - return CR_OK; -} - -static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, - int nargs = 0, int nres = 0, - Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, - Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { - DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name); - - CoreSuspender guard; - - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - - if (!out) - out = &Core::getInstance().getConsole(); - - return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, - nargs, nres, - std::forward(args_lambda), - std::forward(res_lambda)); -} - -static command_result do_command(color_ostream &out, vector ¶meters) { - CoreSuspender suspend; - - if (!Core::getInstance().isWorldLoaded()) { - out.printerr("Cannot configure %s without a loaded world.\n", plugin_name); - return CR_FAILURE; - } - - bool show_help = false; - if (!call_buildingplan_lua(&out, "parse_commandline", parameters.size(), 1, - [&](lua_State *L) { - for (const string ¶m : parameters) - Lua::Push(L, param); - }, - [&](lua_State *L) { - show_help = !lua_toboolean(L, -1); - })) { - return CR_FAILURE; - } - - return show_help ? CR_WRONG_USAGE : CR_OK; -} - -///////////////////////////////////////////////////// -// cycle logic -// - -struct BadFlags { - uint32_t whole; - - BadFlags() { - df::item_flags flags; - #define F(x) 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); F(in_job); - F(owned); F(in_chest); F(removed); F(encased); - F(spider_web); - #undef F - whole = flags.whole; - } -}; - -static bool itemPassesScreen(df::item * item) { - static const BadFlags bad_flags; - return !(item->flags.whole & bad_flags.whole) - && !item->isAssignedToStockpile(); -} - -static bool matchesFilters(df::item * item, df::job_item * job_item) { - // check the properties that are not checked by Job::isSuitableItem() - if (job_item->item_type > -1 && job_item->item_type != item->getType()) - return false; - - if (job_item->item_subtype > -1 && - job_item->item_subtype != item->getSubtype()) - return false; - - if (job_item->flags2.bits.building_material && !item->isBuildMat()) - return false; - - if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) - return false; - - if (job_item->has_tool_use > df::tool_uses::NONE - && !item->hasToolUse(job_item->has_tool_use)) - return false; - - return DFHack::Job::isSuitableItem( - job_item, item->getType(), item->getSubtype()) - && DFHack::Job::isSuitableMaterial( - job_item, item->getMaterial(), item->getMaterialIndex(), - item->getType()); -} - -static bool isJobReady(color_ostream &out, df::job * job) { - int needed_items = 0; - for (auto job_item : job->job_items) { needed_items += job_item->quantity; } - if (needed_items) { - DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); - return false; - } - return true; -} - -static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { - // we want the items in the opposite order of the filters - return a->job_item_idx > b->job_item_idx; -} - -// this function does not remove the job_items since their quantity fields are -// now all at 0, so there is no risk of having extra items attached. we don't -// remove them to keep the "finalize with buildingplan active" path as similar -// as possible to the "finalize with buildingplan disabled" path. -static void finalizeBuilding(color_ostream &out, df::building * bld) { - DEBUG(cycle,out).print("finalizing building %d\n", bld->id); - auto job = bld->jobs[0]; - - // sort the items so they get added to the structure in the correct order - std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); - - // derive the material properties of the building and job from the first - // applicable item. if any boulders are involved, it makes the whole - // structure "rough". - bool rough = false; - for (auto attached_item : job->items) { - df::item *item = attached_item->item; - rough = rough || item->getType() == df::item_type::BOULDER; - if (bld->mat_type == -1) { - bld->mat_type = item->getMaterial(); - job->mat_type = bld->mat_type; - } - if (bld->mat_index == -1) { - bld->mat_index = item->getMaterialIndex(); - job->mat_index = bld->mat_index; - } - } - - if (bld->needsDesign()) { - auto act = (df::building_actual *)bld; - if (!act->design) - act->design = new df::building_design(); - act->design->flags.bits.rough = rough; - } - - // we're good to go! - job->flags.bits.suspend = false; - Job::checkBuildingsNow(); -} - -static df::building * popInvalidTasks(color_ostream &out, queue> & task_queue) { - while (!task_queue.empty()) { - auto & task = task_queue.front(); - auto id = task.first; - if (planned_buildings.count(id) > 0) { - auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out); - if (bld && bld->jobs[0]->job_items[task.second]->quantity) - return bld; - } - DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); - task_queue.pop(); - } - return NULL; -} - -static void doVector(color_ostream &out, df::job_item_vector_id vector_id, - map>> & buckets) { - auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); - auto item_vector = df::global::world->items.other[other_id]; - DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", - item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - buckets.size()); - for (auto item_it = item_vector.rbegin(); - item_it != item_vector.rend(); - ++item_it) { - auto item = *item_it; - if (!itemPassesScreen(item)) - continue; - for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { - auto & task_queue = bucket_it->second; - auto bld = popInvalidTasks(out, task_queue); - if (!bld) { - DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - bucket_it = buckets.erase(bucket_it); - continue; - } - auto & task = task_queue.front(); - auto id = task.first; - auto job = bld->jobs[0]; - auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx]) - && DFHack::Job::attachJobItem(job, item, - df::job_item_ref::Hauled, filter_idx)) - { - MaterialInfo material; - material.decode(item); - ItemTypeInfo item_type; - item_type.decode(item); - DEBUG(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n", - material.toString().c_str(), - item_type.toString().c_str(), - filter_idx, - ENUM_KEY_STR(building_type, bld->getType()).c_str(), - id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); - // keep quantity aligned with the actual number of remaining - // items so if buildingplan is turned off, the building will - // be completed with the correct number of items. - --job->job_items[filter_idx]->quantity; - task_queue.pop(); - if (isJobReady(out, job)) { - finalizeBuilding(out, bld); - planned_buildings.at(id).remove(out); - } - if (task_queue.empty()) { - DEBUG(cycle,out).print( - "removing empty item bucket: %s/%s; %zu left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - buckets.erase(bucket_it); - } - // we found a home for this item; no need to look further - break; - } - ++bucket_it; - } - if (buckets.empty()) - break; - } -} - -struct VectorsToScanLast { - std::vector vectors; - VectorsToScanLast() { - // order is important here. we want to match boulders before wood and - // everything before bars. blocks are not listed here since we'll have - // already scanned them when we did the first pass through the buckets. - vectors.push_back(df::job_item_vector_id::BOULDER); - vectors.push_back(df::job_item_vector_id::WOOD); - vectors.push_back(df::job_item_vector_id::BAR); - } -}; - -static void do_cycle(color_ostream &out) { - static const VectorsToScanLast vectors_to_scan_last; - - // mark that we have recently run - cycle_timestamp = world->frame_counter; - cycle_requested = false; - - DEBUG(cycle,out).print("running %s cycle for %zu registered buildings\n", - plugin_name, planned_buildings.size()); - - for (auto it = tasks.begin(); it != tasks.end(); ) { - auto vector_id = it->first; - // we could make this a set, but it's only three elements - if (std::find(vectors_to_scan_last.vectors.begin(), - vectors_to_scan_last.vectors.end(), - vector_id) != vectors_to_scan_last.vectors.end()) { - ++it; - continue; - } - - auto & buckets = it->second; - doVector(out, vector_id, buckets); - if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - it = tasks.erase(it); - } - else - ++it; - } - for (auto vector_id : vectors_to_scan_last.vectors) { - if (tasks.count(vector_id) == 0) - continue; - auto & buckets = tasks[vector_id]; - doVector(out, vector_id, buckets); - if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - tasks.erase(vector_id); - } - } - DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", - planned_buildings.size()); -} - -///////////////////////////////////////////////////// -// Lua API -// core will already be suspended when coming in through here -// - -static string getBucket(const df::job_item & ji) { - std::ostringstream ser; - - // pull out and serialize only known relevant fields. if we miss a few, then - // the filter bucket will be slighly less specific than it could be, but - // that's probably ok. we'll just end up bucketing slightly different items - // together. this is only a problem if the different filter at the front of - // the queue doesn't match any available items and blocks filters behind it - // that could be matched. - ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':' - << ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole - << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' - << ji.metal_ore << ':' << ji.has_tool_use; - - return ser.str(); -} - -// get a list of item vectors that we should search for matches -static vector getVectorIds(color_ostream &out, df::job_item *job_item) -{ - std::vector ret; - - // if the filter already has the vector_id set to something specific, use it - if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) - { - DEBUG(status,out).print("using vector_id from job_item: %s\n", - ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); - ret.push_back(job_item->vector_id); - return ret; - } - - // if the filer is for building material, refer to our global settings for - // which vectors to search - if (job_item->flags2.bits.building_material) - { - if (get_config_bool(config, CONFIG_BLOCKS)) - ret.push_back(df::job_item_vector_id::BLOCKS); - if (get_config_bool(config, CONFIG_BOULDERS)) - ret.push_back(df::job_item_vector_id::BOULDER); - if (get_config_bool(config, CONFIG_LOGS)) - ret.push_back(df::job_item_vector_id::WOOD); - if (get_config_bool(config, CONFIG_BARS)) - ret.push_back(df::job_item_vector_id::BAR); - } - - // fall back to IN_PLAY if no other vector was appropriate - if (ret.empty()) - ret.push_back(df::job_item_vector_id::IN_PLAY); - return ret; -} -static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { - df::building * bld = pb.getBuildingIfValidOrRemoveIfNot(out); - if (!bld) - return false; - - if (bld->jobs.size() != 1) { - DEBUG(status,out).print("unexpected number of jobs: want 1, got %zu\n", bld->jobs.size()); - return false; - } - auto job_items = bld->jobs[0]->job_items; - int num_job_items = job_items.size(); - if (num_job_items < 1) { - DEBUG(status,out).print("unexpected number of job items: want >0, got %d\n", num_job_items); - return false; - } - int32_t id = bld->id; - for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { - auto job_item = job_items[job_item_idx]; - auto bucket = getBucket(*job_item); - auto vector_ids = getVectorIds(out, job_item); - - // if there are multiple vector_ids, schedule duplicate tasks. after - // the correct number of items are matched, the extras will get popped - // as invalid - for (auto vector_id : vector_ids) { - for (int item_num = 0; item_num < job_item->quantity; ++item_num) { - tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx)); - DEBUG(status,out).print("added task: %s/%s/%d,%d; " - "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket.c_str(), id, job_item_idx, tasks.size(), - tasks[vector_id].size(), tasks[vector_id][bucket].size()); - } - } - } - - // suspend jobs - for (auto job : bld->jobs) - job->flags.bits.suspend = true; - - // add the planned buildings to our register - planned_buildings.emplace(bld->id, pb); - - return true; -} - -static void printStatus(color_ostream &out) { - DEBUG(status,out).print("entering buildingplan_printStatus\n"); - out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); - out.print(" finding materials for %zd buildings\n", planned_buildings.size()); - out.print("Current settings:\n"); - out.print(" use blocks: %s\n", get_config_bool(config, CONFIG_BLOCKS) ? "yes" : "no"); - out.print(" use boulders: %s\n", get_config_bool(config, CONFIG_BOULDERS) ? "yes" : "no"); - out.print(" use logs: %s\n", get_config_bool(config, CONFIG_LOGS) ? "yes" : "no"); - out.print(" use bars: %s\n", get_config_bool(config, CONFIG_BARS) ? "yes" : "no"); - out.print("\n"); -} - -static bool setSetting(color_ostream &out, string name, bool value) { - DEBUG(status,out).print("entering setSetting (%s -> %s)\n", name.c_str(), value ? "true" : "false"); - if (name == "blocks") - set_config_bool(config, CONFIG_BLOCKS, value); - else if (name == "boulders") - set_config_bool(config, CONFIG_BOULDERS, value); - else if (name == "logs") - set_config_bool(config, CONFIG_LOGS, value); - else if (name == "bars") - set_config_bool(config, CONFIG_BARS, value); - else { - out.printerr("unrecognized setting: '%s'\n", name.c_str()); - return false; - } - return true; -} - -static bool isPlannableBuilding(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom) { - DEBUG(status,out).print("entering isPlannableBuilding\n"); - int num_filters = 0; - if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, - [&](lua_State *L) { - Lua::Push(L, type); - Lua::Push(L, subtype); - Lua::Push(L, custom); - }, - [&](lua_State *L) { - num_filters = lua_tonumber(L, -1); - })) { - return false; - } - return num_filters >= 1; -} - -static bool isPlannedBuilding(color_ostream &out, df::building *bld) { - TRACE(status,out).print("entering isPlannedBuilding\n"); - return bld && planned_buildings.count(bld->id) > 0; -} - -static bool addPlannedBuilding(color_ostream &out, df::building *bld) { - DEBUG(status,out).print("entering addPlannedBuilding\n"); - if (!bld || planned_buildings.count(bld->id) - || !isPlannableBuilding(out, bld->getType(), bld->getSubtype(), - bld->getCustomType())) - return false; - PlannedBuilding pb(out, bld); - return registerPlannedBuilding(out, pb); -} - -static void doCycle(color_ostream &out) { - DEBUG(status,out).print("entering doCycle\n"); - do_cycle(out); -} - -static void scheduleCycle(color_ostream &out) { - DEBUG(status,out).print("entering scheduleCycle\n"); - cycle_requested = true; -} - -DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(printStatus), - DFHACK_LUA_FUNCTION(setSetting), - DFHACK_LUA_FUNCTION(isPlannableBuilding), - DFHACK_LUA_FUNCTION(isPlannedBuilding), - DFHACK_LUA_FUNCTION(addPlannedBuilding), - DFHACK_LUA_FUNCTION(doCycle), - DFHACK_LUA_FUNCTION(scheduleCycle), - DFHACK_LUA_END -}; diff --git a/plugins/buildingplan/CMakeLists.txt b/plugins/buildingplan/CMakeLists.txt index 1d34b169a..118b2a1d1 100644 --- a/plugins/buildingplan/CMakeLists.txt +++ b/plugins/buildingplan/CMakeLists.txt @@ -2,10 +2,15 @@ project(buildingplan) set(COMMON_HDRS buildingplan.h - buildingplan-planner.h - buildingplan-rooms.h + buildingtypekey.h + defaultitemfilters.h + itemfilter.h + plannedbuilding.h ) set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) -dfhack_plugin(buildingplan buildingplan.cpp buildingplan-planner.cpp - buildingplan-rooms.cpp ${COMMON_HDRS} LINK_LIBRARIES lua) +dfhack_plugin(buildingplan + buildingplan.cpp buildingplan_cycle.cpp buildingtypekey.cpp + defaultitemfilters.cpp itemfilter.cpp plannedbuilding.cpp + ${COMMON_HDRS} + LINK_LIBRARIES lua) diff --git a/plugins/buildingplan/buildingplan-planner.cpp b/plugins/buildingplan/buildingplan-planner.cpp deleted file mode 100644 index 07f23150a..000000000 --- a/plugins/buildingplan/buildingplan-planner.cpp +++ /dev/null @@ -1,1074 +0,0 @@ -#include -#include // for CHAR_BIT - -#include "df/building_design.h" -#include "df/building_doorst.h" -#include "df/building_type.h" -#include "df/general_ref_building_holderst.h" -#include "df/job_item.h" -#include "df/buildreq.h" - -#include "modules/Buildings.h" -#include "modules/Gui.h" -#include "modules/Job.h" - -#include "LuaTools.h" -#include "../uicommon.h" - -#include "buildingplan.h" - -static const std::string planned_building_persistence_key_v1 = "buildingplan/constraints"; -static const std::string planned_building_persistence_key_v2 = "buildingplan/constraints2"; -static const std::string global_settings_persistence_key = "buildingplan/global"; - -/* - * ItemFilter - */ - -ItemFilter::ItemFilter() -{ - clear(); -} - -void ItemFilter::clear() -{ - min_quality = df::item_quality::Ordinary; - max_quality = df::item_quality::Masterful; - decorated_only = false; - clearMaterialMask(); - materials.clear(); -} - -bool ItemFilter::deserialize(std::string ser) -{ - clear(); - - std::vector tokens; - split_string(&tokens, ser, "/"); - if (tokens.size() != 5) - { - debug("invalid ItemFilter serialization: '%s'", ser.c_str()); - return false; - } - - if (!deserializeMaterialMask(tokens[0]) || !deserializeMaterials(tokens[1])) - return false; - - setMinQuality(atoi(tokens[2].c_str())); - setMaxQuality(atoi(tokens[3].c_str())); - decorated_only = static_cast(atoi(tokens[4].c_str())); - return true; -} - -bool ItemFilter::deserializeMaterialMask(std::string ser) -{ - if (ser.empty()) - return true; - - if (!parseJobMaterialCategory(&mat_mask, ser)) - { - debug("invalid job material category serialization: '%s'", ser.c_str()); - return false; - } - return true; -} - -bool ItemFilter::deserializeMaterials(std::string ser) -{ - if (ser.empty()) - return true; - - std::vector mat_names; - split_string(&mat_names, ser, ","); - for (auto m = mat_names.begin(); m != mat_names.end(); m++) - { - DFHack::MaterialInfo material; - if (!material.find(*m) || !material.isValid()) - { - debug("invalid material name serialization: '%s'", ser.c_str()); - return false; - } - materials.push_back(material); - } - return true; -} - -// format: mat,mask,elements/materials,list/minq/maxq/decorated -std::string ItemFilter::serialize() const -{ - std::ostringstream ser; - ser << bitfield_to_string(mat_mask, ",") << "/"; - if (!materials.empty()) - { - ser << materials[0].getToken(); - for (size_t i = 1; i < materials.size(); ++i) - ser << "," << materials[i].getToken(); - } - ser << "/" << static_cast(min_quality); - ser << "/" << static_cast(max_quality); - ser << "/" << static_cast(decorated_only); - return ser.str(); -} - -void ItemFilter::clearMaterialMask() -{ - mat_mask.whole = 0; -} - -void ItemFilter::addMaterialMask(uint32_t mask) -{ - mat_mask.whole |= mask; -} - -void ItemFilter::setMaterials(std::vector materials) -{ - this->materials = materials; -} - -static void clampItemQuality(df::item_quality *quality) -{ - if (*quality > item_quality::Artifact) - { - debug("clamping quality to Artifact"); - *quality = item_quality::Artifact; - } - if (*quality < item_quality::Ordinary) - { - debug("clamping quality to Ordinary"); - *quality = item_quality::Ordinary; - } -} - -void ItemFilter::setMinQuality(int quality) -{ - min_quality = static_cast(quality); - clampItemQuality(&min_quality); - if (max_quality < min_quality) - max_quality = min_quality; -} - -void ItemFilter::setMaxQuality(int quality) -{ - max_quality = static_cast(quality); - clampItemQuality(&max_quality); - if (max_quality < min_quality) - min_quality = max_quality; -} - -void ItemFilter::incMinQuality() { setMinQuality(min_quality + 1); } -void ItemFilter::decMinQuality() { setMinQuality(min_quality - 1); } -void ItemFilter::incMaxQuality() { setMaxQuality(max_quality + 1); } -void ItemFilter::decMaxQuality() { setMaxQuality(max_quality - 1); } - -void ItemFilter::toggleDecoratedOnly() { decorated_only = !decorated_only; } - -static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } - -uint32_t ItemFilter::getMaterialMask() const { return mat_mask.whole; } - -std::vector ItemFilter::getMaterials() const -{ - std::vector descriptions; - transform_(materials, descriptions, material_to_string_fn); - - if (descriptions.size() == 0) - bitfield_to_string(&descriptions, mat_mask); - - if (descriptions.size() == 0) - descriptions.push_back("any"); - - return descriptions; -} - -std::string ItemFilter::getMinQuality() const -{ - return ENUM_KEY_STR(item_quality, min_quality); -} - -std::string ItemFilter::getMaxQuality() const -{ - return ENUM_KEY_STR(item_quality, max_quality); -} - -bool ItemFilter::getDecoratedOnly() const -{ - return decorated_only; -} - -bool ItemFilter::matchesMask(DFHack::MaterialInfo &mat) const -{ - return mat_mask.whole ? mat.matches(mat_mask) : true; -} - -bool ItemFilter::matches(df::dfhack_material_category mask) const -{ - return mask.whole & mat_mask.whole; -} - -bool ItemFilter::matches(DFHack::MaterialInfo &material) const -{ - for (auto it = materials.begin(); it != materials.end(); ++it) - if (material.matches(*it)) - return true; - return false; -} - -bool ItemFilter::matches(df::item *item) const -{ - if (item->getQuality() < min_quality || item->getQuality() > max_quality) - return false; - - if (decorated_only && !item->hasImprovements()) - return false; - - auto imattype = item->getActualMaterial(); - auto imatindex = item->getActualMaterialIndex(); - auto item_mat = DFHack::MaterialInfo(imattype, imatindex); - - return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat); -} - - -/* - * PlannedBuilding - */ - -// format: itemfilterser|itemfilterser|... -static std::string serializeFilters(const std::vector &filters) -{ - std::ostringstream ser; - if (!filters.empty()) - { - ser << filters[0].serialize(); - for (size_t i = 1; i < filters.size(); ++i) - ser << "|" << filters[i].serialize(); - } - return ser.str(); -} - -static std::vector deserializeFilters(std::string ser) -{ - std::vector isers; - split_string(&isers, ser, "|"); - std::vector ret; - for (auto & iser : isers) - { - ItemFilter filter; - if (filter.deserialize(iser)) - ret.push_back(filter); - } - return ret; -} - -static size_t getNumFilters(BuildingTypeKey key) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 4) || !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "get_num_filters")) - { - debug("failed to push the lua method on the stack"); - return 0; - } - - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - - if (!Lua::SafeCall(out, L, 3, 1)) - { - debug("lua call failed"); - return 0; - } - - int num_filters = lua_tonumber(L, -1); - lua_pop(L, 1); - return num_filters; -} - -PlannedBuilding::PlannedBuilding(df::building *building, const std::vector &filters) - : building(building), - building_id(building->id), - filters(filters) -{ - config = DFHack::World::AddPersistentData(planned_building_persistence_key_v2); - config.ival(0) = building_id; - config.val() = serializeFilters(filters); -} - -PlannedBuilding::PlannedBuilding(PersistentDataItem &config) - : config(config), - building(df::building::find(config.ival(0))), - building_id(config.ival(0)), - filters(deserializeFilters(config.val())) -{ - if (building) - { - if (filters.size() != - getNumFilters(toBuildingTypeKey(building))) - { - debug("invalid ItemFilter vector serialization: '%s'", - config.val().c_str()); - building = NULL; - } - } -} - -// Ensure the building still exists and is in a valid state. It can disappear -// for lots of reasons, such as running the game with the buildingplan plugin -// disabled, manually removing the building, modifying it via the API, etc. -bool PlannedBuilding::isValid() const -{ - return building && df::building::find(building_id) - && building->getBuildStage() == 0; -} - -void PlannedBuilding::remove() -{ - DFHack::World::DeletePersistentData(config); - building = NULL; -} - -df::building * PlannedBuilding::getBuilding() -{ - return building; -} - -const std::vector & PlannedBuilding::getFilters() const -{ - // if we want to be able to dynamically change the filters, we'll need to - // re-bucket the tasks in Planner. - return filters; -} - - -/* - * BuildingTypeKey - */ - -BuildingTypeKey toBuildingTypeKey( - df::building_type btype, int16_t subtype, int32_t custom) -{ - return std::make_tuple(btype, subtype, custom); -} - -BuildingTypeKey toBuildingTypeKey(df::building *bld) -{ - return std::make_tuple( - bld->getType(), bld->getSubtype(), bld->getCustomType()); -} - -BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs) -{ - return std::make_tuple( - uibs->building_type, uibs->building_subtype, uibs->custom_type); -} - -// rotates a size_t value left by count bits -// assumes count is not 0 or >= size_t_bits -// replace this with std::rotl when we move to C++20 -static std::size_t rotl_size_t(size_t val, uint32_t count) -{ - static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); - return val << count | val >> (size_t_bits - count); -} - -std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const -{ - // cast first param to appease gcc-4.8, which is missing the enum - // specializations for std::hash - std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); - std::size_t h2 = std::hash()(std::get<1>(key)); - std::size_t h3 = std::hash()(std::get<2>(key)); - - return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); -} - - -/* - * Planner - */ - -// convert v1 persistent data into v2 format -// we can remove this conversion code once v2 has been live for a while -void migrateV1ToV2() -{ - std::vector configs; - DFHack::World::GetPersistentData(&configs, planned_building_persistence_key_v1); - if (configs.empty()) - return; - - debug("migrating %zu persisted configs to new format", configs.size()); - for (auto config : configs) - { - df::building *bld = df::building::find(config.ival(1)); - if (!bld) - { - debug("buliding no longer exists; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - if (bld->getBuildStage() != 0 || bld->jobs.size() != 1 - || bld->jobs[0]->job_items.size() != 1) - { - debug("building in invalid state; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - // fix up the building so we can set the material properties later - bld->mat_type = -1; - bld->mat_index = -1; - - // the v1 filters are not initialized correctly and will match any item. - // we need to fix them up a bit. - auto filter = bld->jobs[0]->job_items[0]; - df::item_type type; - switch (bld->getType()) - { - case df::building_type::Armorstand: type = df::item_type::ARMORSTAND; break; - case df::building_type::Bed: type = df::item_type::BED; break; - case df::building_type::Chair: type = df::item_type::CHAIR; break; - case df::building_type::Coffin: type = df::item_type::COFFIN; break; - case df::building_type::Door: type = df::item_type::DOOR; break; - case df::building_type::Floodgate: type = df::item_type::FLOODGATE; break; - case df::building_type::Hatch: type = df::item_type::HATCH_COVER; break; - case df::building_type::GrateWall: type = df::item_type::GRATE; break; - case df::building_type::GrateFloor: type = df::item_type::GRATE; break; - case df::building_type::BarsVertical: type = df::item_type::BAR; break; - case df::building_type::BarsFloor: type = df::item_type::BAR; break; - case df::building_type::Cabinet: type = df::item_type::CABINET; break; - case df::building_type::Box: type = df::item_type::BOX; break; - case df::building_type::Weaponrack: type = df::item_type::WEAPONRACK; break; - case df::building_type::Statue: type = df::item_type::STATUE; break; - case df::building_type::Slab: type = df::item_type::SLAB; break; - case df::building_type::Table: type = df::item_type::TABLE; break; - case df::building_type::WindowGlass: type = df::item_type::WINDOW; break; - case df::building_type::AnimalTrap: type = df::item_type::ANIMALTRAP; break; - case df::building_type::Chain: type = df::item_type::CHAIN; break; - case df::building_type::Cage: type = df::item_type::CAGE; break; - case df::building_type::TractionBench: type = df::item_type::TRACTION_BENCH; break; - default: - debug("building has unhandled type; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - filter->item_type = type; - filter->item_subtype = -1; - filter->mat_type = -1; - filter->mat_index = -1; - filter->flags1.whole = 0; - filter->flags2.whole = 0; - filter->flags2.bits.allow_artifact = true; - filter->flags3.whole = 0; - filter->flags4 = 0; - filter->flags5 = 0; - filter->metal_ore = -1; - filter->min_dimension = -1; - filter->has_tool_use = df::tool_uses::NONE; - filter->quantity = 1; - - std::vector tokens; - split_string(&tokens, config.val(), "/"); - if (tokens.size() != 2) - { - debug("invalid v1 format; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - ItemFilter item_filter; - item_filter.deserializeMaterialMask(tokens[0]); - item_filter.deserializeMaterials(tokens[1]); - item_filter.setMinQuality(config.ival(2) - 1); - item_filter.setMaxQuality(config.ival(4) - 1); - if (config.ival(3) - 1) - item_filter.toggleDecoratedOnly(); - - // create the v2 record - std::vector item_filters; - item_filters.push_back(item_filter); - PlannedBuilding pb(bld, item_filters); - - // remove the v1 record - DFHack::World::DeletePersistentData(config); - debug("v1 %s(%d) record successfully migrated", - ENUM_KEY_STR(building_type, bld->getType()).c_str(), - bld->id); - } -} - -// assumes no setting has '=' or '|' characters -static std::string serialize_settings(std::map & settings) -{ - std::ostringstream ser; - for (auto & entry : settings) - { - ser << entry.first << "=" << (entry.second ? "1" : "0") << "|"; - } - return ser.str(); -} - -static void deserialize_settings(std::map & settings, - std::string ser) -{ - std::vector tokens; - split_string(&tokens, ser, "|"); - for (auto token : tokens) - { - if (token.empty()) - continue; - - std::vector parts; - split_string(&parts, token, "="); - if (parts.size() != 2) - { - debug("invalid serialized setting format: '%s'", token.c_str()); - continue; - } - std::string key = parts[0]; - if (settings.count(key) == 0) - { - debug("unknown serialized setting: '%s", key.c_str()); - continue; - } - settings[key] = static_cast(atoi(parts[1].c_str())); - debug("deserialized setting: %s = %d", key.c_str(), settings[key]); - } -} - -static DFHack::PersistentDataItem init_global_settings( - std::map & settings) -{ - settings.clear(); - settings["blocks"] = true; - settings["boulders"] = true; - settings["logs"] = true; - settings["bars"] = false; - - // load persistent global settings if they exist; otherwise create them - std::vector items; - DFHack::World::GetPersistentData(&items, global_settings_persistence_key); - if (items.size() == 1) - { - DFHack::PersistentDataItem & config = items[0]; - deserialize_settings(settings, config.val()); - return config; - } - - debug("initializing persistent global settings"); - DFHack::PersistentDataItem config = - DFHack::World::AddPersistentData(global_settings_persistence_key); - config.val() = serialize_settings(settings); - return config; -} - -const std::map & Planner::getGlobalSettings() const -{ - return global_settings; -} - -bool Planner::setGlobalSetting(std::string name, bool value) -{ - if (global_settings.count(name) == 0) - { - debug("attempted to set invalid setting: '%s'", name.c_str()); - return false; - } - debug("global setting '%s' %d -> %d", - name.c_str(), global_settings[name], value); - global_settings[name] = value; - if (config.isValid()) - config.val() = serialize_settings(global_settings); - return true; -} - -void Planner::reset() -{ - debug("resetting Planner state"); - default_item_filters.clear(); - planned_buildings.clear(); - tasks.clear(); - - config = init_global_settings(global_settings); - - migrateV1ToV2(); - - std::vector items; - DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v2); - debug("found data for %zu planned building(s)", items.size()); - - for (auto i = items.begin(); i != items.end(); i++) - { - PlannedBuilding pb(*i); - if (!pb.isValid()) - { - debug("discarding invalid planned building"); - pb.remove(); - continue; - } - - if (registerTasks(pb)) - planned_buildings.insert(std::make_pair(pb.getBuilding()->id, pb)); - } -} - -void Planner::addPlannedBuilding(df::building *bld) -{ - auto item_filters = getItemFilters(toBuildingTypeKey(bld)).get(); - // not a supported type - if (item_filters.empty()) - { - debug("failed to add building: unsupported type"); - return; - } - - // protect against multiple registrations - if (planned_buildings.count(bld->id) != 0) - { - debug("failed to add building: already registered"); - return; - } - - PlannedBuilding pb(bld, item_filters); - if (pb.isValid() && registerTasks(pb)) - { - for (auto job : bld->jobs) - job->flags.bits.suspend = true; - - planned_buildings.insert(std::make_pair(bld->id, pb)); - } - else - { - pb.remove(); - } -} - -static std::string getBucket(const df::job_item & ji, - const std::vector & item_filters) -{ - std::ostringstream ser; - - // pull out and serialize only known relevant fields. if we miss a few, then - // the filter bucket will be slighly less specific than it could be, but - // that's probably ok. we'll just end up bucketing slightly different items - // together. this is only a problem if the different filter at the front of - // the queue doesn't match any available items and blocks filters behind it - // that could be matched. - ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':' - << ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole - << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' - << ji.metal_ore << ':' << ji.has_tool_use; - - for (auto & item_filter : item_filters) - { - ser << ':' << item_filter.serialize(); - } - - return ser.str(); -} - -// get a list of item vectors that we should search for matches -static std::vector getVectorIds(df::job_item *job_item, - const std::map & global_settings) -{ - std::vector ret; - - // if the filter already has the vector_id set to something specific, use it - if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) - { - debug("using vector_id from job_item: %s", - ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); - ret.push_back(job_item->vector_id); - return ret; - } - - // if the filer is for building material, refer to our global settings for - // which vectors to search - if (job_item->flags2.bits.building_material) - { - if (global_settings.at("blocks")) - ret.push_back(df::job_item_vector_id::BLOCKS); - if (global_settings.at("boulders")) - ret.push_back(df::job_item_vector_id::BOULDER); - if (global_settings.at("logs")) - ret.push_back(df::job_item_vector_id::WOOD); - if (global_settings.at("bars")) - ret.push_back(df::job_item_vector_id::BAR); - } - - // fall back to IN_PLAY if no other vector was appropriate - if (ret.empty()) - ret.push_back(df::job_item_vector_id::IN_PLAY); - return ret; -} - -bool Planner::registerTasks(PlannedBuilding & pb) -{ - df::building * bld = pb.getBuilding(); - if (bld->jobs.size() != 1) - { - debug("unexpected number of jobs: want 1, got %zu", bld->jobs.size()); - return false; - } - auto job_items = bld->jobs[0]->job_items; - int num_job_items = job_items.size(); - if (num_job_items < 1) - { - debug("unexpected number of job items: want >0, got %d", num_job_items); - return false; - } - int32_t id = bld->id; - for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) - { - auto job_item = job_items[job_item_idx]; - auto bucket = getBucket(*job_item, pb.getFilters()); - auto vector_ids = getVectorIds(job_item, global_settings); - - // if there are multiple vector_ids, schedule duplicate tasks. after - // the correct number of items are matched, the extras will get popped - // as invalid - for (auto vector_id : vector_ids) - { - for (int item_num = 0; item_num < job_item->quantity; ++item_num) - { - tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx)); - debug("added task: %s/%s/%d,%d; " - "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket.c_str(), id, job_item_idx, tasks.size(), - tasks[vector_id].size(), tasks[vector_id][bucket].size()); - } - } - } - return true; -} - -PlannedBuilding * Planner::getPlannedBuilding(df::building *bld) -{ - if (!bld || planned_buildings.count(bld->id) == 0) - return NULL; - return &planned_buildings.at(bld->id); -} - -bool Planner::isPlannableBuilding(BuildingTypeKey key) -{ - return getNumFilters(key) >= 1; -} - -Planner::ItemFiltersWrapper Planner::getItemFilters(BuildingTypeKey key) -{ - static std::vector empty_vector; - static const ItemFiltersWrapper empty_ret(empty_vector); - - size_t nfilters = getNumFilters(key); - if (nfilters < 1) - return empty_ret; - while (default_item_filters[key].size() < nfilters) - default_item_filters[key].push_back(ItemFilter()); - return ItemFiltersWrapper(default_item_filters[key]); -} - -// precompute a bitmask with bad item flags -struct BadFlags -{ - uint32_t whole; - - BadFlags() - { - df::item_flags flags; - #define F(x) 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); F(in_job); - F(owned); F(in_chest); F(removed); F(encased); - #undef F - whole = flags.whole; - } -}; - -static bool itemPassesScreen(df::item * item) -{ - static BadFlags bad_flags; - return !(item->flags.whole & bad_flags.whole) - && !item->isAssignedToStockpile() - // TODO: make this configurable - && !(item->getType() == df::item_type::BOX && item->isBag()); -} - -static bool matchesFilters(df::item * item, - df::job_item * job_item, - const ItemFilter & item_filter) -{ - // check the properties that are not checked by Job::isSuitableItem() - if (job_item->item_type > -1 && job_item->item_type != item->getType()) - return false; - - if (job_item->item_subtype > -1 && - job_item->item_subtype != item->getSubtype()) - return false; - - if (job_item->flags2.bits.building_material && !item->isBuildMat()) - return false; - - if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) - return false; - - if (job_item->has_tool_use > df::tool_uses::NONE - && !item->hasToolUse(job_item->has_tool_use)) - return false; - - return DFHack::Job::isSuitableItem( - job_item, item->getType(), item->getSubtype()) - && DFHack::Job::isSuitableMaterial( - job_item, item->getMaterial(), item->getMaterialIndex(), - item->getType()) - && item_filter.matches(item); -} - -// note that this just removes the PlannedBuilding. the tasks will get dropped -// as we discover them in the tasks queues and they fail their isValid() check. -// this "lazy" task cleaning algorithm works because there is no way to -// re-register a building once it has been removed -- if it fails isValid() -// then it has either been built or desroyed. therefore there is no chance of -// duplicate tasks getting added to the tasks queues. -void Planner::unregisterBuilding(int32_t id) -{ - if (planned_buildings.count(id) > 0) - { - planned_buildings.at(id).remove(); - planned_buildings.erase(id); - } -} - -static bool isJobReady(df::job * job) -{ - int needed_items = 0; - for (auto job_item : job->job_items) { needed_items += job_item->quantity; } - if (needed_items) - { - debug("building needs %d more item(s)", needed_items); - return false; - } - return true; -} - -static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) -{ - // we want the items in the opposite order of the filters - return a->job_item_idx > b->job_item_idx; -} - -// this function does not remove the job_items since their quantity fields are -// now all at 0, so there is no risk of having extra items attached. we don't -// remove them to keep the "finalize with buildingplan active" path as similar -// as possible to the "finalize with buildingplan disabled" path. -static void finalizeBuilding(df::building * bld) -{ - debug("finalizing building %d", bld->id); - auto job = bld->jobs[0]; - - // sort the items so they get added to the structure in the correct order - std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); - - // derive the material properties of the building and job from the first - // applicable item, though if any boulders are involved, it makes the whole - // structure "rough". - bool rough = false; - for (auto attached_item : job->items) - { - df::item *item = attached_item->item; - rough = rough || item->getType() == item_type::BOULDER; - if (bld->mat_type == -1) - { - bld->mat_type = item->getMaterial(); - job->mat_type = bld->mat_type; - } - if (bld->mat_index == -1) - { - bld->mat_index = item->getMaterialIndex(); - job->mat_index = bld->mat_index; - } - } - - if (bld->needsDesign()) - { - auto act = (df::building_actual *)bld; - if (!act->design) - act->design = new df::building_design(); - act->design->flags.bits.rough = rough; - } - - // we're good to go! - job->flags.bits.suspend = false; - Job::checkBuildingsNow(); -} - -void Planner::popInvalidTasks(std::queue> & task_queue) -{ - while (!task_queue.empty()) - { - auto & task = task_queue.front(); - auto id = task.first; - if (planned_buildings.count(id) > 0) - { - PlannedBuilding & pb = planned_buildings.at(id); - if (pb.isValid() && - pb.getBuilding()->jobs[0]->job_items[task.second]->quantity) - { - break; - } - } - debug("discarding invalid task: bld=%d, job_item_idx=%d", - id, task.second); - task_queue.pop(); - unregisterBuilding(id); - } -} - -void Planner::doVector(df::job_item_vector_id vector_id, - std::map>> & buckets) -{ - auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); - auto item_vector = df::global::world->items.other[other_id]; - debug("matching %zu item(s) in vector %s against %zu filter bucket(s)", - item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - buckets.size()); - for (auto item_it = item_vector.rbegin(); - item_it != item_vector.rend(); - ++item_it) - { - auto item = *item_it; - if (!itemPassesScreen(item)) - continue; - for (auto bucket_it = buckets.begin(); bucket_it != buckets.end();) - { - auto & task_queue = bucket_it->second; - popInvalidTasks(task_queue); - if (task_queue.empty()) - { - debug("removing empty bucket: %s/%s; %zu bucket(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - bucket_it = buckets.erase(bucket_it); - continue; - } - auto & task = task_queue.front(); - auto id = task.first; - auto & pb = planned_buildings.at(id); - auto building = pb.getBuilding(); - auto job = building->jobs[0]; - auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx], - pb.getFilters()[filter_idx]) - && DFHack::Job::attachJobItem(job, item, - df::job_item_ref::Hauled, filter_idx)) - { - MaterialInfo material; - material.decode(item); - ItemTypeInfo item_type; - item_type.decode(item); - debug("attached %s %s to filter %d for %s(%d): %s/%s", - material.toString().c_str(), - item_type.toString().c_str(), - filter_idx, - ENUM_KEY_STR(building_type, building->getType()).c_str(), - id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); - // keep quantity aligned with the actual number of remaining - // items so if buildingplan is turned off, the building will - // be completed with the correct number of items. - --job->job_items[filter_idx]->quantity; - task_queue.pop(); - if (isJobReady(job)) - { - finalizeBuilding(building); - unregisterBuilding(id); - } - if (task_queue.empty()) - { - debug( - "removing empty item bucket: %s/%s; %zu left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - buckets.erase(bucket_it); - } - // we found a home for this item; no need to look further - break; - } - ++bucket_it; - } - if (buckets.empty()) - break; - } -} - -struct VectorsToScanLast -{ - std::vector vectors; - VectorsToScanLast() - { - // order is important here. we want to match boulders before wood and - // everything before bars. blocks are not listed here since we'll have - // already scanned them when we did the first pass through the buckets. - vectors.push_back(df::job_item_vector_id::BOULDER); - vectors.push_back(df::job_item_vector_id::WOOD); - vectors.push_back(df::job_item_vector_id::BAR); - } -}; - -void Planner::doCycle() -{ - debug("running cycle for %zu registered building(s)", - planned_buildings.size()); - static const VectorsToScanLast vectors_to_scan_last; - for (auto it = tasks.begin(); it != tasks.end();) - { - auto vector_id = it->first; - // we could make this a set, but it's only three elements - if (std::find(vectors_to_scan_last.vectors.begin(), - vectors_to_scan_last.vectors.end(), - vector_id) != vectors_to_scan_last.vectors.end()) - { - ++it; - continue; - } - - auto & buckets = it->second; - doVector(vector_id, buckets); - if (buckets.empty()) - { - debug("removing empty vector: %s; %zu vector(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - it = tasks.erase(it); - } - else - ++it; - } - for (auto vector_id : vectors_to_scan_last.vectors) - { - if (tasks.count(vector_id) == 0) - continue; - auto & buckets = tasks[vector_id]; - doVector(vector_id, buckets); - if (buckets.empty()) - { - debug("removing empty vector: %s; %zu vector(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - tasks.erase(vector_id); - } - } - debug("cycle done; %zu registered building(s) left", - planned_buildings.size()); -} - -Planner planner; diff --git a/plugins/buildingplan/buildingplan-planner.h b/plugins/buildingplan/buildingplan-planner.h deleted file mode 100644 index 7b1615704..000000000 --- a/plugins/buildingplan/buildingplan-planner.h +++ /dev/null @@ -1,140 +0,0 @@ -#pragma once - -#include -#include - -#include "df/building.h" -#include "df/dfhack_material_category.h" -#include "df/item_quality.h" -#include "df/job_item.h" - -#include "modules/Materials.h" -#include "modules/Persistence.h" - -class ItemFilter -{ -public: - ItemFilter(); - - void clear(); - bool deserialize(std::string ser); - std::string serialize() const; - - void addMaterialMask(uint32_t mask); - void clearMaterialMask(); - void setMaterials(std::vector materials); - - void incMinQuality(); - void decMinQuality(); - void incMaxQuality(); - void decMaxQuality(); - void toggleDecoratedOnly(); - - uint32_t getMaterialMask() const; - std::vector getMaterials() const; - std::string getMinQuality() const; - std::string getMaxQuality() const; - bool getDecoratedOnly() const; - - bool matches(df::dfhack_material_category mask) const; - bool matches(DFHack::MaterialInfo &material) const; - bool matches(df::item *item) const; - -private: - // remove friend declaration when we no longer need v1 deserialization - friend void migrateV1ToV2(); - - df::dfhack_material_category mat_mask; - std::vector materials; - df::item_quality min_quality; - df::item_quality max_quality; - bool decorated_only; - - bool deserializeMaterialMask(std::string ser); - bool deserializeMaterials(std::string ser); - void setMinQuality(int quality); - void setMaxQuality(int quality); - bool matchesMask(DFHack::MaterialInfo &mat) const; -}; - -class PlannedBuilding -{ -public: - PlannedBuilding(df::building *building, const std::vector &filters); - PlannedBuilding(DFHack::PersistentDataItem &config); - - bool isValid() const; - void remove(); - - df::building * getBuilding(); - const std::vector & getFilters() const; - -private: - DFHack::PersistentDataItem config; - df::building *building; - const df::building::key_field_type building_id; - const std::vector filters; -}; - -// building type, subtype, custom -typedef std::tuple BuildingTypeKey; - -BuildingTypeKey toBuildingTypeKey( - df::building_type btype, int16_t subtype, int32_t custom); -BuildingTypeKey toBuildingTypeKey(df::building *bld); -BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs); - -struct BuildingTypeKeyHash -{ - std::size_t operator() (const BuildingTypeKey & key) const; -}; - -class Planner -{ -public: - class ItemFiltersWrapper - { - public: - ItemFiltersWrapper(std::vector & item_filters) - : item_filters(item_filters) { } - std::vector::reverse_iterator rbegin() const { return item_filters.rbegin(); } - std::vector::reverse_iterator rend() const { return item_filters.rend(); } - const std::vector & get() const { return item_filters; } - private: - std::vector &item_filters; - }; - - const std::map & getGlobalSettings() const; - bool setGlobalSetting(std::string name, bool value); - - void reset(); - - void addPlannedBuilding(df::building *bld); - PlannedBuilding *getPlannedBuilding(df::building *bld); - - bool isPlannableBuilding(BuildingTypeKey key); - - // returns an empty vector if the type is not supported - ItemFiltersWrapper getItemFilters(BuildingTypeKey key); - - void doCycle(); - -private: - DFHack::PersistentDataItem config; - std::map global_settings; - std::unordered_map, - BuildingTypeKeyHash> default_item_filters; - // building id -> PlannedBuilding - std::unordered_map planned_buildings; - // vector id -> filter bucket -> queue of (building id, job_item index) - std::map>>> tasks; - - bool registerTasks(PlannedBuilding &plannedBuilding); - void unregisterBuilding(int32_t id); - void popInvalidTasks(std::queue> &task_queue); - void doVector(df::job_item_vector_id vector_id, - std::map>> & buckets); -}; - -extern Planner planner; diff --git a/plugins/buildingplan/buildingplan-rooms.cpp b/plugins/buildingplan/buildingplan-rooms.cpp deleted file mode 100644 index a08c85804..000000000 --- a/plugins/buildingplan/buildingplan-rooms.cpp +++ /dev/null @@ -1,226 +0,0 @@ -#include "buildingplan.h" - -#include -#include -#include - -#include -#include -#include - -using namespace DFHack; - -bool canReserveRoom(df::building *building) -{ - if (!building) - return false; - - if (building->jobs.size() > 0 && building->jobs[0]->job_type == df::job_type::DestroyBuilding) - return false; - - return building->is_room; -} - -std::vector getUniqueNoblePositions(df::unit *unit) -{ - std::vector np; - Units::getNoblePositions(&np, unit); - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == "MILITIA_CAPTAIN") - { - np.erase(iter); - break; - } - } - - return np; -} - -/* - * ReservedRoom - */ - -ReservedRoom::ReservedRoom(df::building *building, std::string noble_code) -{ - this->building = building; - config = DFHack::World::AddPersistentData("buildingplan/reservedroom"); - config.val() = noble_code; - config.ival(1) = building->id; - pos = df::coord(building->centerx, building->centery, building->z); -} - -ReservedRoom::ReservedRoom(PersistentDataItem &config, color_ostream &) -{ - this->config = config; - - building = df::building::find(config.ival(1)); - if (!building) - return; - pos = df::coord(building->centerx, building->centery, building->z); -} - -bool ReservedRoom::checkRoomAssignment() -{ - if (!isValid()) - return false; - - auto np = getOwnersNobleCode(); - bool correctOwner = false; - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == getCode()) - { - correctOwner = true; - break; - } - } - - if (correctOwner) - return true; - - for (auto iter = df::global::world->units.active.begin(); iter != df::global::world->units.active.end(); iter++) - { - df::unit* unit = *iter; - if (!Units::isCitizen(unit)) - continue; - - if (!Units::isActive(unit)) - continue; - - np = getUniqueNoblePositions(unit); - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == getCode()) - { - Buildings::setOwner(building, unit); - break; - } - } - } - - return true; -} - -void ReservedRoom::remove() { DFHack::World::DeletePersistentData(config); } - -bool ReservedRoom::isValid() -{ - if (!building) - return false; - - if (Buildings::findAtTile(pos) != building) - return false; - - return canReserveRoom(building); -} - -int32_t ReservedRoom::getId() -{ - if (!isValid()) - return 0; - - return building->id; -} - -std::string ReservedRoom::getCode() { return config.val(); } - -void ReservedRoom::setCode(const std::string &noble_code) { config.val() = noble_code; } - -std::vector ReservedRoom::getOwnersNobleCode() -{ - if (!building->owner) - return std::vector (); - - return getUniqueNoblePositions(building->owner); -} - -/* - * RoomMonitor - */ - -std::string RoomMonitor::getReservedNobleCode(int32_t buildingId) -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (buildingId == iter->getId()) - return iter->getCode(); - } - - return ""; -} - -void RoomMonitor::toggleRoomForPosition(int32_t buildingId, std::string noble_code) -{ - bool found = false; - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (buildingId != iter->getId()) - { - continue; - } - else - { - if (noble_code == iter->getCode()) - { - iter->remove(); - reservedRooms.erase(iter); - } - else - { - iter->setCode(noble_code); - } - found = true; - break; - } - } - - if (!found) - { - ReservedRoom room(df::building::find(buildingId), noble_code); - reservedRooms.push_back(room); - } -} - -void RoomMonitor::doCycle() -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end();) - { - if (iter->checkRoomAssignment()) - { - ++iter; - } - else - { - iter->remove(); - iter = reservedRooms.erase(iter); - } - } -} - -void RoomMonitor::reset(color_ostream &out) -{ - reservedRooms.clear(); - std::vector items; - DFHack::World::GetPersistentData(&items, "buildingplan/reservedroom"); - - for (auto i = items.begin(); i != items.end(); i++) - { - ReservedRoom rr(*i, out); - if (rr.isValid()) - addRoom(rr); - } -} - -void RoomMonitor::addRoom(ReservedRoom &rr) -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (iter->getId() == rr.getId()) - return; - } - - reservedRooms.push_back(rr); -} - -RoomMonitor roomMonitor; diff --git a/plugins/buildingplan/buildingplan-rooms.h b/plugins/buildingplan/buildingplan-rooms.h deleted file mode 100644 index 3880dbe06..000000000 --- a/plugins/buildingplan/buildingplan-rooms.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include "modules/Persistence.h" -#include "modules/Units.h" - -class ReservedRoom -{ -public: - ReservedRoom(df::building *building, std::string noble_code); - - ReservedRoom(DFHack::PersistentDataItem &config, DFHack::color_ostream &out); - - bool checkRoomAssignment(); - void remove(); - bool isValid(); - - int32_t getId(); - std::string getCode(); - void setCode(const std::string &noble_code); - -private: - df::building *building; - DFHack::PersistentDataItem config; - df::coord pos; - - std::vector getOwnersNobleCode(); -}; - -class RoomMonitor -{ -public: - RoomMonitor() { } - - std::string getReservedNobleCode(int32_t buildingId); - - void toggleRoomForPosition(int32_t buildingId, std::string noble_code); - - void doCycle(); - - void reset(DFHack::color_ostream &out); - -private: - std::vector reservedRooms; - - void addRoom(ReservedRoom &rr); -}; - -bool canReserveRoom(df::building *building); -std::vector getUniqueNoblePositions(df::unit *unit); - -extern RoomMonitor roomMonitor; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index cd4e84a6e..7479d348d 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -1,1168 +1,893 @@ -#include "df/construction_type.h" -#include "df/entity_position.h" -#include "df/interface_key.h" -#include "df/buildreq.h" -#include "df/viewscreen_dwarfmodest.h" - -#include "modules/Gui.h" -#include "modules/Maps.h" -#include "modules/World.h" +#include "buildingplan.h" +#include "buildingtypekey.h" +#include "defaultitemfilters.h" +#include "plannedbuilding.h" -#include "Core.h" +#include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" -#include "../uicommon.h" -#include "../listcolumn.h" -#include "buildingplan.h" +#include "modules/World.h" -DFHACK_PLUGIN("buildingplan"); -#define PLUGIN_VERSION "2.0" -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(ui_build_selector); -REQUIRE_GLOBAL(world); // used in buildingplan library +#include "df/item.h" +#include "df/job_item.h" +#include "df/world.h" -#define MAX_MASK 10 -#define MAX_MATERIAL 21 +using std::map; +using std::string; +using std::unordered_map; +using std::vector; -bool show_help = false; -bool quickfort_mode = false; -bool all_enabled = false; -bool in_dummy_screen = false; -std::unordered_map planmode_enabled; +using namespace DFHack; -bool show_debugging = false; +DFHACK_PLUGIN("buildingplan"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); -void debug(const char *fmt, ...) -{ - if (!show_debugging) - return; +REQUIRE_GLOBAL(world); - color_ostream_proxy out(Core::getInstance().getConsole()); - out.print("DEBUG(buildingplan): "); - va_list args; - va_start(args, fmt); - out.vprint(fmt, args); - va_end(args); - out.print("\n"); +namespace DFHack { + DBG_DECLARE(buildingplan, status, DebugCategory::LINFO); + DBG_DECLARE(buildingplan, cycle, DebugCategory::LINFO); } -class ViewscreenChooseMaterial : public dfhack_viewscreen -{ -public: - ViewscreenChooseMaterial(ItemFilter &filter); - - void feed(set *input); +static const string CONFIG_KEY = string(plugin_name) + "/config"; +const string FILTER_CONFIG_KEY = string(plugin_name) + "/filter"; +const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; - void render(); - - std::string getFocusString() { return "buildingplan_choosemat"; } - -private: - ListColumn masks_column; - ListColumn materials_column; - int selected_column; - ItemFilter &filter; - - void addMaskEntry(df::dfhack_material_category &mask, const std::string &text) - { - auto entry = ListEntry(pad_string(text, MAX_MASK, false), mask); - if (filter.matches(mask)) - entry.selected = true; - - masks_column.add(entry); - } +int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); +} - void populateMasks() - { - masks_column.clear(); - df::dfhack_material_category mask; +static PersistentDataItem config; +// for use in counting available materials for the UI +static vector mat_cache; +static unordered_map, BuildingTypeKeyHash> job_item_cache; +static unordered_map cur_heat_safety; +static unordered_map cur_item_filters; +// building id -> PlannedBuilding +static unordered_map planned_buildings; +// vector id -> filter bucket -> queue of (building id, job_item index) +static Tasks tasks; + +// note that this just removes the PlannedBuilding. the tasks will get dropped +// as we discover them in the tasks queues and they fail to be found in planned_buildings. +// this "lazy" task cleaning algorithm works because there is no way to +// re-register a building once it has been removed -- if it has been booted out of +// planned_buildings, then it has either been built or desroyed. therefore there is +// no chance of duplicate tasks getting added to the tasks queues. +void PlannedBuilding::remove(color_ostream &out) { + DEBUG(status,out).print("removing persistent data for building %d\n", id); + World::DeletePersistentData(bld_config); + if (planned_buildings.count(id) > 0) + planned_buildings.erase(id); +} - mask.whole = 0; - mask.bits.stone = true; - addMaskEntry(mask, "Stone"); +static const int32_t CYCLE_TICKS = 600; // twice per game day +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - mask.whole = 0; - mask.bits.wood = true; - addMaskEntry(mask, "Wood"); +static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(status).print("calling buildingplan lua function: '%s'\n", fn_name); - mask.whole = 0; - mask.bits.metal = true; - addMaskEntry(mask, "Metal"); + CoreSuspender guard; - mask.whole = 0; - mask.bits.soap = true; - addMaskEntry(mask, "Soap"); + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); - masks_column.filterDisplay(); - } + if (!out) + out = &Core::getInstance().getConsole(); - void populateMaterials() - { - materials_column.clear(); - df::dfhack_material_category selected_category; - std::vector selected_masks = masks_column.getSelectedElems(); - if (selected_masks.size() == 1) - selected_category = selected_masks[0]; - else if (selected_masks.size() > 1) - return; - - df::world_raws &raws = world->raws; - for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; i++) - { - auto obj = raws.mat_table.builtin[i]; - if (obj) - { - MaterialInfo material; - material.decode(i, -1); - addMaterialEntry(selected_category, material, material.toString()); - } - } + return Lua::CallLuaModuleFunction(*out, L, "plugins.buildingplan", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} - for (size_t i = 0; i < raws.inorganics.size(); i++) - { - MaterialInfo material; - material.decode(0, i); - addMaterialEntry(selected_category, material, material.toString()); - } +static int get_num_filters(color_ostream &out, BuildingTypeKey key) { + int num_filters = 0; + if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, + [&](lua_State *L) { + Lua::Push(L, std::get<0>(key)); + Lua::Push(L, std::get<1>(key)); + Lua::Push(L, std::get<2>(key)); + }, + [&](lua_State *L) { + num_filters = lua_tonumber(L, -1); + })) { + return 0; + } + return num_filters; +} - decltype(selected_category) wood_flag; - wood_flag.bits.wood = true; - if (!selected_category.whole || selected_category.bits.wood) - { - for (size_t i = 0; i < raws.plants.all.size(); i++) - { - df::plant_raw *p = raws.plants.all[i]; - for (size_t j = 0; p->material.size() > 1 && j < p->material.size(); j++) - { - if (p->material[j]->id != "WOOD") - continue; - - MaterialInfo material; - material.decode(DFHack::MaterialInfo::PLANT_BASE+j, i); - auto name = material.toString(); - ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); - if (filter.matches(material)) - entry.selected = true; - - materials_column.add(entry); - } - } +static const vector & get_job_items(color_ostream &out, BuildingTypeKey key) { + if (job_item_cache.count(key)) + return job_item_cache[key]; + const int num_filters = get_num_filters(out, key); + auto &jitems = job_item_cache[key]; + for (int index = 0; index < num_filters; ++index) { + bool failed = false; + if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, + [&](lua_State *L) { + Lua::Push(L, std::get<0>(key)); + Lua::Push(L, std::get<1>(key)); + Lua::Push(L, std::get<2>(key)); + Lua::Push(L, index+1); + }, + [&](lua_State *L) { + df::job_item *jitem = Lua::GetDFObject(L, -1); + DEBUG(status,out).print("retrieving job_item for (%d, %d, %d) index=%d: %p\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), index, jitem); + if (!jitem) + failed = true; + else + jitems.emplace_back(jitem); + }) || failed) { + jitems.clear(); + break; } - materials_column.sort(); } + return jitems; +} - void addMaterialEntry(df::dfhack_material_category &selected_category, - MaterialInfo &material, std::string name) - { - if (!selected_category.whole || material.matches(selected_category)) - { - ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); - if (filter.matches(material)) - entry.selected = true; +static void cache_matched(int16_t type, int32_t index) { + static const df::dfhack_material_category building_material_categories( + df::dfhack_material_category::mask_glass | + df::dfhack_material_category::mask_metal | + df::dfhack_material_category::mask_soap | + df::dfhack_material_category::mask_stone | + df::dfhack_material_category::mask_wood + ); + + MaterialInfo mi; + mi.decode(type, index); + if (mi.matches(building_material_categories)) { + DEBUG(status).print("cached material: %s\n", mi.toString().c_str()); + mat_cache.emplace_back(mi); + } + else + TRACE(status).print("not matched: %s\n", mi.toString().c_str()); +} - materials_column.add(entry); +static void load_material_cache() { + df::world_raws &raws = world->raws; + for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; ++i) + if (raws.mat_table.builtin[i]) + cache_matched(i, -1); + + for (size_t i = 0; i < raws.inorganics.size(); i++) + cache_matched(0, i); + + for (size_t i = 0; i < raws.plants.all.size(); i++) { + df::plant_raw *p = raws.plants.all[i]; + if (p->material.size() <= 1) + continue; + for (size_t j = 0; j < p->material.size(); j++) { + if (p->material[j]->id == "WOOD") { + cache_matched(DFHack::MaterialInfo::PLANT_BASE+j, i); + break; + } } } - - void validateColumn() - { - set_to_limit(selected_column, 1); - } - - void resize(int32_t x, int32_t y) - { - dfhack_viewscreen::resize(x, y); - masks_column.resize(); - materials_column.resize(); - } -}; - -const DFHack::MaterialInfo &material_info_identity_fn(const DFHack::MaterialInfo &m) { return m; } - -ViewscreenChooseMaterial::ViewscreenChooseMaterial(ItemFilter &filter) - : filter(filter) -{ - selected_column = 0; - masks_column.setTitle("Type"); - masks_column.multiselect = true; - masks_column.allow_search = false; - masks_column.left_margin = 2; - materials_column.left_margin = MAX_MASK + 3; - materials_column.setTitle("Material"); - materials_column.multiselect = true; - - masks_column.changeHighlight(0); - - populateMasks(); - populateMaterials(); - - masks_column.selectDefaultEntry(); - materials_column.selectDefaultEntry(); - materials_column.changeHighlight(0); } -void ViewscreenChooseMaterial::feed(set *input) -{ - bool key_processed = false; - switch (selected_column) - { - case 0: - key_processed = masks_column.feed(input); - if (input->count(interface_key::SELECT)) - populateMaterials(); // Redo materials lists based on category selection - break; - case 1: - key_processed = materials_column.feed(input); - break; - } - - if (key_processed) - return; - - if (input->count(interface_key::LEAVESCREEN)) - { - input->clear(); - Screen::dismiss(this); - return; - } - if (input->count(interface_key::CUSTOM_SHIFT_C)) - { - filter.clear(); - masks_column.clearSelection(); - materials_column.clearSelection(); - populateMaterials(); - } - else if (input->count(interface_key::SEC_SELECT)) - { - // Convert list selections to material filters - filter.clearMaterialMask(); - - // Category masks - auto masks = masks_column.getSelectedElems(); - for (auto it = masks.begin(); it != masks.end(); ++it) - filter.addMaterialMask(it->whole); - - // Specific materials - auto materials = materials_column.getSelectedElems(); - std::vector materialInfos; - transform_(materials, materialInfos, material_info_identity_fn); - filter.setMaterials(materialInfos); - - Screen::dismiss(this); - } - else if (input->count(interface_key::STANDARDSCROLL_LEFT)) - { - --selected_column; - validateColumn(); - } - else if (input->count(interface_key::STANDARDSCROLL_RIGHT)) - { - selected_column++; - validateColumn(); - } - else if (enabler->tracking_on && enabler->mouse_lbut) - { - if (masks_column.setHighlightByMouse()) - selected_column = 0; - else if (materials_column.setHighlightByMouse()) - selected_column = 1; +static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { + if (cur_heat_safety.count(key)) + return cur_heat_safety.at(key); + return HEAT_SAFETY_ANY; +} - enabler->mouse_lbut = enabler->mouse_rbut = 0; - } +static DefaultItemFilters & get_item_filters(color_ostream &out, const BuildingTypeKey &key) { + if (cur_item_filters.count(key)) + return cur_item_filters.at(key); + cur_item_filters.emplace(key, DefaultItemFilters(out, key, get_job_items(out, key))); + return cur_item_filters.at(key); } -void ViewscreenChooseMaterial::render() -{ - if (Screen::isDismissed(this)) - return; +static command_result do_command(color_ostream &out, vector ¶meters); +void buildingplan_cycle(color_ostream &out, Tasks &tasks, + unordered_map &planned_buildings); - dfhack_viewscreen::render(); +static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb); - Screen::clear(); - Screen::drawBorder(" Building Material "); +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(status,out).print("initializing %s\n", plugin_name); - masks_column.display(selected_column == 0); - materials_column.display(selected_column == 1); + // provide a configuration interface for the plugin + commands.push_back(PluginCommand( + plugin_name, + "Plan building placement before you have materials.", + do_command)); - int32_t y = gps->dimy - 3; - int32_t x = 2; - OutputHotkeyString(x, y, "Toggle", interface_key::SELECT); - x += 3; - OutputHotkeyString(x, y, "Save", interface_key::SEC_SELECT); - x += 3; - OutputHotkeyString(x, y, "Clear", interface_key::CUSTOM_SHIFT_C); - x += 3; - OutputHotkeyString(x, y, "Cancel", interface_key::LEAVESCREEN); + return CR_OK; } -//START Viewscreen Hook -static bool is_planmode_enabled(BuildingTypeKey key) -{ - return planmode_enabled[key] || quickfort_mode || all_enabled; +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(status,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + } else { + DEBUG(status,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; } -static std::string get_item_label(const BuildingTypeKey &key, int item_idx) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 5) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "get_item_label")) - return "Failed push"; - - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - Lua::Push(L, item_idx); +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(status,out).print("shutting down %s\n", plugin_name); - if (!Lua::SafeCall(out, L, 4, 1)) - return "Failed call"; - - const char *s = lua_tostring(L, -1); - if (!s) - return "No string"; - - return s; + return CR_OK; } -static bool item_can_be_improved(const BuildingTypeKey &key, int item_idx) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 5) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "item_can_be_improved")) - return false; - - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - Lua::Push(L, item_idx); +static void validate_config(color_ostream &out, bool verbose = false) { + if (get_config_bool(config, CONFIG_BLOCKS) + || get_config_bool(config, CONFIG_BOULDERS) + || get_config_bool(config, CONFIG_LOGS) + || get_config_bool(config, CONFIG_BARS)) + return; - if (!Lua::SafeCall(out, L, 4, 1)) - return false; + if (verbose) + out.printerr("all contruction materials disabled; resetting config\n"); - return lua_toboolean(L, -1); + set_config_bool(config, CONFIG_BLOCKS, true); + set_config_bool(config, CONFIG_BOULDERS, true); + set_config_bool(config, CONFIG_LOGS, true); + set_config_bool(config, CONFIG_BARS, false); } -static bool construct_planned_building() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - - CoreSuspendClaimer suspend; - Lua::StackUnwinder top(L); - - if (!(lua_checkstack(L, 1) && - Lua::PushModulePublic(out, L, "plugins.buildingplan", - "construct_buildings_from_ui_state") && - Lua::SafeCall(out, L, 0, 1))) - { - return false; - } - - // register all returned buildings with planner - lua_pushnil(L); - while (lua_next(L, -2) != 0) - { - auto bld = Lua::GetDFObject(L, -1); - if (!bld) - { - out.printerr( - "buildingplan: construct_buildings_from_ui_state() failed\n"); - return false; +static void clear_state(color_ostream &out) { + call_buildingplan_lua(&out, "signal_reset"); + call_buildingplan_lua(&out, "reload_cursors"); + planned_buildings.clear(); + tasks.clear(); + cur_heat_safety.clear(); + cur_item_filters.clear(); + for (auto &entry : job_item_cache ) { + for (auto &jitem : entry.second) { + delete jitem; } - - planner.addPlannedBuilding(bld); - lua_pop(L, 1); } - - return true; + job_item_cache.clear(); + mat_cache.clear(); } -static void show_global_settings_dialog() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); +DFhackCExport command_result plugin_load_data (color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); - if (!lua_checkstack(L, 2) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "show_global_settings_dialog")) - { - debug("Failed to push the module"); - return; + if (!config.isValid()) { + DEBUG(status,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); } + validate_config(out); - lua_newtable(L); - int ctable = lua_gettop(L); - Lua::SetField(L, quickfort_mode, ctable, "quickfort_mode"); - Lua::SetField(L, all_enabled, ctable, "all_enabled"); + DEBUG(status,out).print("loading persisted state\n"); + clear_state(out); - for (auto & setting : planner.getGlobalSettings()) - { - Lua::SetField(L, setting.second, ctable, setting.first.c_str()); - } + load_material_cache(); - if (!Lua::SafeCall(out, L, 1, 0)) - { - debug("Failed call to show_global_settings_dialog"); - return; + vector filter_configs; + World::GetPersistentData(&filter_configs, FILTER_CONFIG_KEY); + for (auto &cfg : filter_configs) { + BuildingTypeKey key = DefaultItemFilters::getKey(cfg); + cur_item_filters.emplace(key, DefaultItemFilters(out, cfg, get_job_items(out, key))); } -} - -static bool is_automaterial_enabled() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - if (!(lua_checkstack(L, 1) && - Lua::PushModulePublic(out, L, "plugins.automaterial", "isEnabled") && - Lua::SafeCall(out, L, 0, 1))) - { - return false; + vector building_configs; + World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); + const size_t num_building_configs = building_configs.size(); + for (size_t idx = 0; idx < num_building_configs; ++idx) { + PlannedBuilding pb(out, building_configs[idx]); + df::building *bld = df::building::find(pb.id); + if (!bld) { + INFO(status,out).print("building %d no longer exists; skipping\n", pb.id); + pb.remove(out); + continue; + } + BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + if (pb.item_filters.size() != get_item_filters(out, key).getItemFilters().size()) { + WARN(status).print("loaded state for building %d doesn't match world\n", pb.id); + pb.remove(out); + continue; + } + registerPlannedBuilding(out, pb); } - return lua_toboolean(L, -1); -} - -static bool is_automaterial_managed(df::building_type type, int16_t subtype) -{ - return is_automaterial_enabled() - && type == df::building_type::Construction - && subtype < df::construction_type::TrackN; + return CR_OK; } -struct buildingplan_query_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - - // no non-static fields allowed (according to VTableInterpose.h) - static df::building *bld; - static PlannedBuilding *pb; - static int filter_count; - static int filter_idx; - - // logic is reversed since we're starting at the last filter - bool hasNextFilter() const { return filter_idx > 0; } - bool hasPrevFilter() const { return filter_idx + 1 < filter_count; } - - bool isInPlannedBuildingQueryMode() - { - return (plotinfo->main.mode == df::ui_sidebar_mode::QueryBuilding || - plotinfo->main.mode == df::ui_sidebar_mode::BuildingItems) && - planner.getPlannedBuilding(world->selected_building); +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == SC_WORLD_UNLOADED) { + DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); + clear_state(out); } + return CR_OK; +} - // reinit static fields when selected building changes - void initStatics() - { - df::building *cur_bld = world->selected_building; - if (bld != cur_bld) - { - bld = cur_bld; - pb = planner.getPlannedBuilding(bld); - filter_count = pb->getFilters().size(); - filter_idx = filter_count - 1; - } - } +static bool cycle_requested = false; - static void invalidateStatics() - { - bld = NULL; - } +static void do_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; + cycle_requested = false; - bool handleInput(set *input) - { - if (!isInPlannedBuildingQueryMode() || Gui::inRenameBuilding()) - return false; - - initStatics(); - - if (input->count(interface_key::SUSPENDBUILDING)) - return true; // Don't unsuspend planned buildings - if (input->count(interface_key::DESTROYBUILDING)) - { - // remove persistent data - pb->remove(); - // still allow the building to be removed - return false; - } + buildingplan_cycle(out, tasks, planned_buildings); + call_buildingplan_lua(&out, "signal_reset"); +} - // ctrl+Right - if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter()) - --filter_idx; - // ctrl+Left - else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter()) - ++filter_idx; - else - return false; - return true; - } +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (!Core::getInstance().isWorldLoaded()) + return CR_OK; - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } + if (is_enabled && + (cycle_requested || world->frame_counter - cycle_timestamp >= CYCLE_TICKS)) + do_cycle(out); + return CR_OK; +} - static bool is_filter_satisfied(df::building *bld, int filter_idx) - { - if (!bld - || bld->jobs.size() < 1 - || int(bld->jobs[0]->job_items.size()) <= filter_idx) - return false; +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; - // if all items for this filter are attached, the quantity will be 0 - return bld->jobs[0]->job_items[filter_idx]->quantity == 0; + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot configure %s without a loaded world.\n", plugin_name); + return CR_FAILURE; } - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - if (!isInPlannedBuildingQueryMode()) - return; - - initStatics(); - - // Hide suspend toggle option - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 20; - Screen::Pen pen(' ', COLOR_BLACK); - Screen::fillRect(pen, x, y, dims.menu_x2, y); - - bool attached = is_filter_satisfied(pb->getBuilding(), filter_idx); - - auto & filter = pb->getFilters()[filter_idx]; - y = 24; - std::string item_label = - stl_sprintf("Item %d of %d (%s)", filter_count - filter_idx, filter_count, attached ? "attached" : "pending"); - OutputString(COLOR_WHITE, x, y, "Planned Building Filter", true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, item_label.c_str(), true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, get_item_label(toBuildingTypeKey(bld), filter_idx).c_str(), true, left_margin); - ++y; - if (item_can_be_improved(toBuildingTypeKey(bld), filter_idx)) - { - OutputString(COLOR_BROWN, x, y, "Min Quality: ", false, left_margin); - OutputString(COLOR_BLUE, x, y, filter.getMinQuality(), true, left_margin); - OutputString(COLOR_BROWN, x, y, "Max Quality: ", false, left_margin); - OutputString(COLOR_BLUE, x, y, filter.getMaxQuality(), true, left_margin); - if (filter.getDecoratedOnly()) - OutputString(COLOR_BLUE, x, y, "Decorated Only", true, left_margin); - } - - OutputString(COLOR_BROWN, x, y, "Materials:", true, left_margin); - auto filters = filter.getMaterials(); - for (auto it = filters.begin(); it != filters.end(); ++it) - OutputString(COLOR_BLUE, x, y, "*" + *it, true, left_margin); - - ++y; - if (hasPrevFilter()) - OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - if (hasNextFilter()) - OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); + bool show_help = false; + if (!call_buildingplan_lua(&out, "parse_commandline", parameters.size(), 1, + [&](lua_State *L) { + for (const string ¶m : parameters) + Lua::Push(L, param); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; } -}; -df::building * buildingplan_query_hook::bld; -PlannedBuilding * buildingplan_query_hook::pb; -int buildingplan_query_hook::filter_count; -int buildingplan_query_hook::filter_idx; - -struct buildingplan_place_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - - // no non-static fields allowed (according to VTableInterpose.h) - static BuildingTypeKey key; - static std::vector::reverse_iterator filter_rbegin; - static std::vector::reverse_iterator filter_rend; - static std::vector::reverse_iterator filter; - static int filter_count; - static int filter_idx; + return show_help ? CR_WRONG_USAGE : CR_OK; +} - bool hasNextFilter() const { return filter + 1 != filter_rend; } - bool hasPrevFilter() const { return filter != filter_rbegin; } +///////////////////////////////////////////////////// +// Lua API +// core will already be suspended when coming in through here +// + +static string getBucket(const df::job_item & ji, const PlannedBuilding & pb, int idx) { + if (idx < 0 || (size_t)idx < pb.item_filters.size()) + return "INVALID"; + + std::ostringstream ser; + + // put elements in front that significantly affect the difficulty of matching + // the filter. ensure the lexicographically "less" value is the pickier value. + const ItemFilter & item_filter = pb.item_filters[idx]; + + if (item_filter.getDecoratedOnly()) + ser << "Da"; + else + ser << "Db"; + + if (ji.flags2.bits.magma_safe || pb.heat_safety == HEAT_SAFETY_MAGMA) + ser << "Ha"; + else if (ji.flags2.bits.fire_safe || pb.heat_safety == HEAT_SAFETY_FIRE) + ser << "Hb"; + else + ser << "Hc"; + + size_t num_materials = item_filter.getMaterials().size(); + if (num_materials == 0 || num_materials >= 9 || item_filter.getMaterialMask().whole) + ser << "M9"; + else + ser << "M" << num_materials; + + // pull out and serialize only known relevant fields. if we miss a few, then + // the filter bucket will be slighly less specific than it could be, but + // that's probably ok. we'll just end up bucketing slightly different items + // together. this is only a problem if the different filter at the front of + // the queue doesn't match any available items and blocks filters behind it + // that could be matched. + ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':' + << ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole + << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' + << ji.metal_ore << ':' << ji.has_tool_use; + + ser << ':' << item_filter.serialize(); + + return ser.str(); +} - bool isInPlannedBuildingPlacementMode() - { - return plotinfo->main.mode == ui_sidebar_mode::Build && - df::global::ui_build_selector && - df::global::ui_build_selector->stage < 2 && - planner.isPlannableBuilding(toBuildingTypeKey(ui_build_selector)); - } +// get a list of item vectors that we should search for matches +vector getVectorIds(color_ostream &out, const df::job_item *job_item) { + std::vector ret; - // reinit static fields when selected building type changes - void initStatics() + // if the filter already has the vector_id set to something specific, use it + if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) { - BuildingTypeKey cur_key = toBuildingTypeKey(ui_build_selector); - if (key != cur_key) - { - key = cur_key; - auto wrapper = planner.getItemFilters(key); - filter_rbegin = wrapper.rbegin(); - filter_rend = wrapper.rend(); - filter = filter_rbegin; - filter_count = wrapper.get().size(); - filter_idx = filter_count - 1; - } + DEBUG(status,out).print("using vector_id from job_item: %s\n", + ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); + ret.push_back(job_item->vector_id); + return ret; } - static void invalidateStatics() + // if the filer is for building material, refer to our global settings for + // which vectors to search + if (job_item->flags2.bits.building_material) { - key = BuildingTypeKey(); + if (get_config_bool(config, CONFIG_BLOCKS)) + ret.push_back(df::job_item_vector_id::BLOCKS); + if (get_config_bool(config, CONFIG_BOULDERS)) + ret.push_back(df::job_item_vector_id::BOULDER); + if (get_config_bool(config, CONFIG_LOGS)) + ret.push_back(df::job_item_vector_id::WOOD); + if (get_config_bool(config, CONFIG_BARS)) + ret.push_back(df::job_item_vector_id::BAR); } - bool handleInput(set *input) - { - if (!isInPlannedBuildingPlacementMode()) - { - show_help = false; - return false; - } - - initStatics(); - - if (in_dummy_screen) - { - if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT) - || input->count(interface_key::LEAVESCREEN)) - { - in_dummy_screen = false; - // pass LEAVESCREEN up to parent view - input->clear(); - input->insert(interface_key::LEAVESCREEN); - return false; - } - return true; - } - - if (input->count(interface_key::CUSTOM_P) || - input->count(interface_key::CUSTOM_G) || - input->count(interface_key::CUSTOM_D) || - input->count(interface_key::CUSTOM_Q) || - input->count(interface_key::CUSTOM_W) || - input->count(interface_key::CUSTOM_A) || - input->count(interface_key::CUSTOM_S) || - input->count(interface_key::CUSTOM_M)) - { - show_help = true; - } - - if (!quickfort_mode && !all_enabled - && input->count(interface_key::CUSTOM_SHIFT_P)) - { - planmode_enabled[key] = !planmode_enabled[key]; - if (!is_planmode_enabled(key)) - Gui::refreshSidebar(); - return true; - } - if (input->count(interface_key::CUSTOM_SHIFT_G)) - { - show_global_settings_dialog(); - return true; - } - - if (!is_planmode_enabled(key)) - return false; - - // if automaterial is enabled, let it handle building allocation and - // registration with planner - if (input->count(interface_key::SELECT) && - !is_automaterial_managed(ui_build_selector->building_type, - ui_build_selector->building_subtype)) - { - if (ui_build_selector->errors.size() == 0 && construct_planned_building()) - { - Gui::refreshSidebar(); - if (quickfort_mode) - in_dummy_screen = true; - } - return true; - } - - - - if (input->count(interface_key::CUSTOM_SHIFT_M)) - Screen::show(dts::make_unique(*filter), plugin_self); + // fall back to IN_PLAY if no other vector was appropriate + if (ret.empty()) + ret.push_back(df::job_item_vector_id::IN_PLAY); + return ret; +} - if (item_can_be_improved(key, filter_idx)) - { - if (input->count(interface_key::CUSTOM_SHIFT_Q)) - filter->decMinQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_W)) - filter->incMinQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_A)) - filter->decMaxQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_S)) - filter->incMaxQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_D)) - filter->toggleDecoratedOnly(); - } +static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { + df::building * bld = pb.getBuildingIfValidOrRemoveIfNot(out); + if (!bld) + return false; - // ctrl+Right - if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter()) - { - ++filter; - --filter_idx; - } - // ctrl+Left - else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter()) - { - --filter; - ++filter_idx; - } - else - return false; - return true; + if (bld->jobs.size() != 1) { + DEBUG(status,out).print("unexpected number of jobs: want 1, got %zu\n", bld->jobs.size()); + return false; } - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); + auto job_items = bld->jobs[0]->job_items; + if (isJobReady(out, job_items)) { + // all items are already attached + finalizeBuilding(out, bld); + return true; } - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - initStatics(); - - bool plannable = isInPlannedBuildingPlacementMode(); - if (plannable && is_planmode_enabled(key)) - { - if (ui_build_selector->stage < 1) - // No materials but turn on cursor - ui_build_selector->stage = 1; - - for (auto iter = ui_build_selector->errors.begin(); - iter != ui_build_selector->errors.end();) - { - // FIXME Hide bags - if (((*iter)->find("Needs") != string::npos - && **iter != "Needs adjacent wall") - || (*iter)->find("No access") != string::npos) - iter = ui_build_selector->errors.erase(iter); - else - ++iter; + int num_job_items = job_items.size(); + int32_t id = bld->id; + for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { + auto job_item = job_items[job_item_idx]; + auto bucket = getBucket(*job_item, pb, job_item_idx); + + // if there are multiple vector_ids, schedule duplicate tasks. after + // the correct number of items are matched, the extras will get popped + // as invalid + for (auto vector_id : pb.vector_ids[job_item_idx]) { + for (int item_num = 0; item_num < job_item->quantity; ++item_num) { + tasks[vector_id][bucket].emplace_back(id, job_item_idx); + DEBUG(status,out).print("added task: %s/%s/%d,%d; " + "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket.c_str(), id, job_item_idx, tasks.size(), + tasks[vector_id].size(), tasks[vector_id][bucket].size()); } } - - INTERPOSE_NEXT(render)(); - - if (!plannable) - return; - - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - - if (in_dummy_screen) - { - Screen::Pen pen(' ',COLOR_BLACK); - int y = dims.y1 + 1; - Screen::fillRect(pen, x, y, dims.menu_x2, y + 20); - - ++y; - - OutputString(COLOR_BROWN, x, y, - "Placeholder for legacy Quickfort. This screen is not required for DFHack native quickfort.", - true, left_margin); - OutputString(COLOR_WHITE, x, y, "Enter, Shift-Enter or Esc", true, left_margin); - return; - } - - int y = 23; - - if (is_automaterial_managed(ui_build_selector->building_type, - ui_build_selector->building_subtype)) - { - // avoid conflict with the automaterial plugin UI - y = 36; - } - - if (show_help) - { - OutputString(COLOR_BROWN, x, y, "Note: "); - OutputString(COLOR_WHITE, x, y, "Use Shift-Keys here", true, left_margin); - } - - OutputHotkeyString(x, y, "Planning Mode", interface_key::CUSTOM_SHIFT_P); - OutputString(COLOR_WHITE, x, y, ": "); - if (quickfort_mode) - OutputString(COLOR_YELLOW, x, y, "Quickfort", true, left_margin); - else if (all_enabled) - OutputString(COLOR_YELLOW, x, y, "All", true, left_margin); - else if (planmode_enabled[key]) - OutputString(COLOR_GREEN, x, y, "On", true, left_margin); - else - OutputString(COLOR_GREY, x, y, "Off", true, left_margin); - OutputHotkeyString(x, y, "Global Settings", interface_key::CUSTOM_SHIFT_G, - true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - - if (!is_planmode_enabled(key)) - return; - - y += 2; - std::string title = - stl_sprintf("Filter for Item %d of %d:", - filter_count - filter_idx, filter_count); - OutputString(COLOR_WHITE, x, y, title.c_str(), true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, get_item_label(key, filter_idx).c_str(), true, left_margin); - - if (item_can_be_improved(key, filter_idx)) - { - OutputHotkeyString(x, y, "Min Quality: ", "QW", false, 0, COLOR_WHITE, COLOR_LIGHTRED); - OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin); - - OutputHotkeyString(x, y, "Max Quality: ", "AS", false, 0, COLOR_WHITE, COLOR_LIGHTRED); - OutputString(COLOR_BROWN, x, y, filter->getMaxQuality(), true, left_margin); - - OutputToggleString(x, y, "Decorated Only", interface_key::CUSTOM_SHIFT_D, - filter->getDecoratedOnly(), true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - } - - OutputHotkeyString(x, y, "Material Filter:", interface_key::CUSTOM_SHIFT_M, true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); - auto filter_descriptions = filter->getMaterials(); - for (auto it = filter_descriptions.begin(); - it != filter_descriptions.end(); ++it) - OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin); - - y += 2; - if (hasPrevFilter()) - OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); - if (hasNextFilter()) - OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); } -}; -BuildingTypeKey buildingplan_place_hook::key; -std::vector::reverse_iterator buildingplan_place_hook::filter_rbegin; -std::vector::reverse_iterator buildingplan_place_hook::filter_rend; -std::vector::reverse_iterator buildingplan_place_hook::filter; -int buildingplan_place_hook::filter_count; -int buildingplan_place_hook::filter_idx; + // suspend jobs + for (auto job : bld->jobs) + job->flags.bits.suspend = true; -struct buildingplan_room_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; + // add the planned buildings to our register + planned_buildings.emplace(bld->id, pb); - std::vector getNoblePositionOfSelectedBuildingOwner() - { - std::vector np; - if (plotinfo->main.mode != df::ui_sidebar_mode::QueryBuilding || - !world->selected_building || - !world->selected_building->owner) - { - return np; - } - - switch (world->selected_building->getType()) - { - case building_type::Bed: - case building_type::Chair: - case building_type::Table: - break; - default: - return np; - } + return true; +} - return getUniqueNoblePositions(world->selected_building->owner); - } +static string get_desc_string(color_ostream &out, df::job_item *jitem, + const vector &vec_ids) { + vector descs; + for (auto &vec_id : vec_ids) { + df::job_item jitem_copy = *jitem; + jitem_copy.vector_id = vec_id; + call_buildingplan_lua(&out, "get_desc", 1, 1, + [&](lua_State *L) { Lua::Push(L, &jitem_copy); }, + [&](lua_State *L) { + descs.emplace_back(lua_tostring(L, -1)); }); + } + return join_strings(" or ", descs); +} - bool isInNobleRoomQueryMode() - { - if (getNoblePositionOfSelectedBuildingOwner().size() > 0) - return canReserveRoom(world->selected_building); - else - return false; - } +static void printStatus(color_ostream &out) { + DEBUG(status,out).print("entering buildingplan_printStatus\n"); + out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); + out.print("Current settings:\n"); + out.print(" use blocks: %s\n", get_config_bool(config, CONFIG_BLOCKS) ? "yes" : "no"); + out.print(" use boulders: %s\n", get_config_bool(config, CONFIG_BOULDERS) ? "yes" : "no"); + out.print(" use logs: %s\n", get_config_bool(config, CONFIG_LOGS) ? "yes" : "no"); + out.print(" use bars: %s\n", get_config_bool(config, CONFIG_BARS) ? "yes" : "no"); + out.print("\n"); - bool handleInput(set *input) - { - if (!isInNobleRoomQueryMode()) - return false; - - if (Gui::inRenameBuilding()) - return false; - auto np = getNoblePositionOfSelectedBuildingOwner(); - df::interface_key last_token = get_string_key(input); - if (last_token >= Screen::charToKey('1') - && last_token <= Screen::charToKey('9')) - { - size_t index = last_token - Screen::charToKey('1'); - if (index >= np.size()) - return false; - roomMonitor.toggleRoomForPosition(world->selected_building->id, np.at(index).position->code); - return true; + map counts; + int32_t total = 0; + for (auto &entry : planned_buildings) { + auto &pb = entry.second; + auto bld = pb.getBuildingIfValidOrRemoveIfNot(out); + if (!bld || bld->jobs.size() != 1) + continue; + auto &job_items = bld->jobs[0]->job_items; + if (job_items.size() != pb.vector_ids.size()) + continue; + int job_item_idx = 0; + for (auto &vec_ids : pb.vector_ids) { + auto &jitem = job_items[job_item_idx++]; + int32_t quantity = jitem->quantity; + if (quantity) { + counts[get_desc_string(out, jitem, vec_ids)] += quantity; + total += quantity; + } } - - return false; } - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); + if (planned_buildings.size()) { + out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", + total, planned_buildings.size()); + for (auto &count : counts) + out.print(" %3d %s%s\n", count.second, count.first.c_str(), count.second == 1 ? "" : "s"); + } else { + out.print("Currently no planned buildings\n"); } + out.print("\n"); +} - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - if (!isInNobleRoomQueryMode()) - return; - - auto np = getNoblePositionOfSelectedBuildingOwner(); - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 24; - OutputString(COLOR_BROWN, x, y, "DFHack", true, left_margin); - OutputString(COLOR_WHITE, x, y, "Auto-allocate to:", true, left_margin); - for (size_t i = 0; i < np.size() && i < 9; i++) - { - bool enabled = - roomMonitor.getReservedNobleCode(world->selected_building->id) - == np[i].position->code; - OutputToggleString(x, y, np[i].position->name[0].c_str(), - int_to_string(i+1).c_str(), enabled, true, left_margin); - } +static bool setSetting(color_ostream &out, string name, bool value) { + DEBUG(status,out).print("entering setSetting (%s -> %s)\n", name.c_str(), value ? "true" : "false"); + if (name == "blocks") + set_config_bool(config, CONFIG_BLOCKS, value); + else if (name == "boulders") + set_config_bool(config, CONFIG_BOULDERS, value); + else if (name == "logs") + set_config_bool(config, CONFIG_LOGS, value); + else if (name == "bars") + set_config_bool(config, CONFIG_BARS, value); + else { + out.printerr("unrecognized setting: '%s'\n", name.c_str()); + return false; } -}; - -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, render); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, render); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, render); - -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -static bool setSetting(std::string name, bool value); -static bool isTrue(std::string val) -{ - val = toLower(val); - return val == "on" || val == "true" || val == "y" || val == "yes" - || val == "1"; + validate_config(out, true); + call_buildingplan_lua(&out, "signal_reset"); + return true; } -static command_result buildingplan_cmd(color_ostream &out, vector & parameters) -{ - if (parameters.empty()) - return CR_OK; - - std::string cmd = toLower(parameters[0]); +static bool isPlannableBuilding(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom) { + DEBUG(status,out).print("entering isPlannableBuilding\n"); + return get_num_filters(out, BuildingTypeKey(type, subtype, custom)) >= 1; +} - if (cmd.size() >= 1 && cmd[0] == 'v') - { - out.print("buildingplan version: %s\n", PLUGIN_VERSION); - } - else if (parameters.size() >= 2 && cmd == "debug") - { - show_debugging = isTrue(parameters[1]); - out.print("buildingplan debugging: %s\n", - show_debugging ? "enabled" : "disabled"); - } - else if (cmd == "set") - { - if (!is_enabled) - { - out.printerr( - "ERROR: buildingplan must be enabled before you can" - " read or set buildingplan global settings."); - return CR_FAILURE; - } +static bool isPlannedBuilding(color_ostream &out, df::building *bld) { + TRACE(status,out).print("entering isPlannedBuilding\n"); + return bld && planned_buildings.count(bld->id); +} - if (!DFHack::Core::getInstance().isMapLoaded()) - { - out.printerr( - "ERROR: A map must be loaded before you can read or set" - "buildingplan global settings. Try adding your" - "'buildingplan set' commands to the onMapLoad.init file.\n"); - return CR_FAILURE; - } +static bool addPlannedBuilding(color_ostream &out, df::building *bld) { + DEBUG(status,out).print("entering addPlannedBuilding\n"); + if (!bld || planned_buildings.count(bld->id) + || !isPlannableBuilding(out, bld->getType(), bld->getSubtype(), + bld->getCustomType())) + return false; + BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key).getItemFilters()); + return registerPlannedBuilding(out, pb); +} - if (parameters.size() == 1) - { - // display current settings - out.print("active settings:\n"); +static void doCycle(color_ostream &out) { + DEBUG(status,out).print("entering doCycle\n"); + do_cycle(out); +} - out.print(" all_enabled = %s\n", all_enabled ? "true" : "false"); - for (auto & setting : planner.getGlobalSettings()) - { - out.print(" %s = %s\n", setting.first.c_str(), - setting.second ? "true" : "false"); - } +static void scheduleCycle(color_ostream &out) { + DEBUG(status,out).print("entering scheduleCycle\n"); + cycle_requested = true; +} - out.print(" quickfort_mode = %s\n", - quickfort_mode ? "true" : "false"); - } - else if (parameters.size() == 3) - { - // set a setting - std::string setting = toLower(parameters[1]); - bool val = isTrue(parameters[2]); - if (!setSetting(setting, val)) - { - out.printerr("ERROR: invalid parameter: '%s'\n", - parameters[1].c_str()); +static int scanAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, + int32_t custom, int index, vector *item_ids = NULL) { + DEBUG(status,out).print( + "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + BuildingTypeKey key(type, subtype, custom); + HeatSafety heat = get_heat_safety_filter(key); + auto &job_items = get_job_items(out, key); + if (index < 0 || job_items.size() <= (size_t)index) + return 0; + auto &item_filters = get_item_filters(out, key).getItemFilters(); + + auto &jitem = job_items[index]; + auto vector_ids = getVectorIds(out, jitem); + + int count = 0; + for (auto vector_id : vector_ids) { + auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); + for (auto &item : df::global::world->items.other[other_id]) { + if (itemPassesScreen(item) && matchesFilters(item, jitem, heat, item_filters[index])) { + if (item_ids) + item_ids->emplace_back(item->id); + ++count; } } - else - { - out.printerr("ERROR: invalid syntax\n"); - } } - return CR_OK; + DEBUG(status,out).print("found matches %d\n", count); + return count; } -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (!gps) - return CR_FAILURE; +static int getAvailableItems(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + vector item_ids; + scanAvailableItems(*out, type, subtype, custom, index, &item_ids); + Lua::PushVector(L, item_ids); + return 1; +} - if (enable != is_enabled) - { - if (DFHack::Core::getInstance().isMapLoaded()) - planner.reset(); +static int getGlobalSettings(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering getGlobalSettings\n"); + map settings; + settings.emplace("blocks", get_config_bool(config, CONFIG_BLOCKS)); + settings.emplace("logs", get_config_bool(config, CONFIG_LOGS)); + settings.emplace("boulders", get_config_bool(config, CONFIG_BOULDERS)); + settings.emplace("bars", get_config_bool(config, CONFIG_BARS)); + Lua::Push(L, settings); + return 1; +} - if (!INTERPOSE_HOOK(buildingplan_query_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_place_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_room_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_query_hook, render).apply(enable) || - !INTERPOSE_HOOK(buildingplan_place_hook, render).apply(enable) || - !INTERPOSE_HOOK(buildingplan_room_hook, render).apply(enable)) - return CR_FAILURE; +static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print( + "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + return scanAvailableItems(out, type, subtype, custom, index); +} - is_enabled = enable; +static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + TRACE(status,out).print("entering hasFilter\n"); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(out, key); + for (auto &filter : filters.getItemFilters()) { + if (!filter.isEmpty()) + return true; } + return false; +} - return CR_OK; +static void clearFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + TRACE(status,out).print("entering clearFilter\n"); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(out, key); + if (index < 0 || filters.getItemFilters().size() <= (size_t)index) + return; + filters.setItemFilter(out, ItemFilter(), index); + call_buildingplan_lua(&out, "signal_reset"); } -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - commands.push_back( - PluginCommand("buildingplan", - "Plan building construction before you have materials.", - buildingplan_cmd)); +static void setMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index, string filter) { + DEBUG(status,out).print("entering setMaterialFilter\n"); + call_buildingplan_lua(&out, "signal_reset"); +} - return CR_OK; +static int getMaterialFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + map counts_per_material; + Lua::Push(L, counts_per_material); + return 1; } -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - buildingplan_place_hook::invalidateStatics(); - buildingplan_query_hook::invalidateStatics(); - planner.reset(); - roomMonitor.reset(out); - break; - default: - break; - } +static void setHeatSafetyFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int heat) { + DEBUG(status,out).print("entering setHeatSafetyFilter\n"); + BuildingTypeKey key(type, subtype, custom); + if (heat == HEAT_SAFETY_FIRE || heat == HEAT_SAFETY_MAGMA) + cur_heat_safety[key] = (HeatSafety)heat; + else + cur_heat_safety.erase(key); + call_buildingplan_lua(&out, "signal_reset"); +} - return CR_OK; +static int getHeatSafetyFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + DEBUG(status,*out).print( + "entering getHeatSafetyFilter building_type=%d subtype=%d custom=%d\n", + type, subtype, custom); + BuildingTypeKey key(type, subtype, custom); + HeatSafety heat = get_heat_safety_filter(key); + Lua::Push(L, heat); + return 1; } -static bool is_paused() -{ - return World::ReadPauseState() || - plotinfo->main.mode > df::ui_sidebar_mode::Squads || - !strict_virtual_cast(Gui::getCurViewscreen(true)); +static void setQualityFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index, + int decorated, int min_quality, int max_quality) { + DEBUG(status,out).print("entering setQualityFilter\n"); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(out, key).getItemFilters(); + if (index < 0 || filters.size() <= (size_t)index) + return; + ItemFilter filter = filters[index]; + filter.setDecoratedOnly(decorated != 0); + filter.setMinQuality(min_quality); + filter.setMaxQuality(max_quality); + get_item_filters(out, key).setItemFilter(out, filter, index); + call_buildingplan_lua(&out, "signal_reset"); } -static bool cycle_requested = false; +static int getQualityFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getQualityFilter building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(*out, key).getItemFilters(); + if (index < 0 || filters.size() <= (size_t)index) + return 0; + auto &filter = filters[index]; + map ret; + ret.emplace("decorated", filter.getDecoratedOnly()); + ret.emplace("min_quality", filter.getMinQuality()); + ret.emplace("max_quality", filter.getMaxQuality()); + Lua::Push(L, ret); + return 1; +} -#define DAY_TICKS 1200 -DFhackCExport command_result plugin_onupdate(color_ostream &) -{ - if (Maps::IsValid() && !is_paused() - && (cycle_requested || world->frame_counter % (DAY_TICKS/2) == 0)) - { - planner.doCycle(); - roomMonitor.doCycle(); - cycle_requested = false; - } +static bool validate_pb(color_ostream &out, df::building *bld, int index) { + if (!isPlannedBuilding(out, bld) || bld->jobs.size() != 1) + return false; - return CR_OK; -} + auto &job_items = bld->jobs[0]->job_items; + if ((int)job_items.size() <= index) + return false; -DFhackCExport command_result plugin_shutdown(color_ostream &) -{ - return CR_OK; + PlannedBuilding &pb = planned_buildings.at(bld->id); + if ((int)pb.vector_ids.size() <= index) + return false; + + return true; } -// Lua API section +static string getDescString(color_ostream &out, df::building *bld, int index) { + DEBUG(status,out).print("entering getDescString\n"); + if (!validate_pb(out, bld, index)) + return 0; -static bool isPlanModeEnabled(df::building_type type, - int16_t subtype, - int32_t custom) { - return is_planmode_enabled(toBuildingTypeKey(type, subtype, custom)); + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &jitem = bld->jobs[0]->job_items[index]; + return get_desc_string(out, jitem, pb.vector_ids[index]); } -static bool isPlannableBuilding(df::building_type type, - int16_t subtype, - int32_t custom) { - return planner.isPlannableBuilding( - toBuildingTypeKey(type, subtype, custom)); -} +static int getQueuePosition(color_ostream &out, df::building *bld, int index) { + DEBUG(status,out).print("entering getQueuePosition\n"); + if (!validate_pb(out, bld, index)) + return 0; -static bool isPlannedBuilding(df::building *bld) { - return !!planner.getPlannedBuilding(bld); -} + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &job_item = bld->jobs[0]->job_items[index]; -static void addPlannedBuilding(df::building *bld) { - planner.addPlannedBuilding(bld); -} + if (job_item->quantity <= 0) + return 0; -static void doCycle() { - planner.doCycle(); -} + int min_pos = -1; + for (auto &vec_id : pb.vector_ids[index]) { + if (!tasks.count(vec_id)) + continue; + auto &buckets = tasks.at(vec_id); + string bucket_id = getBucket(*job_item, pb, index); + if (!buckets.count(bucket_id)) + continue; + int bucket_pos = -1; + for (auto &task : buckets.at(bucket_id)) { + ++bucket_pos; + if (bld->id == task.first && index == task.second) + break; + } + if (bucket_pos++ >= 0) + min_pos = min_pos < 0 ? bucket_pos : std::min(min_pos, bucket_pos); + } -static void scheduleCycle() { - cycle_requested = true; + return min_pos < 0 ? 0 : min_pos; } -static bool setSetting(std::string name, bool value) { - if (name == "quickfort_mode") - { - debug("setting quickfort_mode %d -> %d", quickfort_mode, value); - quickfort_mode = value; - return true; - } - if (name == "all_enabled") - { - debug("setting all_enabled %d -> %d", all_enabled, value); - all_enabled = value; - return true; +static void makeTopPriority(color_ostream &out, df::building *bld) { + DEBUG(status,out).print("entering makeTopPriority\n"); + if (!validate_pb(out, bld, 0)) + return; + + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &job_items = bld->jobs[0]->job_items; + + for (int index = 0; index < (int)job_items.size(); ++index) { + for (auto &vec_id : pb.vector_ids[index]) { + if (!tasks.count(vec_id)) + continue; + auto &buckets = tasks.at(vec_id); + string bucket_id = getBucket(*job_items[index], pb, index); + if (!buckets.count(bucket_id)) + continue; + auto &bucket = buckets.at(bucket_id); + for (auto taskit = bucket.begin(); taskit != bucket.end(); ++taskit) { + if (bld->id == taskit->first && index == taskit->second) { + auto task_bld_id = taskit->first; + auto task_job_item_idx = taskit->second; + bucket.erase(taskit); + bucket.emplace_front(task_bld_id, task_job_item_idx); + break; + } + } + } } - return planner.setGlobalSetting(name, value); } DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(isPlanModeEnabled), + DFHACK_LUA_FUNCTION(printStatus), + DFHACK_LUA_FUNCTION(setSetting), DFHACK_LUA_FUNCTION(isPlannableBuilding), DFHACK_LUA_FUNCTION(isPlannedBuilding), DFHACK_LUA_FUNCTION(addPlannedBuilding), DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), - DFHACK_LUA_FUNCTION(setSetting), + DFHACK_LUA_FUNCTION(countAvailableItems), + DFHACK_LUA_FUNCTION(hasFilter), + DFHACK_LUA_FUNCTION(clearFilter), + DFHACK_LUA_FUNCTION(setMaterialFilter), + DFHACK_LUA_FUNCTION(setHeatSafetyFilter), + DFHACK_LUA_FUNCTION(setQualityFilter), + DFHACK_LUA_FUNCTION(getDescString), + DFHACK_LUA_FUNCTION(getQueuePosition), + DFHACK_LUA_FUNCTION(makeTopPriority), + DFHACK_LUA_END +}; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getGlobalSettings), + DFHACK_LUA_COMMAND(getAvailableItems), + DFHACK_LUA_COMMAND(getMaterialFilter), + DFHACK_LUA_COMMAND(getHeatSafetyFilter), + DFHACK_LUA_COMMAND(getQualityFilter), DFHACK_LUA_END }; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index e906ef1a7..eef9808e6 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -1,8 +1,52 @@ #pragma once -#include "buildingplan-planner.h" -#include "buildingplan-rooms.h" +#include "itemfilter.h" -void debug(const char *fmt, ...) Wformat(printf,1,2); +#include "modules/Persistence.h" -extern bool show_debugging; +#include "df/building.h" +#include "df/job_item.h" +#include "df/job_item_vector_id.h" + +#include + +typedef std::deque> Bucket; +typedef std::map> Tasks; + +extern const std::string FILTER_CONFIG_KEY; +extern const std::string BLD_CONFIG_KEY; + +enum ConfigValues { + CONFIG_BLOCKS = 1, + CONFIG_BOULDERS = 2, + CONFIG_LOGS = 3, + CONFIG_BARS = 4, +}; + +enum FilterConfigValues { + FILTER_CONFIG_TYPE = 0, + FILTER_CONFIG_SUBTYPE = 1, + FILTER_CONFIG_CUSTOM = 2, +}; + +enum BuildingConfigValues { + BLD_CONFIG_ID = 0, + BLD_CONFIG_HEAT = 1, +}; + +enum HeatSafety { + HEAT_SAFETY_ANY = 0, + HEAT_SAFETY_FIRE = 1, + HEAT_SAFETY_MAGMA = 2, +}; + +int get_config_val(DFHack::PersistentDataItem &c, int index); +bool get_config_bool(DFHack::PersistentDataItem &c, int index); +void set_config_val(DFHack::PersistentDataItem &c, int index, int value); +void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); + +std::vector getVectorIds(DFHack::color_ostream &out, const df::job_item *job_item); +bool itemPassesScreen(df::item * item); +bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter); +bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); +void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp new file mode 100644 index 000000000..f401c90a8 --- /dev/null +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -0,0 +1,304 @@ +#include "plannedbuilding.h" +#include "buildingplan.h" + +#include "Debug.h" + +#include "modules/Items.h" +#include "modules/Job.h" +#include "modules/Maps.h" +#include "modules/Materials.h" + +#include "df/building_design.h" +#include "df/item.h" +#include "df/job.h" +#include "df/map_block.h" +#include "df/world.h" + +#include + +using std::map; +using std::string; +using std::unordered_map; + +namespace DFHack { + DBG_EXTERN(buildingplan, cycle); +} + +using namespace DFHack; + +struct BadFlags { + uint32_t whole; + + BadFlags() { + df::item_flags flags; + #define F(x) 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); F(in_job); + F(owned); F(in_chest); F(removed); F(encased); + F(spider_web); + #undef F + whole = flags.whole; + } +}; + +bool itemPassesScreen(df::item * item) { + static const BadFlags bad_flags; + return !(item->flags.whole & bad_flags.whole) + && !item->isAssignedToStockpile(); +} + +bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter) { + // check the properties that are not checked by Job::isSuitableItem() + if (job_item->item_type > -1 && job_item->item_type != item->getType()) + return false; + + if (job_item->item_subtype > -1 && + job_item->item_subtype != item->getSubtype()) + return false; + + if (job_item->flags2.bits.building_material && !item->isBuildMat()) + return false; + + if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) + return false; + + if (job_item->has_tool_use > df::tool_uses::NONE + && !item->hasToolUse(job_item->has_tool_use)) + return false; + + df::job_item jitem = *job_item; + if (heat == HEAT_SAFETY_MAGMA) { + jitem.flags2.bits.magma_safe = true; + jitem.flags2.bits.fire_safe = false; + } else if (heat == HEAT_SAFETY_FIRE && !jitem.flags2.bits.magma_safe) + jitem.flags2.bits.fire_safe = true; + + return Job::isSuitableItem( + &jitem, item->getType(), item->getSubtype()) + && Job::isSuitableMaterial( + &jitem, item->getMaterial(), item->getMaterialIndex(), + item->getType()) + && item_filter.matches(item); +} + +bool isJobReady(color_ostream &out, const std::vector &jitems) { + int needed_items = 0; + for (auto job_item : jitems) { needed_items += job_item->quantity; } + if (needed_items) { + DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); + return false; + } + return true; +} + +static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { + // we want the items in the opposite order of the filters + return a->job_item_idx > b->job_item_idx; +} + +// this function does not remove the job_items since their quantity fields are +// now all at 0, so there is no risk of having extra items attached. we don't +// remove them to keep the "finalize with buildingplan active" path as similar +// as possible to the "finalize with buildingplan disabled" path. +void finalizeBuilding(color_ostream &out, df::building *bld) { + DEBUG(cycle,out).print("finalizing building %d\n", bld->id); + auto job = bld->jobs[0]; + + // sort the items so they get added to the structure in the correct order + std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); + + // derive the material properties of the building and job from the first + // applicable item. if any boulders are involved, it makes the whole + // structure "rough". + bool rough = false; + for (auto attached_item : job->items) { + df::item *item = attached_item->item; + rough = rough || item->getType() == df::item_type::BOULDER; + if (bld->mat_type == -1) { + bld->mat_type = item->getMaterial(); + job->mat_type = bld->mat_type; + } + if (bld->mat_index == -1) { + bld->mat_index = item->getMaterialIndex(); + job->mat_index = bld->mat_index; + } + } + + if (bld->needsDesign()) { + auto act = (df::building_actual *)bld; + if (!act->design) + act->design = new df::building_design(); + act->design->flags.bits.rough = rough; + } + + // we're good to go! + job->flags.bits.suspend = false; + Job::checkBuildingsNow(); +} + +static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, + unordered_map &planned_buildings) { + while (!task_queue.empty()) { + auto & task = task_queue.front(); + auto id = task.first; + if (planned_buildings.count(id) > 0) { + auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out); + if (bld && bld->jobs[0]->job_items[task.second]->quantity) + return bld; + } + DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); + task_queue.pop_front(); + } + return NULL; +} + +// This is tricky. we want to choose an item that can be brought to the job site, but that's not +// necessarily the same as job->pos. it could be many tiles off in any direction (e.g. for bridges), or +// up or down (e.g. for stairs). For now, just return if the item is on a walkable tile. +static bool isAccessibleFrom(color_ostream &out, df::item *item, df::job *job) { + df::coord item_pos = Items::getPosition(item); + df::map_block *block = Maps::getTileBlock(item_pos); + bool is_walkable = false; + if (block) { + uint16_t walkability_group = index_tile(block->walkable, item_pos); + is_walkable = walkability_group != 0; + TRACE(cycle,out).print("item %d in walkability_group %u at (%d,%d,%d) is %saccessible from job site\n", + item->id, walkability_group, item_pos.x, item_pos.y, item_pos.z, is_walkable ? "" : "not "); + } + return is_walkable; +} + +static void doVector(color_ostream &out, df::job_item_vector_id vector_id, + map &buckets, + unordered_map &planned_buildings) { + auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); + auto item_vector = df::global::world->items.other[other_id]; + DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", + item_vector.size(), + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + buckets.size()); + for (auto item_it = item_vector.rbegin(); + item_it != item_vector.rend(); + ++item_it) { + auto item = *item_it; + if (!itemPassesScreen(item)) + continue; + for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { + auto & task_queue = bucket_it->second; + auto bld = popInvalidTasks(out, task_queue, planned_buildings); + if (!bld) { + DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str(), + buckets.size() - 1); + bucket_it = buckets.erase(bucket_it); + continue; + } + auto & task = task_queue.front(); + auto id = task.first; + auto job = bld->jobs[0]; + auto filter_idx = task.second; + auto &pb = planned_buildings.at(id); + if (isAccessibleFrom(out, item, job) + && matchesFilters(item, job->job_items[filter_idx], pb.heat_safety, + pb.item_filters[filter_idx]) + && Job::attachJobItem(job, item, + df::job_item_ref::Hauled, filter_idx)) + { + MaterialInfo material; + material.decode(item); + ItemTypeInfo item_type; + item_type.decode(item); + DEBUG(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n", + material.toString().c_str(), + item_type.toString().c_str(), + filter_idx, + ENUM_KEY_STR(building_type, bld->getType()).c_str(), + id, + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str()); + // keep quantity aligned with the actual number of remaining + // items so if buildingplan is turned off, the building will + // be completed with the correct number of items. + --job->job_items[filter_idx]->quantity; + task_queue.pop_front(); + if (isJobReady(out, job->job_items)) { + finalizeBuilding(out, bld); + planned_buildings.at(id).remove(out); + } + if (task_queue.empty()) { + DEBUG(cycle,out).print( + "removing empty item bucket: %s/%s; %zu left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str(), + buckets.size() - 1); + buckets.erase(bucket_it); + } + // we found a home for this item; no need to look further + break; + } + ++bucket_it; + } + if (buckets.empty()) + break; + } +} + +struct VectorsToScanLast { + std::vector vectors; + VectorsToScanLast() { + // order is important here. we want to match boulders before wood and + // everything before bars. blocks are not listed here since we'll have + // already scanned them when we did the first pass through the buckets. + vectors.push_back(df::job_item_vector_id::BOULDER); + vectors.push_back(df::job_item_vector_id::WOOD); + vectors.push_back(df::job_item_vector_id::BAR); + vectors.push_back(df::job_item_vector_id::IN_PLAY); + } +}; + +void buildingplan_cycle(color_ostream &out, Tasks &tasks, + unordered_map &planned_buildings) { + static const VectorsToScanLast vectors_to_scan_last; + + DEBUG(cycle,out).print( + "running buildingplan cycle for %zu registered buildings\n", + planned_buildings.size()); + + for (auto it = tasks.begin(); it != tasks.end(); ) { + auto vector_id = it->first; + // we could make this a set, but it's only a few elements + if (std::find(vectors_to_scan_last.vectors.begin(), + vectors_to_scan_last.vectors.end(), + vector_id) != vectors_to_scan_last.vectors.end()) { + ++it; + continue; + } + + auto & buckets = it->second; + doVector(out, vector_id, buckets, planned_buildings); + if (buckets.empty()) { + DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + tasks.size() - 1); + it = tasks.erase(it); + } + else + ++it; + } + for (auto vector_id : vectors_to_scan_last.vectors) { + if (tasks.count(vector_id) == 0) + continue; + auto & buckets = tasks[vector_id]; + doVector(out, vector_id, buckets, planned_buildings); + if (buckets.empty()) { + DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + tasks.size() - 1); + tasks.erase(vector_id); + } + } + DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", + planned_buildings.size()); +} diff --git a/plugins/buildingplan/buildingtypekey.cpp b/plugins/buildingplan/buildingtypekey.cpp new file mode 100644 index 000000000..664fdf27d --- /dev/null +++ b/plugins/buildingplan/buildingtypekey.cpp @@ -0,0 +1,59 @@ +#include "buildingplan.h" +#include "buildingtypekey.h" + +#include "Debug.h" +#include "MiscUtils.h" + +using std::string; +using std::vector; + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using namespace DFHack; + +// building type, subtype, custom +BuildingTypeKey::BuildingTypeKey(df::building_type type, int16_t subtype, int32_t custom) + : tuple(type, subtype, custom) { } + +static BuildingTypeKey deserialize(color_ostream &out, const std::string &serialized) { + vector key_parts; + split_string(&key_parts, serialized, ","); + if (key_parts.size() != 3) { + WARN(status,out).print("invalid key_str: '%s'\n", serialized.c_str()); + return BuildingTypeKey(df::building_type::NONE, -1, -1); + } + return BuildingTypeKey((df::building_type)string_to_int(key_parts[0]), + string_to_int(key_parts[1]), string_to_int(key_parts[2])); +} + +BuildingTypeKey::BuildingTypeKey(color_ostream &out, const std::string &serialized) + :tuple(deserialize(out, serialized)) { } + +string BuildingTypeKey::serialize() const { + std::ostringstream ser; + ser << std::get<0>(*this) << ","; + ser << std::get<1>(*this) << ","; + ser << std::get<2>(*this); + return ser.str(); +} + +// rotates a size_t value left by count bits +// assumes count is not 0 or >= size_t_bits +// replace this with std::rotl when we move to C++20 +static std::size_t rotl_size_t(size_t val, uint32_t count) +{ + static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); + return val << count | val >> (size_t_bits - count); +} + +std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const { + // cast first param to appease gcc-4.8, which is missing the enum + // specializations for std::hash + std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); + std::size_t h2 = std::hash()(std::get<1>(key)); + std::size_t h3 = std::hash()(std::get<2>(key)); + + return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); +} diff --git a/plugins/buildingplan/buildingtypekey.h b/plugins/buildingplan/buildingtypekey.h new file mode 100644 index 000000000..81bb043c5 --- /dev/null +++ b/plugins/buildingplan/buildingtypekey.h @@ -0,0 +1,22 @@ +#pragma once + +#include "df/building_type.h" + +#include +#include + +namespace DFHack { + class color_ostream; +} + +// building type, subtype, custom +struct BuildingTypeKey : public std::tuple { + BuildingTypeKey(df::building_type type, int16_t subtype, int32_t custom); + BuildingTypeKey(DFHack::color_ostream &out, const std::string & serialized); + + std::string serialize() const; +}; + +struct BuildingTypeKeyHash { + std::size_t operator() (const BuildingTypeKey & key) const; +}; diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp new file mode 100644 index 000000000..36d074363 --- /dev/null +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -0,0 +1,60 @@ +#include "defaultitemfilters.h" + +#include "Debug.h" +#include "MiscUtils.h" + +#include "modules/World.h" + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using std::string; +using std::vector; +using namespace DFHack; + +BuildingTypeKey DefaultItemFilters::getKey(PersistentDataItem &filter_config) { + return BuildingTypeKey( + (df::building_type)get_config_val(filter_config, FILTER_CONFIG_TYPE), + get_config_val(filter_config, FILTER_CONFIG_SUBTYPE), + get_config_val(filter_config, FILTER_CONFIG_CUSTOM)); +} + +DefaultItemFilters::DefaultItemFilters(color_ostream &out, BuildingTypeKey key, const std::vector &jitems) + : key(key) { + DEBUG(status,out).print("creating persistent data for filter key %d,%d,%d\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key)); + filter_config = World::AddPersistentData(FILTER_CONFIG_KEY); + set_config_val(filter_config, FILTER_CONFIG_TYPE, std::get<0>(key)); + set_config_val(filter_config, FILTER_CONFIG_SUBTYPE, std::get<1>(key)); + set_config_val(filter_config, FILTER_CONFIG_CUSTOM, std::get<2>(key)); + item_filters.resize(jitems.size()); + filter_config.val() = serialize_item_filters(item_filters); +} + +DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &filter_config, const std::vector &jitems) + : key(getKey(filter_config)), filter_config(filter_config) { + auto &serialized = filter_config.val(); + DEBUG(status,out).print("deserializing item filters for key %d,%d,%d: %s\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + std::vector filters = deserialize_item_filters(out, serialized); + if (filters.size() != jitems.size()) { + WARN(status,out).print("ignoring invalid filters_str for key %d,%d,%d: '%s'\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + item_filters.resize(jitems.size()); + } else + item_filters = filters; +} + +void DefaultItemFilters::setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index) { + if (index < 0 || item_filters.size() <= (size_t)index) { + WARN(status,out).print("invalid index for filter key %d,%d,%d: %d\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), index); + return; + } + + item_filters[index] = filter; + filter_config.val() = serialize_item_filters(item_filters); + DEBUG(status,out).print("updated item filter and persisted for key %d,%d,%d: %s\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), filter_config.val().c_str()); +} diff --git a/plugins/buildingplan/defaultitemfilters.h b/plugins/buildingplan/defaultitemfilters.h new file mode 100644 index 000000000..4d1d5cbd2 --- /dev/null +++ b/plugins/buildingplan/defaultitemfilters.h @@ -0,0 +1,24 @@ +#pragma once + +#include "buildingplan.h" +#include "buildingtypekey.h" + +#include "modules/Persistence.h" + +class DefaultItemFilters { +public: + static BuildingTypeKey getKey(DFHack::PersistentDataItem &filter_config); + + const BuildingTypeKey key; + + DefaultItemFilters(DFHack::color_ostream &out, BuildingTypeKey key, const std::vector &jitems); + DefaultItemFilters(DFHack::color_ostream &out, DFHack::PersistentDataItem &filter_config, const std::vector &jitems); + + void setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index); + + const std::vector & getItemFilters() const { return item_filters; } + +private: + DFHack::PersistentDataItem filter_config; + std::vector item_filters; +}; diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp new file mode 100644 index 000000000..86c9c1378 --- /dev/null +++ b/plugins/buildingplan/itemfilter.cpp @@ -0,0 +1,181 @@ +#include "itemfilter.h" + +#include "Debug.h" + +#include "df/item.h" + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using std::string; +using std::vector; + +using namespace DFHack; + +ItemFilter::ItemFilter() { + clear(); +} + +void ItemFilter::clear() { + min_quality = df::item_quality::Ordinary; + max_quality = df::item_quality::Masterful; + decorated_only = false; + mat_mask.whole = 0; + materials.clear(); +} + +bool ItemFilter::isEmpty() const { + return min_quality == df::item_quality::Ordinary + && max_quality == df::item_quality::Masterful + && !decorated_only + && !mat_mask.whole + && materials.empty(); +} + +static bool deserializeMaterialMask(string ser, df::dfhack_material_category mat_mask) { + if (ser.empty()) + return true; + + if (!parseJobMaterialCategory(&mat_mask, ser)) { + DEBUG(status).print("invalid job material category serialization: '%s'", ser.c_str()); + return false; + } + return true; +} + +static bool deserializeMaterials(string ser, vector &materials) { + if (ser.empty()) + return true; + + vector mat_names; + split_string(&mat_names, ser, ","); + for (auto m = mat_names.begin(); m != mat_names.end(); m++) { + DFHack::MaterialInfo material; + if (!material.find(*m) || !material.isValid()) { + DEBUG(status).print("invalid material name serialization: '%s'", ser.c_str()); + return false; + } + materials.push_back(material); + } + return true; +} + +ItemFilter::ItemFilter(color_ostream &out, string serialized) { + clear(); + + vector tokens; + split_string(&tokens, serialized, "/"); + if (tokens.size() != 5) { + DEBUG(status,out).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); + return; + } + + if (!deserializeMaterialMask(tokens[0], mat_mask) || !deserializeMaterials(tokens[1], materials)) + return; + + setMinQuality(atoi(tokens[2].c_str())); + setMaxQuality(atoi(tokens[3].c_str())); + decorated_only = static_cast(atoi(tokens[4].c_str())); +} + +// format: mat,mask,elements/materials,list/minq/maxq/decorated +string ItemFilter::serialize() const { + std::ostringstream ser; + ser << bitfield_to_string(mat_mask, ",") << "/"; + if (!materials.empty()) { + ser << materials[0].getToken(); + for (size_t i = 1; i < materials.size(); ++i) + ser << "," << materials[i].getToken(); + } + ser << "/" << static_cast(min_quality); + ser << "/" << static_cast(max_quality); + ser << "/" << static_cast(decorated_only); + return ser.str(); +} + +static void clampItemQuality(df::item_quality *quality) { + if (*quality > df::item_quality::Artifact) { + DEBUG(status).print("clamping quality to Artifact"); + *quality = df::item_quality::Artifact; + } + if (*quality < df::item_quality::Ordinary) { + DEBUG(status).print("clamping quality to Ordinary"); + *quality = df::item_quality::Ordinary; + } +} + +void ItemFilter::setMinQuality(int quality) { + min_quality = static_cast(quality); + clampItemQuality(&min_quality); + if (max_quality < min_quality) + max_quality = min_quality; +} + +void ItemFilter::setMaxQuality(int quality) { + max_quality = static_cast(quality); + clampItemQuality(&max_quality); + if (max_quality < min_quality) + min_quality = max_quality; +} + +void ItemFilter::setDecoratedOnly(bool decorated) { + decorated_only = decorated; +} + +void ItemFilter::setMaterialMask(uint32_t mask) { + mat_mask.whole = mask; +} + +void ItemFilter::setMaterials(const vector &materials) { + this->materials = materials; +} + +static bool matchesMask(DFHack::MaterialInfo &mat, df::dfhack_material_category mat_mask) { + return mat_mask.whole ? mat.matches(mat_mask) : true; +} + +bool ItemFilter::matches(df::dfhack_material_category mask) const { + return mask.whole & mat_mask.whole; +} + +bool ItemFilter::matches(DFHack::MaterialInfo &material) const { + for (auto it = materials.begin(); it != materials.end(); ++it) + if (material.matches(*it)) + return true; + return false; +} + +bool ItemFilter::matches(df::item *item) const { + if (item->getQuality() < min_quality || item->getQuality() > max_quality) + return false; + + if (decorated_only && !item->hasImprovements()) + return false; + + auto imattype = item->getActualMaterial(); + auto imatindex = item->getActualMaterialIndex(); + auto item_mat = DFHack::MaterialInfo(imattype, imatindex); + + return (materials.size() == 0) ? matchesMask(item_mat, mat_mask) : matches(item_mat); +} + +vector deserialize_item_filters(color_ostream &out, const string &serialized) { + std::vector filters; + + vector filter_strs; + split_string(&filter_strs, serialized, ";"); + for (auto &str : filter_strs) { + filters.emplace_back(out, str); + } + + return filters; +} + +string serialize_item_filters(const vector &filters) { + vector strs; + for (auto &filter : filters) { + strs.emplace_back(filter.serialize()); + } + return join_strings(";", strs); +} diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h new file mode 100644 index 000000000..29eb7226c --- /dev/null +++ b/plugins/buildingplan/itemfilter.h @@ -0,0 +1,42 @@ +#pragma once + +#include "modules/Materials.h" + +#include "df/dfhack_material_category.h" +#include "df/item_quality.h" + +class ItemFilter { +public: + ItemFilter(); + ItemFilter(DFHack::color_ostream &out, std::string serialized); + + void clear(); + bool isEmpty() const; + std::string serialize() const; + + void setMinQuality(int quality); + void setMaxQuality(int quality); + void setDecoratedOnly(bool decorated); + void setMaterialMask(uint32_t mask); + void setMaterials(const std::vector &materials); + + df::item_quality getMinQuality() const { return min_quality; } + df::item_quality getMaxQuality() const {return max_quality; } + bool getDecoratedOnly() const { return decorated_only; } + df::dfhack_material_category getMaterialMask() const { return mat_mask; } + std::vector getMaterials() const { return materials; } + + bool matches(df::dfhack_material_category mask) const; + bool matches(DFHack::MaterialInfo &material) const; + bool matches(df::item *item) const; + +private: + df::item_quality min_quality; + df::item_quality max_quality; + bool decorated_only; + df::dfhack_material_category mat_mask; + std::vector materials; +}; + +std::vector deserialize_item_filters(DFHack::color_ostream &out, const std::string &serialized); +std::string serialize_item_filters(const std::vector &filters); diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp new file mode 100644 index 000000000..27be36a5b --- /dev/null +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -0,0 +1,110 @@ +#include "plannedbuilding.h" +#include "buildingplan.h" + +#include "Debug.h" +#include "MiscUtils.h" + +#include "modules/World.h" + +#include "df/job.h" + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using std::string; +using std::vector; +using namespace DFHack; + +static vector> get_vector_ids(color_ostream &out, int bld_id) { + vector> ret; + + df::building *bld = df::building::find(bld_id); + + if (!bld || bld->jobs.size() != 1) + return ret; + + auto &job = bld->jobs[0]; + for (auto &jitem : job->job_items) { + ret.emplace_back(getVectorIds(out, jitem)); + } + return ret; +} + +static vector> deserialize_vector_ids(color_ostream &out, PersistentDataItem &bld_config) { + vector> ret; + + vector rawstrs; + split_string(&rawstrs, bld_config.val(), "|"); + const string &serialized = rawstrs[0]; + + DEBUG(status,out).print("deserializing vector ids for building %d: %s\n", + get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str()); + + vector joined; + split_string(&joined, serialized, ";"); + for (auto &str : joined) { + vector lst; + split_string(&lst, str, ","); + vector ids; + for (auto &s : lst) + ids.emplace_back(df::job_item_vector_id(string_to_int(s))); + ret.emplace_back(ids); + } + + if (!ret.size()) + ret = get_vector_ids(out, get_config_val(bld_config, BLD_CONFIG_ID)); + + return ret; +} + +static std::vector get_item_filters(color_ostream &out, PersistentDataItem &bld_config) { + std::vector ret; + + vector rawstrs; + split_string(&rawstrs, bld_config.val(), "|"); + if (rawstrs.size() < 2) + return ret; + return deserialize_item_filters(out, rawstrs[1]); +} + +static string serialize(const vector> &vector_ids, const vector &item_filters) { + vector joined; + for (auto &vec_list : vector_ids) { + joined.emplace_back(join_strings(",", vec_list)); + } + std::ostringstream out; + out << join_strings(";", joined) << "|" << serialize_item_filters(item_filters); + return out.str(); +} + +PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat, const vector &item_filters) + : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat), + item_filters(item_filters) { + DEBUG(status,out).print("creating persistent data for building %d\n", id); + bld_config = World::AddPersistentData(BLD_CONFIG_KEY); + set_config_val(bld_config, BLD_CONFIG_ID, id); + set_config_val(bld_config, BLD_CONFIG_HEAT, heat_safety); + bld_config.val() = serialize(vector_ids, item_filters); + DEBUG(status,out).print("serialized state for building %d: %s\n", id, bld_config.val().c_str()); +} + +PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) + : id(get_config_val(bld_config, BLD_CONFIG_ID)), + vector_ids(deserialize_vector_ids(out, bld_config)), + heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), + item_filters(get_item_filters(out, bld_config)), + bld_config(bld_config) { } + +// Ensure the building still exists and is in a valid state. It can disappear +// for lots of reasons, such as running the game with the buildingplan plugin +// disabled, manually removing the building, modifying it via the API, etc. +df::building * PlannedBuilding::getBuildingIfValidOrRemoveIfNot(color_ostream &out) { + auto bld = df::building::find(id); + bool valid = bld && bld->getBuildStage() == 0; + if (!valid) { + remove(out); + return NULL; + } + return bld; +} diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h new file mode 100644 index 000000000..5bd09ba5a --- /dev/null +++ b/plugins/buildingplan/plannedbuilding.h @@ -0,0 +1,36 @@ +#pragma once + +#include "buildingplan.h" +#include "itemfilter.h" + +#include "Core.h" + +#include "modules/Persistence.h" + +#include "df/building.h" +#include "df/job_item_vector_id.h" + +class PlannedBuilding { +public: + const df::building::key_field_type id; + + // job_item idx -> list of vectors the task is linked to + const std::vector> vector_ids; + + const HeatSafety heat_safety; + + const std::vector item_filters; + + PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat, const std::vector &item_filters); + PlannedBuilding(DFHack::color_ostream &out, DFHack::PersistentDataItem &bld_config); + + void remove(DFHack::color_ostream &out); + + // Ensure the building still exists and is in a valid state. It can disappear + // for lots of reasons, such as running the game with the buildingplan plugin + // disabled, manually removing the building, modifying it via the API, etc. + df::building * getBuildingIfValidOrRemoveIfNot(DFHack::color_ostream &out); + +private: + DFHack::PersistentDataItem bld_config; +}; diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index 2650d92d0..c1a5b5953 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -21,24 +21,27 @@ void ChannelJobs::load_channel_jobs() { } bool ChannelJobs::has_cavein_conditions(const df::coord &map_pos) { - auto p = map_pos; - auto ttype = *Maps::getTileType(p); - if (!DFHack::isOpenTerrain(ttype)) { - // check shared neighbour for cave-in conditions - df::coord neighbours[4]; - get_connected_neighbours(map_pos, neighbours); - int connectedness = 4; - for (auto n: neighbours) { - if (active.count(n) || DFHack::isOpenTerrain(*Maps::getTileType(n))) { - connectedness--; + if likely(Maps::isValidTilePos(map_pos)) { + auto p = map_pos; + auto ttype = *Maps::getTileType(p); + if (!DFHack::isOpenTerrain(ttype)) { + // check shared neighbour for cave-in conditions + df::coord neighbours[4]; + get_connected_neighbours(map_pos, neighbours); + int connectedness = 4; + for (auto n: neighbours) { + if (!Maps::isValidTilePos(n) || active.count(n) || DFHack::isOpenTerrain(*Maps::getTileType(n))) { + connectedness--; + } } - } - if (!connectedness) { - // do what? - p.z--; - ttype = *Maps::getTileType(p); - if (DFHack::isOpenTerrain(ttype) || DFHack::isFloorTerrain(ttype)) { - return true; + if (!connectedness) { + // do what? + p.z--; + if (!Maps::isValidTilePos(p)) return false; + ttype = *Maps::getTileType(p); + if (DFHack::isOpenTerrain(ttype) || DFHack::isFloorTerrain(ttype)) { + return true; + } } } } @@ -88,6 +91,7 @@ void ChannelGroups::add(const df::coord &map_pos) { DEBUG(groups).print(" add(" COORD ")\n", COORDARGS(map_pos)); // and so we begin iterating the neighbours for (auto &neighbour: neighbors) { + if unlikely(!Maps::isValidTilePos(neighbour)) continue; // go to the next neighbour if this one doesn't have a group if (!groups_map.count(neighbour)) { TRACE(groups).print(" -> neighbour is not designated\n"); diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index adb668468..910e0ee7c 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -112,6 +112,10 @@ enum SettingConfigData { // dig-now.cpp df::coord simulate_fall(const df::coord &pos) { + if unlikely(!Maps::isValidTilePos(pos)) { + ERR(plugin).print("Error: simulate_fall(" COORD ") - invalid coordinate\n", COORDARGS(pos)); + return {}; + } df::coord resting_pos(pos); while (Maps::ensureTileBlock(resting_pos)) { @@ -130,6 +134,7 @@ df::coord simulate_area_fall(const df::coord &pos) { get_neighbours(pos, neighbours); df::coord lowest = simulate_fall(pos); for (auto p : neighbours) { + if unlikely(!Maps::isValidTilePos(p)) continue; auto nlow = simulate_fall(p); if (nlow.z < lowest.z) { lowest = nlow; @@ -299,10 +304,11 @@ namespace CSP { int32_t tick = df::global::world->frame_counter; auto report_id = (int32_t)(intptr_t(r)); if (df::global::world) { - std::vector &reports = df::global::world->status.reports; - size_t idx = -1; - idx = df::report::binsearch_index(reports, report_id); - df::report* report = reports.at(idx); + df::report* report = df::report::find(report_id); + if (!report) { + WARN(plugin).print("Error: NewReportEvent() received an invalid report_id - a report* cannot be found\n"); + return; + } switch (report->type) { case announcement_type::CANCEL_JOB: if (config.insta_dig) { diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index 172275778..a29f5a04d 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -64,11 +64,13 @@ inline uint8_t count_accessibility(const df::coord &unit_pos, const df::coord &m get_connected_neighbours(map_pos, connections); uint8_t accessibility = Maps::canWalkBetween(unit_pos, map_pos) ? 1 : 0; for (auto n: neighbours) { + if unlikely(!Maps::isValidTilePos(n)) continue; if (Maps::canWalkBetween(unit_pos, n)) { accessibility++; } } for (auto n : connections) { + if unlikely(Maps::isValidTilePos(n)) continue; if (Maps::canWalkBetween(unit_pos, n)) { accessibility++; } @@ -77,22 +79,22 @@ inline uint8_t count_accessibility(const df::coord &unit_pos, const df::coord &m } inline bool isEntombed(const df::coord &unit_pos, const df::coord &map_pos) { - if (Maps::canWalkBetween(unit_pos, map_pos)) { + if likely(Maps::canWalkBetween(unit_pos, map_pos)) { return false; } df::coord neighbours[8]; get_neighbours(map_pos, neighbours); return std::all_of(neighbours+0, neighbours+8, [&unit_pos](df::coord n) { - return !Maps::canWalkBetween(unit_pos, n); + return !Maps::isValidTilePos(n) || !Maps::canWalkBetween(unit_pos, n); }); } inline bool is_dig_job(const df::job* job) { - return job->job_type == df::job_type::Dig || job->job_type == df::job_type::DigChannel; + return job && (job->job_type == df::job_type::Dig || job->job_type == df::job_type::DigChannel); } inline bool is_channel_job(const df::job* job) { - return job->job_type == df::job_type::DigChannel; + return job && (job->job_type == df::job_type::DigChannel); } inline bool is_group_job(const ChannelGroups &groups, const df::job* job) { @@ -111,34 +113,48 @@ inline bool is_safe_fall(const df::coord &map_pos) { df::coord below(map_pos); for (uint8_t zi = 0; zi < config.fall_threshold; ++zi) { below.z--; + // falling out of bounds is probably considerably unsafe for a dwarf + if unlikely(!Maps::isValidTilePos(below)) { + return false; + } + // if we require vision, and we can't see below.. we'll need to assume it's safe to get anything done if (config.require_vision && Maps::getTileDesignation(below)->bits.hidden) { - return true; //we require vision, and we can't see below.. so we gotta assume it's safe + return true; } + // finally, if we're not looking at open space (air to fall through) it's safe to fall to df::tiletype type = *Maps::getTileType(below); if (!DFHack::isOpenTerrain(type)) { return true; } } + // we exceeded the fall threshold, so it's not a safe fall return false; } inline bool is_safe_to_dig_down(const df::coord &map_pos) { df::coord pos(map_pos); + // todo: probably should rely on is_safe_fall, it looks like it could be simplified a great deal for (uint8_t zi = 0; zi <= config.fall_threshold; ++zi) { - // assume safe if we can't see and need vision + // if we're digging out of bounds, the game can handle that (hopefully) + if unlikely(!Maps::isValidTilePos(pos)) { + return true; + } + // if we require vision, and we can't see the tiles in question.. we'll need to assume it's safe to dig to get anything done if (config.require_vision && Maps::getTileDesignation(pos)->bits.hidden) { return true; } + df::tiletype type = *Maps::getTileType(pos); if (zi == 0 && DFHack::isOpenTerrain(type)) { + // todo: remove? this is probably not useful.. and seems like the only considerable difference to is_safe_fall (aside from where each stops looking) // the starting tile is open space, that's obviously not safe return false; } else if (!DFHack::isOpenTerrain(type)) { // a tile after the first one is not open space return true; } - pos.z--; + pos.z--; // todo: this can probably move to the beginning of the loop } return false; } diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 9b953dd7c..a0da8d838 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -4,8 +4,6 @@ local _ENV = mkmodule('plugins.buildingplan') Native functions: - * void setSetting(string name, boolean value) - * bool isPlanModeEnabled(df::building_type type, int16_t subtype, int32_t custom) * bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom) * bool isPlannedBuilding(df::building *bld) * void addPlannedBuilding(df::building *bld) @@ -15,8 +13,15 @@ local _ENV = mkmodule('plugins.buildingplan') --]] local argparse = require('argparse') +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require('gui.widgets') require('dfhack.buildings') +local uibs = df.global.buildreq + local function process_args(opts, args) if args[1] == 'help' then opts.help = true @@ -36,67 +41,595 @@ function parse_commandline(...) return false end + local command = table.remove(positionals, 1) + if not command or command == 'status' then + printStatus() + elseif command == 'set' then + setSetting(positionals[1], positionals[2] == 'true') + else + return false + end + return true end function get_num_filters(btype, subtype, custom) - local filters = dfhack.buildings.getFiltersByType( - {}, btype, subtype, custom) - if filters then return #filters end - return 0 + local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom) + return filters and #filters or 0 end -local dialogs = require('gui.dialogs') -local guidm = require('gui.dwarfmode') +function get_job_item(btype, subtype, custom, index) + local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom) + if not filters or not filters[index] then return nil end + local obj = df.job_item:new() + obj:assign(filters[index]) + return obj +end -local function to_title_case(str) - str = str:gsub('(%a)([%w_]*)', - function (first, rest) return first:upper()..rest:lower() end) - str = str:gsub('_', ' ') - return str +local function get_cur_filters() + return dfhack.buildings.getFiltersByType({}, uibs.building_type, + uibs.building_subtype, uibs.custom_type) +end + +local function is_choosing_area() + return uibs.selection_pos.x >= 0 end -local function get_filter(btype, subtype, custom, reverse_idx) - local filters = dfhack.buildings.getFiltersByType( - {}, btype, subtype, custom) - if not filters or reverse_idx < 0 or reverse_idx >= #filters then - error(string.format('invalid index: %d', reverse_idx)) +local function get_cur_area_dims(placement_data) + if not placement_data and not is_choosing_area() then return 1, 1, 1 end + local selection_pos = placement_data and placement_data.p1 or uibs.selection_pos + local pos = placement_data and placement_data.p2 or uibs.pos + return math.abs(selection_pos.x - pos.x) + 1, + math.abs(selection_pos.y - pos.y) + 1, + math.abs(selection_pos.z - pos.z) + 1 +end + +local function get_quantity(filter, hollow, placement_data) + local quantity = filter.quantity or 1 + local dimx, dimy, dimz = get_cur_area_dims(placement_data) + if quantity < 1 then + return (((dimx * dimy) // 4) + 1) * dimz + end + if hollow and dimx > 2 and dimy > 2 then + return quantity * (2*dimx + 2*dimy - 4) * dimz end - return filters[#filters-reverse_idx] + return quantity * dimx * dimy * dimz end --- returns a reasonable label for the item based on the qualities of the filter --- does not need the core suspended --- reverse_idx is 0-based and is expected to be counted from the *last* filter -function get_item_label(btype, subtype, custom, reverse_idx) - local filter = get_filter(btype, subtype, custom, reverse_idx) - if filter.has_tool_use then - return to_title_case(df.tool_uses[filter.has_tool_use]) +local BUTTON_START_PEN, BUTTON_END_PEN, SELECTED_ITEM_PEN = nil, nil, nil +local reset_counts_flag = false +local reset_inspector_flag = false +function signal_reset() + BUTTON_START_PEN = nil + BUTTON_END_PEN = nil + SELECTED_ITEM_PEN = nil + reset_counts_flag = true + reset_inspector_flag = true +end + +local to_pen = dfhack.pen.parse +local function get_button_start_pen() + if not BUTTON_START_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + BUTTON_START_PEN = to_pen{ch='[', fg=COLOR_YELLOW, + tile=texpos_base > 0 and texpos_base + 13 or nil} end - if filter.item_type then - return to_title_case(df.item_type[filter.item_type]) + return BUTTON_START_PEN +end +local function get_button_end_pen() + if not BUTTON_END_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + BUTTON_END_PEN = to_pen{ch=']', fg=COLOR_YELLOW, + tile=texpos_base > 0 and texpos_base + 15 or nil} end - if filter.flags2 and filter.flags2.building_material then - if filter.flags2.fire_safe then - return "Fire-safe building material"; + return BUTTON_END_PEN +end +local function get_selected_item_pen() + if not SELECTED_ITEM_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + SELECTED_ITEM_PEN = to_pen{ch='x', fg=COLOR_GREEN, + tile=texpos_base > 0 and texpos_base + 9 or nil} + end + return SELECTED_ITEM_PEN +end + +BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) +BuildingplanScreen.ATTRS { + pass_movement_keys=true, + pass_mouse_clicks=false, + defocusable=false, +} + +-------------------------------- +-- ItemSelection +-- + +local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true} +local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true} + +-- map of building type -> {set=set of recently used, list=list of recently used} +-- most recent entries are at the *end* of the list +local recently_used = {} + +local function sort_by_type(a, b) + local ad, bd = a.data, b.data + return ad.item_type < bd.item_type or + (ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality) +end + +local function sort_by_recency(a, b) + local tracker = recently_used[uibs.building_type] + if not tracker then return sort_by_type(a, b) end + local recent_a, recent_b = tracker.set[a.search_key], tracker.set[b.search_key] + -- if they're both in the set, return the one with the greater index, + -- indicating more recent + if recent_a and recent_b then return recent_a > recent_b end + if recent_a and not recent_b then return true end + if not recent_a and recent_b then return false end + return sort_by_type(a, b) +end + +local function sort_by_name(a, b) + return a.search_key < b.search_key or + (a.search_key == b.search_key and sort_by_type(a, b)) +end + +local function sort_by_quantity(a, b) + local ad, bd = a.data, b.data + return ad.quantity > bd.quantity or + (ad.quantity == bd.quantity and sort_by_type(a, b)) +end + +ItemSelection = defclass(ItemSelection, widgets.Window) +ItemSelection.ATTRS{ + frame_title='Choose items', + frame={w=56, h=20, l=4, t=8}, + resizable=true, + index=DEFAULT_NIL, + quantity=DEFAULT_NIL, + on_submit=DEFAULT_NIL, + on_cancel=DEFAULT_NIL, +} + +function ItemSelection:init() + local filter = get_cur_filters()[self.index] + self.num_selected = 0 + self.selected_set = {} + local plural = self.quantity == 1 and '' or 's' + + self:addviews{ + widgets.Label{ + frame={t=0, l=0, r=10}, + text={ + get_desc(filter), + plural, + NEWLINE, + ('Select up to %d item%s ('):format(self.quantity, plural), + {text=function() return self.num_selected end}, + ' selected)', + }, + }, + widgets.Label{ + frame={r=0, w=9, t=0, h=3}, + text_pen=BUILD_TEXT_PEN, + text_hpen=BUILD_TEXT_HPEN, + text={ + ' ', NEWLINE, + ' Build ', NEWLINE, + ' ', + }, + on_click=self:callback('submit'), + }, + widgets.FilteredList{ + view_id='flist', + frame={t=3, l=0, r=0, b=4}, + case_sensitive=false, + choices=self:get_choices(sort_by_recency), + icon_width=2, + on_submit=self:callback('toggle_group'), + }, + widgets.CycleHotkeyLabel{ + frame={l=0, b=2}, + key='CUSTOM_CTRL_X', + label='Sort by:', + options={ + {label='Recently used', value=sort_by_recency}, + {label='Name', value=sort_by_name}, + {label='Amount', value=sort_by_quantity}, + }, + on_change=self:callback('on_sort'), + }, + widgets.HotkeyLabel{ + frame={l=0, b=1}, + key='SELECT', + label='Use all/none', + auto_width=true, + on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.HotkeyLabel{ + frame={l=22, b=1}, + key='CUSTOM_CTRL_D', + label='Build', + auto_width=true, + on_activate=self:callback('submit'), + }, + widgets.HotkeyLabel{ + frame={l=38, b=1}, + key='LEAVESCREEN', + label='Go back', + auto_width=true, + on_activate=self:callback('on_cancel'), + }, + widgets.HotkeyLabel{ + frame={l=0, b=0}, + key='KEYBOARD_CURSOR_RIGHT_FAST', + key_sep=' : ', + label='Use one', + auto_width=true, + on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=6, b=0, w=5}, + text_pen=COLOR_LIGHTGREEN, + text='Right', + }, + widgets.HotkeyLabel{ + frame={l=23, b=0}, + key='KEYBOARD_CURSOR_LEFT_FAST', + key_sep=' : ', + label='Use one fewer', + auto_width=true, + on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=29, b=0, w=4}, + text_pen=COLOR_LIGHTGREEN, + text='Left', + }, + } +end + +-- resort and restore selection +function ItemSelection:on_sort(sort_fn) + local flist = self.subviews.flist + local saved_filter = flist:getFilter() + flist:setFilter('') + flist:setChoices(self:get_choices(sort_fn), flist:getSelected()) + flist:setFilter(saved_filter) +end + +local function make_search_key(str) + local out = '' + for c in str:gmatch("[%w%s]") do + out = out .. c + end + return out +end + +function ItemSelection:get_choices(sort_fn) + local item_ids = getAvailableItems(uibs.building_type, + uibs.building_subtype, uibs.custom_type, self.index-1) + local buckets = {} + for _,item_id in ipairs(item_ids) do + local item = df.item.find(item_id) + if not item then goto continue end + local desc = dfhack.items.getDescription(item, 0, true) + if buckets[desc] then + local bucket = buckets[desc] + table.insert(bucket.data.item_ids, item_id) + bucket.data.quantity = bucket.data.quantity + 1 + else + local entry = { + search_key=make_search_key(desc), + icon=self:callback('get_entry_icon', item_id), + data={ + item_ids={item_id}, + item_type=item:getType(), + item_subtype=item:getSubtype(), + quantity=1, + quality=item:getQuality(), + selected=0, + }, + } + buckets[desc] = entry end - if filter.flags2.magma_safe then - return "Magma-safe building material"; + ::continue:: + end + local choices = {} + for desc,choice in pairs(buckets) do + local data = choice.data + choice.text = { + {width=10, text=function() return ('[%d/%d]'):format(data.selected, data.quantity) end}, + {gap=2, text=desc}, + } + table.insert(choices, choice) + end + table.sort(choices, sort_fn) + return choices +end + +function ItemSelection:increment_group(idx, choice) + local data = choice.data + if self.quantity <= self.num_selected then return false end + if data.selected >= data.quantity then return false end + data.selected = data.selected + 1 + self.num_selected = self.num_selected + 1 + local item_id = data.item_ids[data.selected] + self.selected_set[item_id] = true + return true +end + +function ItemSelection:decrement_group(idx, choice) + local data = choice.data + if data.selected <= 0 then return false end + local item_id = data.item_ids[data.selected] + self.selected_set[item_id] = nil + self.num_selected = self.num_selected - 1 + data.selected = data.selected - 1 + return true +end + +function ItemSelection:toggle_group(idx, choice) + local data = choice.data + if data.selected > 0 then + while self:decrement_group(idx, choice) do end + else + while self:increment_group(idx, choice) do end + end +end + +function ItemSelection:get_entry_icon(item_id) + return self.selected_set[item_id] and get_selected_item_pen() or nil +end + +local function track_recently_used(choices) + -- use same set for all subtypes + local tracker = ensure_key(recently_used, uibs.building_type) + for _,choice in ipairs(choices) do + local data = choice.data + if data.selected <= 0 then goto continue end + local key = choice.search_key + local recent_set = ensure_key(tracker, 'set') + local recent_list = ensure_key(tracker, 'list') + if recent_set[key] then + if recent_list[#recent_list] ~= key then + for i,v in ipairs(recent_list) do + if v == key then + table.remove(recent_list, i) + table.insert(recent_list, key) + break + end + end + tracker.set = utils.invert(recent_list) + end + else + -- only keep most recent 10 + if #recent_list >= 10 then + -- remove least recently used from list and set + recent_set[table.remove(recent_list, 1)] = nil + end + table.insert(recent_list, key) + recent_set[key] = #recent_list + end + ::continue:: + end +end + +function ItemSelection:submit() + local selected_items = {} + for item_id in pairs(self.selected_set) do + table.insert(selected_items, item_id) + end + if #selected_items > 0 then + track_recently_used(self.subviews.flist:getChoices()) + end + self.on_submit(selected_items) +end + +function ItemSelection:onInput(keys) + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + self.on_cancel() + return true + elseif keys._MOUSE_L_DOWN then + local list = self.subviews.flist.list + local idx = list:getIdxUnderMouse() + if idx then + list:setSelected(idx) + local modstate = dfhack.internal.getModstate() + if modstate & 2 > 0 then -- ctrl + local choice = list:getChoices()[idx] + if modstate & 1 > 0 then -- shift + self:decrement_group(idx, choice) + else + self:increment_group(idx, choice) + end + return true + end + end + end + return ItemSelection.super.onInput(self, keys) +end + +ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) +ItemSelectionScreen.ATTRS { + focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/itemselection', + force_pause=true, + pass_pause=false, + index=DEFAULT_NIL, + quantity=DEFAULT_NIL, + on_submit=DEFAULT_NIL, + on_cancel=DEFAULT_NIL, +} + +function ItemSelectionScreen:init() + self:addviews{ + ItemSelection{ + index=self.index, + quantity=self.quantity, + on_submit=self.on_submit, + on_cancel=self.on_cancel, + } + } +end + +-------------------------------- +-- Slider +-- + +Slider = defclass(Slider, widgets.Widget) +Slider.ATTRS{ + num_stops=DEFAULT_NIL, + get_left_idx_fn=DEFAULT_NIL, + get_right_idx_fn=DEFAULT_NIL, + on_left_change=DEFAULT_NIL, + on_right_change=DEFAULT_NIL, +} + +function Slider:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function Slider:init() + if self.num_stops < 2 then error('too few Slider stops') end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +local function slider_get_width_per_idx(self) + return math.max(5, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function Slider:onInput(keys) + if not keys._MOUSE_L_DOWN then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + local width_per_idx = slider_get_width_per_idx(self) + local left_pos = width_per_idx*(left_idx-1) + local right_pos = width_per_idx*(right_idx-1) + 4 + if x < left_pos then + self.on_left_change(self.get_left_idx_fn() - 1) + elseif x < left_pos+3 then + self.is_dragging_target = 'left' + self.is_dragging_idx = x - left_pos + elseif x < right_pos then + self.is_dragging_target = 'both' + self.is_dragging_idx = x - left_pos + elseif x < right_pos+3 then + self.is_dragging_target = 'right' + self.is_dragging_idx = x - right_pos + else + self.on_right_change(self.get_right_idx_fn() + 1) + end + return true +end + +local function slider_do_drag(self, width_per_idx) + local x = self.frame_body:localXY(dfhack.screen.getMousePos()) + local cur_pos = x - self.is_dragging_idx + cur_pos = math.max(0, cur_pos) + cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) + local offset = self.is_dragging_target == 'right' and -2 or 1 + local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 + local new_left_idx, new_right_idx + if self.is_dragging_target == 'right' then + new_right_idx = new_idx + else + new_left_idx = new_idx + if self.is_dragging_target == 'both' then + new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() + if new_right_idx > self.num_stops then + return + end + end + end + if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then + self.on_left_change(new_left_idx) + end + if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then + self.on_right_change(new_right_idx) + end +end + +local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} + +function Slider:onRenderBody(dc, rect) + local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + local width_per_idx = slider_get_width_per_idx(self) + -- draw track + dc:seek(1,0) + dc:char(nil, SLIDER_LEFT_END) + dc:char(nil, SLIDER_TRACK) + for stop_idx=1,self.num_stops-1 do + local track_stop_pen = SLIDER_TRACK_STOP_SELECTED + local track_pen = SLIDER_TRACK_SELECTED + if left_idx > stop_idx or right_idx < stop_idx then + track_stop_pen = SLIDER_TRACK_STOP + track_pen = SLIDER_TRACK + elseif right_idx == stop_idx then + track_pen = SLIDER_TRACK end - return "Generic building material"; + dc:char(nil, track_stop_pen) + for i=2,width_per_idx do + dc:char(nil, track_pen) + end + end + if right_idx >= self.num_stops then + dc:char(nil, SLIDER_TRACK_STOP_SELECTED) + else + dc:char(nil, SLIDER_TRACK_STOP) + end + dc:char(nil, SLIDER_TRACK) + dc:char(nil, SLIDER_RIGHT_END) + -- draw tabs + dc:seek(width_per_idx*(left_idx-1)) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + dc:seek(width_per_idx*(right_idx-1)+4) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + -- manage dragging + if self.is_dragging_target then + slider_do_drag(self, width_per_idx) end - if filter.vector_id then - return to_title_case(df.job_item_vector_id[filter.vector_id]) + if df.global.enabler.mouse_lbut == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil end - return "Unknown"; end +-------------------------------- +-- QualityAndMaterialsPage +-- + +QualityAndMaterialsPage = defclass(QualityAndMaterialsPage, widgets.Panel) +QualityAndMaterialsPage.ATTRS{ + frame={t=0, l=0}, + index=DEFAULT_NIL, +} + +local TYPE_COL_WIDTH = 20 +local HEADER_HEIGHT = 8 +local QUALITY_HEIGHT = 9 +local FOOTER_HEIGHT = 4 + -- returns whether the items matched by the specified filter can have a quality -- rating. This also conveniently indicates whether an item can be decorated. --- does not need the core suspended --- reverse_idx is 0-based and is expected to be counted from the *last* filter -function item_can_be_improved(btype, subtype, custom, reverse_idx) - local filter = get_filter(btype, subtype, custom, reverse_idx) +local function can_be_improved(idx) + local filter = get_cur_filters()[idx] if filter.flags2 and filter.flags2.building_material then return false; end @@ -106,41 +639,1128 @@ function item_can_be_improved(btype, subtype, custom, reverse_idx) filter.item_type ~= df.item_type.BOULDER end --- needs the core suspended --- returns a vector of constructed buildings (usually of size 1, but potentially --- more for constructions) -function construct_buildings_from_ui_state() - local uibs = df.global.buildreq - local world = df.global.world - local direction = world.selected_direction - local _, width, height = dfhack.buildings.getCorrectSize( - world.building_width, world.building_height, uibs.building_type, - uibs.building_subtype, uibs.custom_type, direction) - -- the cursor is at the center of the building; we need the upper-left - -- corner of the building - local pos = guidm.getCursorPos() - pos.x = pos.x - math.floor(width/2) - pos.y = pos.y - math.floor(height/2) - local min_x, max_x = pos.x, pos.x - local min_y, max_y = pos.y, pos.y - if width == 1 and height == 1 and - (world.building_width > 1 or world.building_height > 1) then - min_x = math.ceil(pos.x - world.building_width/2) - max_x = min_x + world.building_width - 1 - min_y = math.ceil(pos.y - world.building_height/2) - max_y = min_y + world.building_height - 1 +function QualityAndMaterialsPage:init() + self.lowest_other_item_heat_safety = 2 + self.dirty = true + + local enable_item_quality = can_be_improved(self.index) + + self:addviews{ + widgets.Panel{ + view_id='header', + frame={l=0, t=0, h=HEADER_HEIGHT, r=0}, + frame_inset={l=1}, + subviews={ + widgets.Label{ + frame={l=0, t=0, h=1, r=0}, + text={ + 'Current filter:', + {gap=1, pen=COLOR_LIGHTCYAN, text=self:callback('get_summary')} + }, + }, + widgets.CycleHotkeyLabel{ + view_id='safety', + frame={t=2, l=0, w=35}, + key='CUSTOM_SHIFT_G', + label='Building heat safety:', + options={ + {label='Fire Magma', value=0, pen=COLOR_GREY}, + {label='Fire Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, + }, + on_change=self:callback('set_heat_safety'), + }, + widgets.Label{ + frame={t=2, l=30}, + text='Magma', + auto_width=true, + text_pen=COLOR_GREY, + visible=function() return self.subviews.safety:getOptionValue() == 1 end, + }, + widgets.Label{ + frame={t=3, l=3}, + text='Other items for this building may not be able to use all of their selected materials.', + visible=function() return self.subviews.safety:getOptionValue() > self.lowest_other_item_heat_safety end, + }, + widgets.EditField{ + frame={l=0, t=4, w=23}, + label_text='Search: ', + on_char=function(ch) return ch:match('%l') end, + }, + widgets.CycleHotkeyLabel{ + frame={l=24, t=4, w=21}, + label='Sort by:', + key='CUSTOM_SHIFT_R', + options={'name', 'available'}, + }, + widgets.ToggleHotkeyLabel{ + frame={l=24, t=5, w=24}, + label='Hide unavailable:', + key='CUSTOM_SHIFT_H', + initial_option=false, + }, + widgets.Label{ + frame={l=1, b=0}, + text='Type', + text_pen=COLOR_LIGHTRED, + }, + widgets.Label{ + frame={l=TYPE_COL_WIDTH, b=0}, + text='Material', + text_pen=COLOR_LIGHTRED, + }, + }, + }, + widgets.Panel{ + view_id='materials_lists', + frame={l=0, t=HEADER_HEIGHT, r=0, b=FOOTER_HEIGHT+QUALITY_HEIGHT}, + frame_style=gui.INTERIOR_FRAME, + subviews={ + widgets.List{ + view_id='materials_categories', + frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3}, + scroll_keys={}, + choices={ + {text='Stone', key='CUSTOM_SHIFT_S'}, + {text='Wood', key='CUSTOM_SHIFT_O'}, + {text='Metal', key='CUSTOM_SHIFT_M'}, + {text='Other', key='CUSTOM_SHIFT_T'}, + }, + }, + widgets.List{ + view_id='materials_mats', + frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0}, + choices={ + {text='9 - granite'}, + {text='0 - graphite'}, + }, + }, + }, + }, + widgets.Panel{ + view_id='divider', + frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=FOOTER_HEIGHT+QUALITY_HEIGHT, w=1}, + on_render=self:callback('draw_divider'), + }, + widgets.Panel{ + view_id='quality_panel', + frame={l=0, r=0, h=QUALITY_HEIGHT, b=FOOTER_HEIGHT}, + frame_style=gui.INTERIOR_FRAME, + frame_title='Item quality', + subviews={ + widgets.CycleHotkeyLabel{ + view_id='decorated', + frame={l=0, t=1, w=23}, + key='CUSTOM_SHIFT_D', + label='Decorated only:', + options={ + {label='No', value=false}, + {label='Yes', value=true}, + }, + enabled=enable_item_quality, + on_change=self:callback('set_decorated'), + }, + widgets.CycleHotkeyLabel{ + view_id='min_quality', + frame={l=0, t=3, w=18}, + label='Min quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Z', + key='CUSTOM_SHIFT_X', + options={ + {label='Ordinary', value=0}, + {label='Well Crafted', value=1}, + {label='Finely Crafted', value=2}, + {label='Superior', value=3}, + {label='Exceptional', value=4}, + {label='Masterful', value=5}, + {label='Artifact', value=6}, + }, + enabled=enable_item_quality, + on_change=function(val) self:set_min_quality(val+1) end, + }, + widgets.CycleHotkeyLabel{ + view_id='max_quality', + frame={r=1, t=3, w=18}, + label='Max quality:', + label_below=true, + key_back='CUSTOM_SHIFT_Q', + key='CUSTOM_SHIFT_W', + options={ + {label='Ordinary', value=0}, + {label='Well Crafted', value=1}, + {label='Finely Crafted', value=2}, + {label='Superior', value=3}, + {label='Exceptional', value=4}, + {label='Masterful', value=5}, + {label='Artifact', value=6}, + }, + enabled=enable_item_quality, + on_change=function(val) self:set_max_quality(val+1) end, + }, + Slider{ + frame={l=0, t=6}, + num_stops=7, + get_left_idx_fn=function() + return self.subviews.min_quality:getOptionValue() + 1 + end, + get_right_idx_fn=function() + return self.subviews.max_quality:getOptionValue() + 1 + end, + on_left_change=self:callback('set_min_quality'), + on_right_change=self:callback('set_max_quality'), + active=enable_item_quality, + }, + }, + }, + widgets.Panel{ + view_id='footer', + frame={l=0, r=0, b=0, h=FOOTER_HEIGHT}, + frame_inset={t=1, l=1}, + subviews={ + widgets.HotkeyLabel{ + frame={l=0, t=0}, + label='Toggle', + auto_width=true, + key='SELECT', + }, + widgets.HotkeyLabel{ + frame={l=0, t=2}, + label='Done', + auto_width=true, + key='LEAVESCREEN', + }, + widgets.HotkeyLabel{ + frame={l=30, t=0}, + label='Select all', + auto_width=true, + key='CUSTOM_SHIFT_A', + }, + widgets.HotkeyLabel{ + frame={l=30, t=1}, + label='Invert selection', + auto_width=true, + key='CUSTOM_SHIFT_I', + }, + widgets.HotkeyLabel{ + frame={l=30, t=2}, + label='Clear selection', + auto_width=true, + key='CUSTOM_SHIFT_C', + }, + }, + } + } +end + +function QualityAndMaterialsPage:refresh() + local summary = '' + local subviews = self.subviews + + local heat = getHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type) + subviews.safety:setOption(heat) + if heat >= 2 then summary = summary .. 'Magma safe ' + elseif heat == 1 then summary = summary .. 'Fire safe ' + end + + local quality = getQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) + subviews.decorated:setOption(quality.decorated ~= 0) + subviews.min_quality:setOption(quality.min_quality) + subviews.max_quality:setOption(quality.max_quality) + + self.summary = summary + self.dirty = false +end + +function QualityAndMaterialsPage:get_summary() + -- TODO: summarize materials + return self.summary +end + +function QualityAndMaterialsPage:set_heat_safety(heat) + setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) + self.dirty = true +end + +function QualityAndMaterialsPage:set_decorated(decorated) + local subviews = self.subviews + setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, + decorated and 1 or 0, subviews.min_quality:getOptionValue(), subviews.max_quality:getOptionValue()) + self.dirty = true +end + +function QualityAndMaterialsPage:set_min_quality(idx) + idx = math.min(6, math.max(0, idx-1)) + local subviews = self.subviews + subviews.min_quality:setOption(idx) + if subviews.max_quality:getOptionValue() < idx then + subviews.max_quality:setOption(idx) + end + setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, + subviews.decorated:getOptionValue() and 1 or 0, idx, subviews.max_quality:getOptionValue()) + self.dirty = true +end + +function QualityAndMaterialsPage:set_max_quality(idx) + idx = math.min(6, math.max(0, idx-1)) + local subviews = self.subviews + subviews.max_quality:setOption(idx) + if subviews.min_quality:getOptionValue() > idx then + subviews.min_quality:setOption(idx) + end + setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, + subviews.decorated:getOptionValue() and 1 or 0, subviews.min_quality:getOptionValue(), idx) + self.dirty = true +end + +local texpos = dfhack.textures.getThinBordersTexposStart() +local tp = function(offset) + if texpos == -1 then return nil end + return texpos + offset +end + +local TOP_PEN = to_pen{tile=tp(10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK} +local MID_PEN = to_pen{tile=tp(4), ch=192, fg=COLOR_GREY, bg=COLOR_BLACK} +local BOT_PEN = to_pen{tile=tp(11), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} + +function QualityAndMaterialsPage:draw_divider(dc) + local y2 = dc.height - 1 + for y=0,y2 do + dc:seek(0, y) + if y == 0 then + dc:char(nil, TOP_PEN) + elseif y == y2 then + dc:char(nil, BOT_PEN) + else + dc:char(nil, MID_PEN) + end + end +end + +function QualityAndMaterialsPage:onRenderFrame(dc, rect) + QualityAndMaterialsPage.super.onRenderFrame(self, dc, rect) + if self.dirty then + self:refresh() + end +end + +-------------------------------- +-- GlobalSettingsPage +-- + +GlobalSettingsPage = defclass(GlobalSettingsPage, widgets.ResizingPanel) +GlobalSettingsPage.ATTRS{ + autoarrange_subviews=true, + frame={t=0, l=0}, + frame_inset={l=1, r=1}, +} + +function GlobalSettingsPage:init() + self:addviews{ + widgets.WrappedLabel{ + frame={l=0}, + text_to_wrap='These options will affect the selection of "Generic Materials" for all future buildings.', + }, + widgets.Panel{ + frame={h=1}, + }, + widgets.ToggleHotkeyLabel{ + view_id='blocks', + frame={l=0}, + key='CUSTOM_B', + label='Blocks', + label_width=8, + on_change=self:callback('update_setting', 'blocks'), + }, + widgets.ToggleHotkeyLabel{ + view_id='logs', + frame={l=0}, + key='CUSTOM_L', + label='Logs', + label_width=8, + on_change=self:callback('update_setting', 'logs'), + }, + widgets.ToggleHotkeyLabel{ + view_id='boulders', + frame={l=0}, + key='CUSTOM_O', + label='Boulders', + label_width=8, + on_change=self:callback('update_setting', 'boulders'), + }, + widgets.ToggleHotkeyLabel{ + view_id='bars', + frame={l=0}, + key='CUSTOM_R', + label='Bars', + label_width=8, + on_change=self:callback('update_setting', 'bars'), + }, + } + + self:init_settings() +end + +function GlobalSettingsPage:init_settings() + local settings = getGlobalSettings() + local subviews = self.subviews + subviews.blocks:setOption(settings.blocks) + subviews.logs:setOption(settings.logs) + subviews.boulders:setOption(settings.boulders) + subviews.bars:setOption(settings.bars) +end + +function GlobalSettingsPage:update_setting(setting, val) + dfhack.run_command('buildingplan', 'set', setting, tostring(val)) + self:init_settings() +end + +-------------------------------- +-- FilterSelection +-- + +FilterSelection = defclass(FilterSelection, widgets.Window) +FilterSelection.ATTRS{ + frame_title='Choose filters [MOCK -- NOT FUNCTIONAL]', + frame={w=53, h=53, l=30, t=8}, + frame_inset={t=1}, + resizable=true, + index=DEFAULT_NIL, + autoarrange_subviews=true, +} + +function FilterSelection:init() + self:addviews{ + widgets.TabBar{ + frame={t=0}, + labels={ + 'Quality and materials', + 'Global settings', + }, + on_select=function(idx) + self.subviews.pages:setSelected(idx) + self:updateLayout() + end, + get_cur_page=function() return self.subviews.pages:getSelected() end, + key='CUSTOM_CTRL_T', + }, + widgets.Widget{ + frame={h=1}, + }, + widgets.Pages{ + view_id='pages', + frame={t=5, l=0, b=0, r=0}, + subviews={ + QualityAndMaterialsPage{index=self.index}, + GlobalSettingsPage{}, + }, + }, + } +end + +FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) +FilterSelectionScreen.ATTRS { + focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection', + index=DEFAULT_NIL, +} + +function FilterSelectionScreen:init() + self:addviews{ + FilterSelection{index=self.index} + } +end + +function FilterSelectionScreen:onShow() + -- don't let the building "shadow" follow the mouse cursor while this screen is open + df.global.game.main_interface.bottom_mode_selected = -1 +end + +function FilterSelectionScreen:onDismiss() + -- re-enable building shadow + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT +end + +-------------------------------- +-- ItemLine +-- + +local function cur_building_has_no_area() + if uibs.building_type == df.building_type.Construction then return false end + local filters = dfhack.buildings.getFiltersByType({}, + uibs.building_type, uibs.building_subtype, uibs.custom_type) + -- this works because all variable-size buildings have either no item + -- filters or a quantity of -1 for their first (and only) item + return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) +end + +local function is_plannable() + return get_cur_filters() and + not (uibs.building_type == df.building_type.Construction + and uibs.building_subtype == df.construction_type.TrackNSEW) +end + +local function is_construction() + return uibs.building_type == df.building_type.Construction +end + +local function is_stairs() + return is_construction() + and uibs.building_subtype == df.construction_type.UpDownStair +end + +local direction_panel_frame = {t=4, h=13, w=46, r=28} + +local direction_panel_types = utils.invert{ + df.building_type.Bridge, + df.building_type.ScrewPump, + df.building_type.WaterWheel, + df.building_type.AxleHorizontal, + df.building_type.Rollers, +} + +local function has_direction_panel() + return direction_panel_types[uibs.building_type] + or (uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.TrackStop) +end + +local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28} + +local function has_pressure_plate_panel() + return uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.PressurePlate +end + +local function is_over_options_panel() + local frame = nil + if has_direction_panel() then + frame = direction_panel_frame + elseif has_pressure_plate_panel() then + frame = pressure_plate_panel_frame + else + return false + end + local v = widgets.Widget{frame=frame} + local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) + v:updateLayout(gui.ViewRect{rect=rect}) + return v:getMousePos() +end + +local function to_title_case(str) + str = str:gsub('(%a)([%w_]*)', + function (first, rest) return first:upper()..rest:lower() end) + str = str:gsub('_', ' ') + return str +end + +ItemLine = defclass(ItemLine, widgets.Panel) +ItemLine.ATTRS{ + idx=DEFAULT_NIL, + is_selected_fn=DEFAULT_NIL, + is_hollow_fn=DEFAULT_NIL, + on_select=DEFAULT_NIL, + on_filter=DEFAULT_NIL, + on_clear_filter=DEFAULT_NIL, +} + +function ItemLine:init() + self.frame.h = 1 + self.visible = function() return #get_cur_filters() >= self.idx end + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='*', + auto_width=true, + visible=self.is_selected_fn, + }, + widgets.Label{ + frame={t=0, l=25}, + text={ + {tile=get_button_start_pen}, + {gap=6, tile=get_button_end_pen}, + }, + auto_width=true, + on_click=function() self.on_filter(self.idx) end, + }, + widgets.Label{ + frame={t=0, l=33}, + text={ + {tile=get_button_start_pen}, + {gap=1, tile=get_button_end_pen}, + }, + auto_width=true, + on_click=function() self.on_clear_filter(self.idx) end, + }, + widgets.Label{ + frame={t=0, l=2}, + text={ + {width=21, text=self:callback('get_item_line_text')}, + {gap=3, text='filter', pen=COLOR_GREEN}, + {gap=2, text='x', pen=self:callback('get_x_pen')}, + {gap=3, text=function() return self.note end, + pen=function() return self.note_pen end}, + }, + }, + } +end + +function ItemLine:reset() + self.desc = nil + self.available = nil +end + +function ItemLine:onInput(keys) + if keys._MOUSE_L_DOWN and self:getMousePos() then + self.on_select(self.idx) + end + return ItemLine.super.onInput(self, keys) +end + +function ItemLine:get_x_pen() + return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) and + COLOR_GREEN or COLOR_GREY +end + +function get_desc(filter) + local desc = 'Unknown' + if filter.has_tool_use and filter.has_tool_use > -1 then + desc = to_title_case(df.tool_uses[filter.has_tool_use]) + elseif filter.flags2 and filter.flags2.screw then + desc = 'Screw' + elseif filter.item_type and filter.item_type > -1 then + desc = to_title_case(df.item_type[filter.item_type]) + elseif filter.vector_id and filter.vector_id > -1 then + desc = to_title_case(df.job_item_vector_id[filter.vector_id]) + elseif filter.flags2 and filter.flags2.building_material then + desc = 'Building material'; + if filter.flags2.fire_safe then + desc = 'Fire-safe material'; + end + if filter.flags2.magma_safe then + desc = 'Magma-safe material'; + end + end + + if desc:endswith('s') then + desc = desc:sub(1,-2) + end + if desc == 'Trappart' then + desc = 'Mechanism' + elseif desc == 'Wood' then + desc = 'Log' + end + return desc +end + +function ItemLine:get_item_line_text() + local idx = self.idx + local filter = get_cur_filters()[idx] + local quantity = get_quantity(filter, self.is_hollow_fn()) + + self.desc = self.desc or get_desc(filter) + + self.available = self.available or countAvailableItems(uibs.building_type, + uibs.building_subtype, uibs.custom_type, idx - 1) + if self.available >= quantity then + self.note_pen = COLOR_GREEN + self.note = 'Available now' + else + self.note_pen = COLOR_YELLOW + self.note = 'Will link later' + end + + return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') +end + +function ItemLine:reduce_quantity(used_quantity) + if not self.available then return end + local filter = get_cur_filters()[self.idx] + used_quantity = used_quantity or get_quantity(filter, self.is_hollow_fn()) + self.available = math.max(0, self.available - used_quantity) +end + +local function get_placement_errors() + local out = '' + for _,str in ipairs(uibs.errors) do + if #out > 0 then out = out .. NEWLINE end + out = out .. str.value + end + return out +end + +-------------------------------- +-- PlannerOverlay +-- + +PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) +PlannerOverlay.ATTRS{ + default_pos={x=5,y=9}, + default_enabled=true, + viewscreens='dwarfmode/Building/Placement', + frame={w=56, h=20}, +} + +function PlannerOverlay:init() + self.selected = 1 + + local main_panel = widgets.Panel{ + view_id='main', + frame={t=0, l=0, r=0, h=14}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + } + + local function make_is_selected_fn(idx) + return function() return self.selected == idx end + end + + local function on_select_fn(idx) + self.selected = idx + end + + local function is_hollow_fn() + return self.subviews.hollow:getOptionValue() + end + + main_panel:addviews{ + widgets.Label{ + frame={}, + auto_width=true, + text='No items required.', + visible=function() return #get_cur_filters() == 0 end, + }, + ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, + is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, + is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, + is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, + is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), + on_clear_filter=self:callback('clear_filter')}, + widgets.CycleHotkeyLabel{ + view_id='hollow', + frame={t=3, l=4}, + key='CUSTOM_H', + label='Hollow area:', + visible=is_construction, + options={ + {label='No', value=false}, + {label='Yes', value=true}, + }, + }, + widgets.CycleHotkeyLabel{ + view_id='stairs_top_subtype', + frame={t=4, l=4}, + key='CUSTOM_R', + label='Top Stair Type: ', + visible=is_stairs, + options={ + {label='Auto', value='auto'}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Down', value=df.construction_type.DownStair}, + }, + }, + widgets.CycleHotkeyLabel { + view_id='stairs_bottom_subtype', + frame={t=5, l=4}, + key='CUSTOM_B', + label='Bottom Stair Type: ', + visible=is_stairs, + options={ + {label='Auto', value='auto'}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Up', value=df.construction_type.UpStair}, + }, + }, + widgets.Label{ + frame={b=3, l=17}, + text={ + 'Selected area: ', + {text=function() + return ('%dx%dx%d'):format(get_cur_area_dims(self.saved_placement)) + end + }, + }, + visible=function() + return not cur_building_has_no_area() and (self.saved_placement or is_choosing_area()) + end, + }, + widgets.Panel{ + visible=function() return #get_cur_filters() > 0 end, + subviews={ + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='STRING_A042', + auto_width=true, + enabled=function() return #get_cur_filters() > 1 end, + on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=1}, + key='STRING_A047', + label='Prev/next item', + auto_width=true, + enabled=function() return #get_cur_filters() > 1 end, + on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=21}, + key='CUSTOM_F', + label='Set filter', + auto_width=true, + on_activate=function() self:set_filter(self.selected) end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=37}, + key='CUSTOM_X', + label='Clear filter', + auto_width=true, + on_activate=function() self:clear_filter(self.selected) end, + enabled=function() + return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + end + }, + widgets.CycleHotkeyLabel{ + view_id='choose', + frame={b=0, l=0, w=25}, + key='CUSTOM_I', + label='Choose from items:', + options={{label='Yes', value=true}, + {label='No', value=false}}, + initial_option=false, + enabled=function() + for idx = 1,4 do + if (self.subviews['item'..idx].available or 0) > 0 then + return true + end + end + end, + }, + widgets.CycleHotkeyLabel{ + view_id='safety', + frame={b=0, l=29, w=25}, + key='CUSTOM_G', + label='Building safety:', + options={ + {label='Any', value=0}, + {label='Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, + }, + initial_option=0, + on_change=function(heat) + setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) + end, + }, + }, + }, + } + + local error_panel = widgets.ResizingPanel{ + view_id='errors', + frame={t=14, l=0, r=0}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + } + + error_panel:addviews{ + widgets.WrappedLabel{ + frame={t=0, l=0, r=0}, + text_pen=COLOR_LIGHTRED, + text_to_wrap=get_placement_errors, + visible=function() return #uibs.errors > 0 end, + }, + widgets.Label{ + frame={t=0, l=0, r=0}, + text_pen=COLOR_GREEN, + text='OK to build', + visible=function() return #uibs.errors == 0 end, + }, + } + + self:addviews{ + main_panel, + error_panel, + } +end + +function PlannerOverlay:reset() + self.subviews.item1:reset() + self.subviews.item2:reset() + self.subviews.item3:reset() + self.subviews.item4:reset() + reset_counts_flag = false +end + +function PlannerOverlay:set_filter(idx) + FilterSelectionScreen{index=idx}:show() +end + +function PlannerOverlay:clear_filter(idx) + clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) +end + +local function get_placement_data() + local pos = uibs.pos + local direction = uibs.direction + local width, height, depth = get_cur_area_dims() + local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( + width, height, uibs.building_type, uibs.building_subtype, + uibs.custom_type, direction) + -- get the upper-left corner of the building/area at min z-level + local has_selection = is_choosing_area() + local start_pos = xyz2pos( + has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2, + has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2, + has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z + ) + if uibs.building_type == df.building_type.ScrewPump then + if direction == df.screw_pump_direction.FromSouth then + start_pos.y = start_pos.y + 1 + elseif direction == df.screw_pump_direction.FromEast then + start_pos.x = start_pos.x + 1 + end + end + local min_x, max_x = start_pos.x, start_pos.x + local min_y, max_y = start_pos.y, start_pos.y + local min_z, max_z = start_pos.z, start_pos.z + if adjusted_width == 1 and adjusted_height == 1 + and (width > 1 or height > 1 or depth > 1) then + max_x = min_x + width - 1 + max_y = min_y + height - 1 + max_z = math.max(uibs.selection_pos.z, pos.z) + end + return { + p1=xyz2pos(min_x, min_y, min_z), + p2=xyz2pos(max_x, max_y, max_z), + width=adjusted_width, + height=adjusted_height + } +end + +function PlannerOverlay:save_placement() + self.saved_placement = get_placement_data() + if (uibs.selection_pos:isValid()) then + self.saved_selection_pos_valid = true + self.saved_selection_pos = copyall(uibs.selection_pos) + self.saved_pos = copyall(uibs.pos) + uibs.selection_pos:clear() + else + self.saved_selection_pos = copyall(self.saved_placement.p1) + self.saved_pos = copyall(self.saved_placement.p2) + self.saved_pos.x = self.saved_pos.x + self.saved_placement.width - 1 + self.saved_pos.y = self.saved_pos.y + self.saved_placement.height - 1 + end +end + +function PlannerOverlay:restore_placement() + if self.saved_selection_pos_valid then + uibs.selection_pos = self.saved_selection_pos + self.saved_selection_pos_valid = nil + else + uibs.selection_pos:clear() + end + self.saved_selection_pos = nil + self.saved_pos = nil + local placement_data = self.saved_placement + self.saved_placement = nil + return placement_data +end + +function PlannerOverlay:onInput(keys) + if not is_plannable() then return false end + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if uibs.selection_pos:isValid() then + uibs.selection_pos:clear() + return true + end + self.selected = 1 + self.subviews.hollow:setOption(false) + self.subviews.choose:setOption(false) + self:reset() + reset_counts_flag = true + return false + end + if PlannerOverlay.super.onInput(self, keys) then + return true end + if keys._MOUSE_L_DOWN then + if is_over_options_panel() then return false end + local detect_rect = copyall(self.frame_rect) + detect_rect.height = self.subviews.main.frame_rect.height + + self.subviews.errors.frame_rect.height + detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1 + if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect}) + or self.subviews.errors:getMousePos() then + return true + end + if not is_construction() and #uibs.errors > 0 then return true end + if dfhack.gui.getMousePos() then + if is_choosing_area() or cur_building_has_no_area() then + local filters = get_cur_filters() + local num_filters = #filters + if num_filters == 0 then + return false -- we don't add value; let the game place it + end + local choose = self.subviews.choose + if choose.enabled() and choose:getOptionValue() then + self:save_placement() + local is_hollow = self.subviews.hollow:getOptionValue() + local chosen_items, active_screens = {}, {} + local pending = num_filters + df.global.game.main_interface.bottom_mode_selected = -1 + for idx = num_filters,1,-1 do + chosen_items[idx] = {} + if (self.subviews['item'..idx].available or 0) > 0 then + active_screens[idx] = ItemSelectionScreen{ + index=idx, + quantity=get_quantity(filters[idx], is_hollow, + self.saved_placement), + on_submit=function(items) + chosen_items[idx] = items + active_screens[idx]:dismiss() + active_screens[idx] = nil + pending = pending - 1 + if pending == 0 then + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT + self:place_building(self:restore_placement(), chosen_items) + end + end, + on_cancel=function() + for i,scr in pairs(active_screens) do + scr:dismiss() + end + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT + self:restore_placement() + end, + }:show() + else + pending = pending - 1 + end + end + else + self:place_building(get_placement_data()) + end + return true + elseif not is_choosing_area() then + return false + end + end + end + return keys._MOUSE_L +end + +function PlannerOverlay:render(dc) + if not is_plannable() then return end + self.subviews.errors:updateLayout() + PlannerOverlay.super.render(self, dc) +end + +local GOOD_PEN, BAD_PEN +function reload_cursors() + GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} + BAD_PEN = to_pen{ch='X', fg=COLOR_RED, tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)} +end +reload_cursors() + +local ONE_BY_ONE = xy2pos(1, 1) + +function PlannerOverlay:onRenderFrame(dc, rect) + PlannerOverlay.super.onRenderFrame(self, dc, rect) + + if reset_counts_flag then + self:reset() + self.subviews.safety:setOption(getHeatSafetyFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type)) + end + + local selection_pos = self.saved_selection_pos or uibs.selection_pos + if not selection_pos or selection_pos.x < 0 then return end + + local pos = self.saved_pos or uibs.pos + local bounds = { + x1 = math.max(0, math.min(selection_pos.x, pos.x)), + x2 = math.min(df.global.world.map.x_count-1, math.max(selection_pos.x, pos.x)), + y1 = math.max(0, math.min(selection_pos.y, pos.y)), + y2 = math.min(df.global.world.map.y_count-1, math.max(selection_pos.y, pos.y)), + } + + local hollow = self.subviews.hollow:getOptionValue() + local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN + + local get_pen_fn = is_construction() and function(pos) + return dfhack.buildings.checkFreeTiles(pos, ONE_BY_ONE) and GOOD_PEN or BAD_PEN + end or function() + return default_pen + end + + local function get_overlay_pen(pos) + if not hollow then return get_pen_fn(pos) end + if pos.x == bounds.x1 or pos.x == bounds.x2 or + pos.y == bounds.y1 or pos.y == bounds.y2 then + return get_pen_fn(pos) + end + return gui.TRANSPARENT_PEN + end + + guidm.renderMapOverlay(get_overlay_pen, bounds) +end + +function PlannerOverlay:get_stairs_subtype(pos, corner1, corner2) + local subtype = uibs.building_subtype + if pos.z == corner1.z then + local opt = self.subviews.stairs_bottom_subtype:getOptionValue() + if opt == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape ~= df.tiletype_shape.STAIR_DOWN then + subtype = df.construction_type.UpStair + end + else + subtype = opt + end + elseif pos.z == corner2.z then + local opt = self.subviews.stairs_top_subtype:getOptionValue() + if opt == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape ~= df.tiletype_shape.STAIR_UP then + subtype = df.construction_type.DownStair + end + else + subtype = opt + end + end + return subtype +end + +function PlannerOverlay:place_building(placement_data, chosen_items) + local p1, p2 = placement_data.p1, placement_data.p2 local blds = {} - for y=min_y,max_y do for x=min_x,max_x do - local bld, err = dfhack.buildings.constructBuilding{ - type=uibs.building_type, subtype=uibs.building_subtype, - custom=uibs.custom_type, pos=xyz2pos(x, y, pos.z), - width=width, height=height, direction=direction} + local hollow = self.subviews.hollow:getOptionValue() + local subtype = uibs.building_subtype + for z=p1.z,p2.z do for y=p1.y,p2.y do for x=p1.x,p2.x do + if hollow and x ~= p1.x and x ~= p2.x and y ~= p1.y and y ~= p2.y then + goto continue + end + local pos = xyz2pos(x, y, z) + if is_stairs() then + subtype = self:get_stairs_subtype(pos, p1, p2) + end + local bld, err = dfhack.buildings.constructBuilding{pos=pos, + type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, + width=placement_data.width, height=placement_data.height, + direction=uibs.direction} if err then - for _,b in ipairs(blds) do - dfhack.buildings.deconstruct(b) - end - error(err) + -- it's ok if some buildings fail to build + goto continue end -- assign fields for the types that need them. we can't pass them all in -- to the call to constructBuilding since attempting to assign unrelated @@ -153,133 +1773,183 @@ function construct_buildings_from_ui_state() if k == 'speed' then bld.speed = uibs.speed end end table.insert(blds, bld) - end end - return blds + ::continue:: + end end end + local used_quantity = is_construction() and #blds or false + self.subviews.item1:reduce_quantity(used_quantity) + self.subviews.item2:reduce_quantity(used_quantity) + self.subviews.item3:reduce_quantity(used_quantity) + self.subviews.item4:reduce_quantity(used_quantity) + for _,bld in ipairs(blds) do + -- attach chosen items and reduce job_item quantity + if chosen_items then + local job = bld.jobs[0] + local jitems = job.job_items + for idx=1,#get_cur_filters() do + local item_ids = chosen_items[idx] + while jitems[idx-1].quantity > 0 and #item_ids > 0 do + local item_id = item_ids[#item_ids] + local item = df.item.find(item_id) + if not item then + dfhack.printerr(('item no longer available: %d'):format(item_id)) + break + end + if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then + dfhack.printerr(('cannot attach item: %d'):format(item_id)) + break + end + jitems[idx-1].quantity = jitems[idx-1].quantity - 1 + item_ids[#item_ids] = nil + end + end + end + addPlannedBuilding(bld) + end + scheduleCycle() + uibs.selection_pos:clear() end --- --- GlobalSettings dialog +-------------------------------- +-- InspectorLine -- -local GlobalSettings = defclass(GlobalSettings, dialogs.MessageBox) -GlobalSettings.focus_path = 'buildingplan_globalsettings' +local function get_building_filters() + local bld = dfhack.gui.getSelectedBuilding() + return dfhack.buildings.getFiltersByType({}, + bld:getType(), bld:getSubtype(), bld:getCustomType()) +end -GlobalSettings.ATTRS{ - settings = {} +InspectorLine = defclass(InspectorLine, widgets.Panel) +InspectorLine.ATTRS{ + idx=DEFAULT_NIL, } -function GlobalSettings:onDismiss() - for k,v in pairs(self.settings) do - -- call back into C++ to save changes - setSetting(k, v) - end +function InspectorLine:init() + self.frame.h = 2 + self.visible = function() return #get_building_filters() >= self.idx end + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={{text=self:callback('get_desc_string')}}, + }, + widgets.Label{ + frame={t=1, l=2}, + text={{text=self:callback('get_status_line')}}, + }, + } end --- does not need the core suspended. -function show_global_settings_dialog(settings) - GlobalSettings{ - frame_title="Buildingplan Global Settings", - settings=settings, - }:show() +function InspectorLine:get_desc_string() + if self.desc then return self.desc end + self.desc = getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1) + return self.desc end -function GlobalSettings:toggle_setting(name) - self.settings[name] = not self.settings[name] +function InspectorLine:get_status_line() + if self.status then return self.status end + local queue_pos = getQueuePosition(dfhack.gui.getSelectedBuilding(), self.idx-1) + if queue_pos <= 0 then + return 'Item attached' + end + self.status = ('Position in line: %d'):format(queue_pos) + return self.status end -function GlobalSettings:get_setting_string(name) - if self.settings[name] then return 'On' end - return 'Off' +function InspectorLine:reset() + self.desc = nil + self.status = nil end -function GlobalSettings:get_setting_pen(name) - if self.settings[name] then return COLOR_LIGHTGREEN end - return COLOR_LIGHTRED +-------------------------------- +-- InspectorOverlay +-- + +InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) +InspectorOverlay.ATTRS{ + default_pos={x=-41,y=14}, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING', + frame={w=30, h=15}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function InspectorOverlay:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Waiting for items:', + }, + InspectorLine{view_id='item1', frame={t=2, l=0}, idx=1}, + InspectorLine{view_id='item2', frame={t=4, l=0}, idx=2}, + InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3}, + InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4}, + widgets.HotkeyLabel{ + frame={t=11, l=0}, + label='adjust filters', + key='CUSTOM_CTRL_F', + }, + widgets.HotkeyLabel{ + frame={t=12, l=0}, + label='make top priority', + key='CUSTOM_CTRL_T', + on_activate=self:callback('make_top_priority'), + }, + } end -function GlobalSettings:is_setting_enabled(name) - return self.settings[name] +function InspectorOverlay:reset() + self.subviews.item1:reset() + self.subviews.item2:reset() + self.subviews.item3:reset() + self.subviews.item4:reset() + reset_inspector_flag = false end -function GlobalSettings:make_setting_label_token(text, key, name, width) - return {text=text, key=key, key_sep=': ', key_pen=COLOR_LIGHTGREEN, - on_activate=self:callback('toggle_setting', name), width=width} +function InspectorOverlay:make_top_priority() + makeTopPriority(dfhack.gui.getSelectedBuilding()) + self:reset() end -function GlobalSettings:make_setting_value_token(name) - return {text=self:callback('get_setting_string', name), - enabled=self:callback('is_setting_enabled', name), - pen=self:callback('get_setting_pen', name), - dpen=COLOR_GRAY} +local RESUME_BUTTON_FRAME = {t=15, h=3, r=73, w=25} + +local function mouse_is_over_resume_button(rect) + local x,y = dfhack.screen.getMousePos() + if not x then return false end + if y < RESUME_BUTTON_FRAME.t or y > RESUME_BUTTON_FRAME.t + RESUME_BUTTON_FRAME.h - 1 then + return false + end + if x > rect.x2 - RESUME_BUTTON_FRAME.r + 1 or x < rect.x2 - RESUME_BUTTON_FRAME.r - RESUME_BUTTON_FRAME.w + 2 then + return false + end + return true end --- mockup: ---[[ - Buildingplan Global Settings - - e: Enable all: Off - Enables buildingplan for all building types. Use this to avoid having to - manually enable buildingplan for each building type that you want to plan. - Note that DFHack quickfort will use buildingplan to manage buildings - regardless of whether buildingplan is "enabled" for the building type. - - Allowed types for generic, fire-safe, and magma-safe building material: - b: Blocks: On - s: Boulders: On - w: Wood: On - r: Bars: Off - Changes to these settings will be applied to newly-planned buildings. - - A: Apply building material filter settings to existing planned buildings - Use this if your planned buildings can't be completed because the settings - above were too restrictive when the buildings were originally planned. - - M: Edit list of materials to avoid - potash - pearlash - ash - coal - Buildingplan will avoid using these material types when a planned building's - material filter is set to 'any'. They can stil be matched when they are - explicitly allowed by a planned building's material filter. Changes to this - list take effect for existing buildings immediately. - - g: Allow bags: Off - This allows bags to be placed where a 'coffer' is planned. - - f: Legacy Quickfort Mode: Off - Compatibility mode for the legacy Python-based Quickfort application. This - setting is not needed for DFHack quickfort. ---]] -function GlobalSettings:init() - - self.subviews.label:setText{ - self:make_setting_label_token('Enable all', 'CUSTOM_E', 'all_enabled', 12), - self:make_setting_value_token('all_enabled'), '\n', - ' Enables buildingplan for all building types. Use this to avoid having\n', - ' to manually enable buildingplan for each building type that you want\n', - ' to plan. Note that DFHack quickfort will use buildingplan to manage\n', - ' buildings regardless of whether buildingplan is "enabled" for the\n', - ' building type.\n', - '\n', - 'Allowed types for generic, fire-safe, and magma-safe building material:\n', - self:make_setting_label_token('Blocks', 'CUSTOM_B', 'blocks', 10), - self:make_setting_value_token('blocks'), '\n', - self:make_setting_label_token('Boulders', 'CUSTOM_S', 'boulders', 10), - self:make_setting_value_token('boulders'), '\n', - self:make_setting_label_token('Wood', 'CUSTOM_W', 'logs', 10), - self:make_setting_value_token('logs'), '\n', - self:make_setting_label_token('Bars', 'CUSTOM_R', 'bars', 10), - self:make_setting_value_token('bars'), '\n', - ' Changes to these settings will be applied to newly-planned buildings.\n', - ' If no types are enabled above, then any building material is allowed.\n', - '\n', - self:make_setting_label_token('Legacy Quickfort Mode', 'CUSTOM_F', - 'quickfort_mode', 23), - self:make_setting_value_token('quickfort_mode'), '\n', - ' Compatibility mode for the legacy Python-based Quickfort application.\n', - ' This setting is not needed for DFHack quickfort.' - } +function InspectorOverlay:onInput(keys) + if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then + return false + end + if keys._MOUSE_L_DOWN and mouse_is_over_resume_button(self.frame_parent_rect) then + return true + elseif keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then + self:reset() + end + return InspectorOverlay.super.onInput(self, keys) +end + +function InspectorOverlay:render(dc) + if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then + return + end + if reset_inspector_flag then + self:reset() + end + InspectorOverlay.super.render(self, dc) end +OVERLAY_WIDGETS = { + planner=PlannerOverlay, + inspector=InspectorOverlay, +} + return _ENV diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 56ad72372..3d476bf0d 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -493,6 +493,9 @@ function feed_viewscreen_widgets(vs_name, keys) return false end gui.markMouseClicksHandled(keys) + if keys._MOUSE_L_DOWN then + df.global.enabler.mouse_lbut = 0 + end return true end diff --git a/plugins/manipulator.cpp b/plugins/manipulator.cpp index 6731aa513..b8f6ce706 100644 --- a/plugins/manipulator.cpp +++ b/plugins/manipulator.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -1305,7 +1306,7 @@ void viewscreen_unitlaborsst::refreshNames() cur->job_mode = UnitInfo::JOB; } if (unit->military.squad_id > -1) { - cur->squad_effective_name = Units::getSquadName(unit); + cur->squad_effective_name = Military::getSquadName(unit->military.squad_id); cur->squad_info = stl_sprintf("%i", unit->military.squad_position + 1) + "." + cur->squad_effective_name; } else { cur->squad_effective_name = ""; diff --git a/plugins/remotefortressreader/remotefortressreader.cpp b/plugins/remotefortressreader/remotefortressreader.cpp index d1a269d52..f33044877 100644 --- a/plugins/remotefortressreader/remotefortressreader.cpp +++ b/plugins/remotefortressreader/remotefortressreader.cpp @@ -27,6 +27,7 @@ #include "modules/MapCache.h" #include "modules/Maps.h" #include "modules/Materials.h" +#include "modules/DFSDL.h" #include "modules/Translation.h" #include "modules/Units.h" #include "modules/World.h" @@ -2897,7 +2898,7 @@ static command_result PassKeyboardEvent(color_ostream &stream, const KeyboardEve e.key.ksym.scancode = in->scancode(); e.key.ksym.sym = (SDL::Key)in->sym(); e.key.ksym.unicode = in->unicode(); - SDL_PushEvent(&e); + DFHack::DFSDL::DFSDL_PushEvent(&e); #endif return CR_OK; } diff --git a/plugins/stonesense b/plugins/stonesense index 6570fe010..3e494d9d9 160000 --- a/plugins/stonesense +++ b/plugins/stonesense @@ -1 +1 @@ -Subproject commit 6570fe01081f7e402495bc5339b4ff7a1aabf305 +Subproject commit 3e494d9d968add443ebd63cc167933cc813f0eee diff --git a/scripts b/scripts index f2c2f6aa7..81183a380 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit f2c2f6aa7e7fe94871adf0a22d6966ddcac38afc +Subproject commit 81183a380b11f4c3045a7888c35afe215d2185ad