diff --git a/Lua API.html b/Lua API.html index 04af5d672..aa41b8b63 100644 --- a/Lua API.html +++ b/Lua API.html @@ -1749,7 +1749,10 @@ options; if multiple interpretations exist, the table will contain multiple keys

Maps to an integer in range 0-255. Duplicates a separate "STRING_A???" code for convenience.

_MOUSE_L, _MOUSE_R
-

If the left or right mouse button is pressed.

+

If the left or right mouse button is being pressed.

+
+
_MOUSE_L_DOWN, _MOUSE_R_DOWN
+

If the left or right mouse button was just pressed.

If this method is omitted, the screen is dismissed on receival of the LEAVESCREEN key.

@@ -2787,6 +2790,14 @@ before rendering the token.

  • token.tile = pen

    Specifies a pen to paint as one tile before the main part of the token.

  • +
  • token.width = ...

    +

    If specified either as a value or a callback, the text field is padded +or truncated to the specified number.

    +
  • +
  • token.pad_char = '?'

    +

    If specified together with width, the padding area is filled with +this character instead of just being skipped over.

    +
  • token.key = '...'

    Specifies the keycode associated with the token. The string description of the key binding is added to the text content of the token.

    @@ -2848,11 +2859,16 @@ this may be extended with mouse click support.

    icon_pen:Default pen for icons. -on_select:Selection change callback; called as on_select(index,choice). +on_select:Selection change callback; called as on_select(index,choice). +This is also called with nil arguments if setChoices is called +with an empty list. on_submit:Enter key callback; if specified, the list reacts to the key and calls it as on_submit(index,choice). +on_submit2:Shift-Enter key callback; if specified, the list reacts to the key +and calls it as on_submit2(index,choice). + row_height:Height of every row in text lines. icon_width:If not nil, the specified number of character columns @@ -2908,6 +2924,9 @@ with the following fields:

  • list:submit()

    Call the on_submit callback, as if the Enter key was handled.

  • +
  • list:submit2()

    +

    Call the on_submit2 callback, as if the Shift-Enter key was handled.

    +
  • @@ -2922,6 +2941,8 @@ supports:

    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. + not_found_label:  Specifies the text of the label shown when no items match the filter. diff --git a/Lua API.rst b/Lua API.rst index 4087ff0aa..8906d1d44 100644 --- a/Lua API.rst +++ b/Lua API.rst @@ -1610,7 +1610,10 @@ Supported callbacks and fields are: Maps to an integer in range 0-255. Duplicates a separate "STRING_A???" code for convenience. ``_MOUSE_L, _MOUSE_R`` - If the left or right mouse button is pressed. + If the left or right mouse button is being pressed. + + ``_MOUSE_L_DOWN, _MOUSE_R_DOWN`` + If the left or right mouse button was just pressed. If this method is omitted, the screen is dismissed on receival of the ``LEAVESCREEN`` key. @@ -2710,6 +2713,16 @@ containing newlines, or a table with the following possible fields: Specifies a pen to paint as one tile before the main part of the token. +* ``token.width = ...`` + + If specified either as a value or a callback, the text field is padded + or truncated to the specified number. + +* ``token.pad_char = '?'`` + + If specified together with ``width``, the padding area is filled with + this character instead of just being skipped over. + * ``token.key = '...'`` Specifies the keycode associated with the token. The string description @@ -2775,8 +2788,12 @@ It has the following attributes: :inactive_pen: If specified, used for the cursor when the widget is not active. :icon_pen: Default pen for icons. :on_select: Selection change callback; called as ``on_select(index,choice)``. + This is also called with *nil* arguments if ``setChoices`` is called + with an empty list. :on_submit: Enter key callback; if specified, the list reacts to the key and calls it as ``on_submit(index,choice)``. +:on_submit2: Shift-Enter key callback; if specified, the list reacts to the key + and calls it as ``on_submit2(index,choice)``. :row_height: Height of every row in text lines. :icon_width: If not *nil*, the specified number of character columns are reserved to the left of the list item for the icons. @@ -2826,6 +2843,10 @@ The list supports the following methods: Call the ``on_submit`` callback, as if the Enter key was handled. +* ``list:submit2()`` + + Call the ``on_submit2`` callback, as if the Shift-Enter key was handled. + FilteredList class ------------------ @@ -2836,6 +2857,7 @@ In addition to passing through all attributes supported by List, it supports: :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. :not_found_label: Specifies the text of the label shown when no items match the filter. The list choices may include the following attributes: @@ -2932,6 +2954,36 @@ sort Does not export any native functions as of now. Instead, it calls lua code to perform the actual ordering of list items. +Eventful +======== + +This plugin exports some events to lua thus allowing to run lua functions +on DF world events. + +List of events +-------------- + +1. onReactionComplete(reaction,unit,input_items,input_reagents,output_items,call_native) - auto activates if detects reactions starting with ``LUA_HOOK_``. Is called when reaction finishes. +2. onItemContaminateWound(item,unit,wound,number1,number2) - Is called when item tries to contaminate wound (e.g. stuck in) + +Examples +-------- +Spawn dragon breath on each item attempt to contaminate wound: +:: + + b=require "plugins.eventful" + b.onItemContaminateWound.one=function(item,unit,un_wound,x,y) + local flw=dfhack.maps.spawnFlow(unit.pos,6,0,0,50000) + end + +Reaction complete example" +:: + + b.onReactionComplete.one=function(reaction,unit,in_items,in_reag,out_items,call_native) + local pos=copyall(unit.pos) + dfhack.timeout(100,"ticks",function() dfhack.maps.spawnFlow(pos,6,0,0,50000) end) -- spawn dragonbreath after 100 ticks + call_native.value=false --do not call real item creation code + end ======= Scripts diff --git a/NEWS b/NEWS index b0802b9be..4421d83ba 100644 --- a/NEWS +++ b/NEWS @@ -12,6 +12,7 @@ DFHack future - removebadthoughts: add --dry-run option - superdwarf: work in adventure mode too - tweak stable-cursor: carries cursor location from/to Build menu. + - deathcause: allow selection from the unitlist screen New tweaks: - tweak military-training: speed up melee squad training up to 10x (normally 3-5x). New scripts: @@ -22,34 +23,36 @@ DFHack future - embark: lets you embark anywhere. - lever: list and pull fort levers from the dfhack console. - stripcaged: mark items inside cages for dumping, eg caged goblin weapons. + - soundsense-season: writes the correct season to gamelog.txt on world load. New GUI scripts: - gui/guide-path: displays the cached path for minecart Guide orders. - gui/workshop-job: displays inputs of a workshop job and allows tweaking them. - - gui/workflow: a front-end for the workflow plugin. + - gui/workflow: a front-end for the workflow plugin (part inspired by falconne). - gui/assign-rack: works together with a binary patch to fix weapon racks. - gui/gm-editor: an universal editor for lots of dfhack things. - gui/companion-order: a adventure mode command interface for your companions. - gui/advfort: a way to do jobs with your adventurer (e.g. build fort). - New binary patches: - - armorstand-capacity - - custom-reagent-size - - deconstruct-heapfall - - deconstruct-teleport - - hospital-overstocking - - training-ammo - - weaponrack-unassign + New binary patches (for use with binpatch): + - armorstand-capacity: doubles the capacity of armor stands. + - custom-reagent-size: lets custom reactions use small amounts of inputs. + - deconstruct-heapfall: stops some items still falling on head when deconstructing. + - deconstruct-teleport: stops items from 16x16 block teleporting when deconstructing. + - hospital-overstocking: stops hospital overstocking with supplies. + - training-ammo: lets dwarves with quiver full of combat-only ammo train. + - weaponrack-unassign: fixes bug that negates work done by gui/assign-rack. Workflow plugin: - properly considers minecarts assigned to routes busy. - code for deducing job outputs rewritten in lua for flexibility. - logic fix: collecting webs produces silk, and ungathered webs are not thread. - items assigned to squads are considered busy, even if not in inventory. - shearing and milking jobs are supported, but only with generic MILK or YARN outputs. + - workflow announces when the stock level gets very low once a season. New Fix Armory plugin: Together with a couple of binary patches and the gui/assign-rack script, this plugin makes weapon racks, armor stands, chests and cabinets in properly designated barracks be used again for storage of squad equipment. New Search plugin by falconne: - Adds an incremental search function to the Stocks, Trading and Unit List screens. + Adds an incremental search function to the Stocks, Trading, Stockpile and Unit List screens. New AutoMaterial plugin by falconne: Makes building constructions (walls, floors, fortifications, etc) a little bit easier by saving you from having to trawl through long lists of materials each time you place one. diff --git a/Readme.html b/Readme.html index cdc4dd631..54deb013f 100644 --- a/Readme.html +++ b/Readme.html @@ -2873,7 +2873,7 @@ directly to the main dwarf mode screen.

    AutoMaterial

    @@ -3034,22 +3041,39 @@ current job, and their current status.

    current count is below the lower bound of the range, the job is resumed; if it is above or equal to the top bound, it will be suspended. Within the range, the specific constraint has no effect on the job; others may still affect it.

    -

    Pressing 'c' switches the current constraint between counting stacks or items. -Pressing 'm' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the -bounds by 1, 5, or 25 depending on the direction and the 'c' setting (counting -items and expanding the range each gives a 5x bonus).

    -

    Pressing 'n' produces a list of possible outputs of this job as guessed by +

    Pressing 'I' switches the current constraint between counting stacks or items. +Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the +bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting +items and expanding the range each gives a 2x bonus).

    +

    Pressing 'A' produces a list of possible outputs of this job as guessed by workflow, and lets you create a new constraint by choosing one as template. If you don't see the choice you want in the list, it likely means you have to adjust the job material first using job item-material or gui/workshop-job, as described in workflow documentation above. In this manner, this feature can be used for troubleshooting jobs that don't match the right constraints.

    images/workflow-new1.png -

    After selecting one of the presented outputs, the interface proceeds to the +

    If you select one of the outputs with Enter, the matching constraint is simply +added to the list. If you use Shift-Enter, the interface proceeds to the next dialog, which allows you to edit the suggested constraint parameters to suit your need, and set the item count range.

    images/workflow-new2.png -

    If you don't need advanced settings, you can just press 'y' to confirm creation.

    +

    Pressing 'S' (or, with the example config, Alt-W in the 'z' stocks screen) +opens the overall status screen, which was copied from the C++ implementation +by falconne for better integration with the rest of the lua script:

    +images/workflow-status.png +

    This screen shows all currently existing workflow constraints, and allows +monitoring and/or changing them from one screen. The constraint list can +be filtered by typing text in the field below.

    +

    The color of the stock level number indicates how "healthy" the stock level +is, based on current count and trend. Bright green is very good, green is good, +red is bad, bright red is very bad.

    +

    The limit number is also color-coded. Red means that there are currently no +workshops producing that item (i.e. no jobs). If it's yellow, that means the +production has been delayed, possibly due to lack of input materials.

    +

    The chart on the right is a plot of the last 14 days (28 half day plots) worth +of stock history for the selected item, with the rightmost point representing +the current stock value. The bright green dashed line is the target +limit (maximum) and the dark green line is that minus the gap (minimum).

    gui/assign-rack

    diff --git a/Readme.rst b/Readme.rst index 325690ab2..7fb3a5565 100644 --- a/Readme.rst +++ b/Readme.rst @@ -2074,7 +2074,7 @@ directly to the main dwarf mode screen. Search ====== -The search plugin adds search to the Stocks, Trading and Unit List screens. +The search plugin adds search to the Stocks, Trading, Stockpile and Unit List screens. .. image:: images/search.png @@ -2097,6 +2097,16 @@ are actually visible in the list; the same effect applies to the Trade Value numbers displayed by the screen. Because of this, pressing the 't' key while search is active clears the search instead of executing the trade. +In the stockpile screen the option only appears if the cursor is in the +rightmost list: + +.. image:: images/search-stockpile.png + +Note that the 'Permit XXX'/'Forbid XXX' keys conveniently operate only +on items actually shown in the rightmost list, so it is possible to select +only fat or tallow by forbidding fats, then searching for fat/tallow, and +using Permit Fats again while the list is filtered. + AutoMaterial ============ @@ -2295,12 +2305,12 @@ current count is below the lower bound of the range, the job is resumed; if it is above or equal to the top bound, it will be suspended. Within the range, the specific constraint has no effect on the job; others may still affect it. -Pressing 'c' switches the current constraint between counting stacks or items. -Pressing 'm' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the -bounds by 1, 5, or 25 depending on the direction and the 'c' setting (counting -items and expanding the range each gives a 5x bonus). +Pressing 'I' switches the current constraint between counting stacks or items. +Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the +bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting +items and expanding the range each gives a 2x bonus). -Pressing 'n' produces a list of possible outputs of this job as guessed by +Pressing 'A' produces a list of possible outputs of this job as guessed by workflow, and lets you create a new constraint by choosing one as template. If you don't see the choice you want in the list, it likely means you have to adjust the job material first using ``job item-material`` or ``gui/workshop-job``, @@ -2309,14 +2319,35 @@ can be used for troubleshooting jobs that don't match the right constraints. .. image:: images/workflow-new1.png -After selecting one of the presented outputs, the interface proceeds to the +If you select one of the outputs with Enter, the matching constraint is simply +added to the list. If you use Shift-Enter, the interface proceeds to the next dialog, which allows you to edit the suggested constraint parameters to suit your need, and set the item count range. .. image:: images/workflow-new2.png -If you don't need advanced settings, you can just press 'y' to confirm creation. +Pressing 'S' (or, with the example config, Alt-W in the 'z' stocks screen) +opens the overall status screen, which was copied from the C++ implementation +by falconne for better integration with the rest of the lua script: + +.. image:: images/workflow-status.png + +This screen shows all currently existing workflow constraints, and allows +monitoring and/or changing them from one screen. The constraint list can +be filtered by typing text in the field below. + +The color of the stock level number indicates how "healthy" the stock level +is, based on current count and trend. Bright green is very good, green is good, +red is bad, bright red is very bad. + +The limit number is also color-coded. Red means that there are currently no +workshops producing that item (i.e. no jobs). If it's yellow, that means the +production has been delayed, possibly due to lack of input materials. +The chart on the right is a plot of the last 14 days (28 half day plots) worth +of stock history for the selected item, with the rightmost point representing +the current stock value. The bright green dashed line is the target +limit (maximum) and the dark green line is that minus the gap (minimum). gui/assign-rack diff --git a/dfhack.init-example b/dfhack.init-example index 8fafa4cf4..1a5aee48f 100644 --- a/dfhack.init-example +++ b/dfhack.init-example @@ -91,6 +91,7 @@ keybinding add Alt-A@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workshop-job # workflow front-end keybinding add Alt-W@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workflow +keybinding add Alt-W@overallstatus "gui/workflow status" # assign weapon racks to squads so that they can be used keybinding add P@dwarfmode/QueryBuilding/Some/Weaponrack gui/assign-rack @@ -136,6 +137,13 @@ tweak military-color-assigned # remove inverse dependency of squad training speed on unit list size and use more sparring tweak military-training +########### +# Scripts # +########### + +# write the correct season to gamelog on world load +soundsense-season + ####################################################### # Apply binary patches at runtime # # # diff --git a/images/search-stockpile.png b/images/search-stockpile.png new file mode 100644 index 000000000..37a0e57cd Binary files /dev/null and b/images/search-stockpile.png differ diff --git a/images/workflow-new1.png b/images/workflow-new1.png index 25b498bca..498fc4e80 100644 Binary files a/images/workflow-new1.png and b/images/workflow-new1.png differ diff --git a/images/workflow-new2.png b/images/workflow-new2.png index 74a4922be..58698d30f 100644 Binary files a/images/workflow-new2.png and b/images/workflow-new2.png differ diff --git a/images/workflow-status.png b/images/workflow-status.png new file mode 100644 index 000000000..6c3d989f3 Binary files /dev/null and b/images/workflow-status.png differ diff --git a/images/workflow.png b/images/workflow.png index a0a0d4216..7506c730f 100644 Binary files a/images/workflow.png and b/images/workflow.png differ diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index f67b6fe44..784b54c90 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -121,7 +121,6 @@ include/modules/Materials.h include/modules/Notes.h include/modules/Screen.h include/modules/Translation.h -include/modules/Vegetation.h include/modules/Vermin.h include/modules/World.h include/modules/Graphic.h @@ -142,7 +141,6 @@ modules/Materials.cpp modules/Notes.cpp modules/Screen.cpp modules/Translation.cpp -modules/Vegetation.cpp modules/Vermin.cpp modules/World.cpp modules/Graphic.cpp diff --git a/library/Core.cpp b/library/Core.cpp index fd96d5601..7e9c90e98 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -343,6 +343,50 @@ command_result Core::runCommand(color_ostream &out, const std::string &command) return CR_NOT_IMPLEMENTED; } +static bool try_autocomplete(color_ostream &con, const std::string &first, std::string &completed) +{ + std::vector possible; + + auto plug_mgr = Core::getInstance().getPluginManager(); + for(size_t i = 0; i < plug_mgr->size(); i++) + { + const Plugin * plug = (plug_mgr->operator[](i)); + for (size_t j = 0; j < plug->size(); j++) + { + const PluginCommand &pcmd = plug->operator[](j); + if (pcmd.isHotkeyCommand()) + continue; + if (pcmd.name.substr(0, first.size()) == first) + possible.push_back(pcmd.name); + } + } + + bool all = (first.find('/') != std::string::npos); + + std::map scripts; + listScripts(plug_mgr, scripts, Core::getInstance().getHackPath() + "scripts/", all); + for (auto iter = scripts.begin(); iter != scripts.end(); ++iter) + if (iter->first.substr(0, first.size()) == first) + possible.push_back(iter->first); + + if (possible.size() == 1) + { + completed = possible[0]; + fprintf(stderr, "Autocompleted %s to %s\n", first.c_str(), completed.c_str()); + return true; + } + + if (possible.size() > 1 && possible.size() < 8) + { + std::string out; + for (size_t i = 0; i < possible.size(); i++) + out += " " + possible[i]; + con.print("Possible completions:%s\n", out.c_str()); + } + + return false; +} + command_result Core::runCommand(color_ostream &con, const std::string &first, vector &parts) { if (!first.empty()) @@ -665,10 +709,14 @@ command_result Core::runCommand(color_ostream &con, const std::string &first, ve if(res == CR_NOT_IMPLEMENTED) { auto filename = getHackPath() + "scripts/" + first; + std::string completed; + if (fileExists(filename + ".lua")) res = runLuaScript(con, first, parts); else if (plug_mgr->eval_ruby && fileExists(filename + ".rb")) res = runRubyScript(con, plug_mgr, first, parts); + else if (try_autocomplete(con, first, completed)) + return runCommand(con, completed, parts); else con.printerr("%s is not a recognized command.\n", first.c_str()); } @@ -733,7 +781,6 @@ void fIOthread(void * iodata) { string command = ""; int ret = con.lineedit("[DFHack]# ",command, main_history); - fprintf(stderr,"Command: [%s]\n",command.c_str()); if(ret == -2) { cerr << "Console is shutting down properly." << endl; @@ -747,14 +794,10 @@ void fIOthread(void * iodata) else if(ret) { // a proper, non-empty command was entered - fprintf(stderr,"Adding command to history\n"); main_history.add(command); - fprintf(stderr,"Saving history\n"); main_history.save("dfhack.history"); } - fprintf(stderr,"Running command\n"); - auto rv = core->runCommand(con, command); if (rv == CR_NOT_IMPLEMENTED) diff --git a/library/include/DFHack.h b/library/include/DFHack.h index d606df94b..8a094cf86 100644 --- a/library/include/DFHack.h +++ b/library/include/DFHack.h @@ -61,7 +61,6 @@ distribution. #include "modules/Translation.h" #include "modules/World.h" #include "modules/Items.h" -#include "modules/Vegetation.h" #include "modules/Maps.h" #include "modules/Gui.h" diff --git a/library/include/ModuleFactory.h b/library/include/ModuleFactory.h index 1f3d4222a..87c9a726f 100644 --- a/library/include/ModuleFactory.h +++ b/library/include/ModuleFactory.h @@ -33,7 +33,6 @@ namespace DFHack Module* createGui(); Module* createWorld(); Module* createMaterials(); - Module* createVegetation(); Module* createNotes(); Module* createGraphic(); } diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index 632e8ec13..82f79e94b 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -32,7 +32,6 @@ distribution. #include "Export.h" #include "Module.h" -#include "modules/Vegetation.h" #include #include "BitArray.h" #include "modules/Materials.h" diff --git a/library/include/modules/Vegetation.h b/library/include/modules/Vegetation.h deleted file mode 100644 index 89ba5ff6c..000000000 --- a/library/include/modules/Vegetation.h +++ /dev/null @@ -1,70 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - -#pragma once -#ifndef CL_MOD_VEGETATION -#define CL_MOD_VEGETATION -/** - * \defgroup grp_vegetation Vegetation : stuff that grows and gets cut down or trampled by dwarves - * @ingroup grp_modules - */ - -#include "Export.h" -#include "DataDefs.h" -#include "df/plant.h" - -namespace DFHack -{ -namespace Vegetation -{ -const uint32_t sapling_to_tree_threshold = 120 * 28 * 12 * 3; // 3 years - -// "Simplified" copy of plant -struct t_plant { - df::language_name name; - df::plant_flags flags; - int16_t material; - df::coord pos; - int32_t grow_counter; - uint16_t temperature_1; - uint16_t temperature_2; - int32_t is_burning; - int32_t hitpoints; - int16_t update_order; - //std::vector unk1; - //int32_t unk2; - //uint16_t temperature_3; - //uint16_t temperature_4; - //uint16_t temperature_5; - // Pointer to original object, in case you want to modify it - df::plant *origin; -}; - -DFHACK_EXPORT bool isValid(); -DFHACK_EXPORT uint32_t getCount(); -DFHACK_EXPORT df::plant * getPlant(const int32_t index); -DFHACK_EXPORT bool copyPlant (const int32_t index, t_plant &out); -} -} -#endif diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 2145cfad1..603c7ab44 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -13,6 +13,8 @@ CLEAR_PEN = to_pen{ch=32,fg=0,bg=0} local FAKE_INPUT_KEYS = { _MOUSE_L = true, _MOUSE_R = true, + _MOUSE_L_DOWN = true, + _MOUSE_R_DOWN = true, _STRING = true, } @@ -112,10 +114,14 @@ function inset_frame(rect, inset, gap) return mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap) end -function compute_frame_body(wavail, havail, spec, inset, gap) +function compute_frame_body(wavail, havail, spec, inset, gap, inner_frame) gap = gap or 0 local l,t,r,b = parse_inset(inset) - local rect = compute_frame_rect(wavail, havail, spec, gap*2+l+r, gap*2+t+b) + local xgap,ygap = 0,0 + if inner_frame then + xgap,ygap = gap*2+l+r, gap*2+t+b + end + local rect = compute_frame_rect(wavail, havail, spec, xgap, ygap) local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap) return rect, body end @@ -623,7 +629,7 @@ end function FramedScreen:computeFrame(parent_rect) local sw, sh = parent_rect.width, parent_rect.height local fw, fh = self:getWantedFrameSize(parent_rect) - return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1) + return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1, true) end function FramedScreen:onRenderFrame(dc, rect) diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index 0a79b4c3e..fb9b8fd63 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -152,7 +152,9 @@ ListBox.ATTRS{ with_filter = false, cursor_pen = DEFAULT_NIL, select_pen = DEFAULT_NIL, - on_select = DEFAULT_NIL + on_select = DEFAULT_NIL, + on_select2 = DEFAULT_NIL, + select2_hint = DEFAULT_NIL, } function ListBox:preinit(info) @@ -168,6 +170,16 @@ function ListBox:init(info) list_widget = widgets.FilteredList end + local on_submit2 + if self.select2_hint or self.on_select2 then + on_submit2 = function(sel, obj) + self:dismiss() + if self.on_select2 then self.on_select2(sel, obj) end + local cb = obj.on_select2 + if cb then cb(obj, sel) end + end + end + self:addviews{ list_widget{ view_id = 'list', @@ -182,11 +194,19 @@ function ListBox:init(info) local cb = obj.on_select or obj[2] if cb then cb(obj, sel) end end, + on_submit2 = on_submit2, frame = { l = 0, r = 0 }, } } end +function ListBox:onRenderFrame(dc,rect) + ListBox.super.onRenderFrame(self,dc,rect) + if self.select2_hint then + dc:seek(rect.x1+2,rect.y2):key('SEC_SELECT'):string(': '..self.select2_hint,COLOR_DARKGREY) + end +end + function ListBox:getWantedFrameSize() local mw, mh = InputBox.super.getWantedFrameSize(self) local list = self.subviews.list diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 67090e114..cb9e1c9be 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -60,6 +60,7 @@ Panel = defclass(Panel, Widget) Panel.ATTRS { on_render = DEFAULT_NIL, + on_layout = DEFAULT_NIL, } function Panel:init(args) @@ -70,6 +71,10 @@ function Panel:onRenderBody(dc) if self.on_render then self.on_render(dc) end end +function Panel:postComputeFrame(body) + if self.on_layout then self.on_layout(body) end +end + ----------- -- Pages -- ----------- @@ -242,7 +247,7 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled) end if token.text or token.key then - local text = getval(token.text) or '' + local text = ''..(getval(token.text) or '') local keypen if dc then @@ -256,7 +261,23 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled) end end - x = x + #text + local width = getval(token.width) + local padstr + if width then + x = x + width + if #text > width then + text = string.sub(text,1,width) + else + if token.pad_char then + padstr = string.rep(token.pad_char,width-#text) + end + if dc and token.rjustify then + if padstr then dc:string(padstr) else dc:advance(width-#text) end + end + end + else + x = x + #text + end if token.key then local keystr = gui.getKeyDisplay(token.key) @@ -281,6 +302,10 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled) dc:string(text) end end + + if width and dc and not token.rjustify then + if padstr then dc:string(padstr) else dc:advance(width-#text) end + end end token.x2 = x @@ -384,6 +409,7 @@ List.ATTRS{ inactive_pen = DEFAULT_NIL, on_select = DEFAULT_NIL, on_submit = DEFAULT_NIL, + on_submit2 = DEFAULT_NIL, row_height = 1, scroll_keys = STANDARDSCROLL, icon_width = DEFAULT_NIL, @@ -392,7 +418,13 @@ List.ATTRS{ function List:init(info) self.page_top = 1 self.page_size = 1 - self:setChoices(info.choices, info.selected) + + if info.choices then + self:setChoices(info.choices, info.selected) + else + self.choices = {} + self.selected = 1 + end end function List:setChoices(choices, selected) @@ -455,6 +487,9 @@ function List:moveCursor(delta, force_cb) if cnt < 1 then self.page_top = 1 self.selected = 1 + if force_cb and self.on_select then + self.on_select(nil,nil) + end return end @@ -542,10 +577,19 @@ function List:submit() end end +function List:submit2() + if self.on_submit2 and #self.choices > 0 then + self.on_submit2(self:getSelected()) + end +end + function List:onInput(keys) if self.on_submit and keys.SELECT then self:submit() return true + elseif self.on_submit2 and keys.SEC_SELECT then + self:submit2() + return true else for k,v in pairs(self.scroll_keys) do if keys[k] then @@ -581,10 +625,14 @@ end FilteredList = defclass(FilteredList, Widget) +FilteredList.ATTRS { + edit_below = false, +} + function FilteredList:init(info) self.edit = EditField{ text_pen = info.edit_pen or info.cursor_pen, - frame = { l = info.icon_width, t = 0 }, + frame = { l = info.icon_width, t = 0, h = 1 }, on_change = self:callback('onFilterChange'), on_char = self:callback('onFilterChar'), } @@ -598,6 +646,10 @@ function FilteredList:init(info) scroll_keys = info.scroll_keys, icon_width = info.icon_width, } + if self.edit_below then + self.edit.frame = { l = info.icon_width, b = 0, h = 1 } + self.list.frame = { t = 0, b = 2 } + end if info.on_select then self.list.on_select = function() return info.on_select(self:getSelected()) @@ -608,14 +660,23 @@ function FilteredList:init(info) return info.on_submit(self:getSelected()) end end + if info.on_submit2 then + self.list.on_submit2 = function() + return info.on_submit2(self:getSelected()) + end + end self.not_found = Label{ - visible = false, + visible = true, text = info.not_found_label or 'No matches', text_pen = COLOR_LIGHTRED, - frame = { l = info.icon_width, t = 2 }, + frame = { l = info.icon_width, t = self.list.frame.t }, } self:addviews{ self.edit, self.list, self.not_found } - self:setChoices(info.choices, info.selected) + if info.choices then + self:setChoices(info.choices, info.selected) + else + self.choices = {} + end end function FilteredList:getChoices() @@ -634,6 +695,10 @@ function FilteredList:submit() return self.list:submit() end +function FilteredList:submit2() + return self.list:submit2() +end + function FilteredList:canSubmit() return not self.not_found.visible end diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 363de8064..38f8bfb9f 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -59,6 +59,7 @@ using namespace std; #include "df/z_level_flags.h" #include "df/region_map_entry.h" #include "df/flow_info.h" +#include "df/plant.h" using namespace DFHack; using namespace df::enums; diff --git a/library/modules/Screen.cpp b/library/modules/Screen.cpp index cd20bc25e..782bb317d 100644 --- a/library/modules/Screen.cpp +++ b/library/modules/Screen.cpp @@ -664,14 +664,24 @@ int dfhack_lua_viewscreen::do_input(lua_State *L) if (enabler && enabler->tracking_on) { - if (enabler->mouse_lbut) { + if (enabler->mouse_lbut_down) { lua_pushboolean(L, true); lua_setfield(L, -2, "_MOUSE_L"); } - if (enabler->mouse_rbut) { + if (enabler->mouse_rbut_down) { lua_pushboolean(L, true); lua_setfield(L, -2, "_MOUSE_R"); } + if (enabler->mouse_lbut) { + lua_pushboolean(L, true); + lua_setfield(L, -2, "_MOUSE_L_DOWN"); + enabler->mouse_lbut = 0; + } + if (enabler->mouse_rbut) { + lua_pushboolean(L, true); + lua_setfield(L, -2, "_MOUSE_R_DOWN"); + enabler->mouse_rbut = 0; + } } lua_call(L, 2, 0); diff --git a/library/modules/Translation.cpp b/library/modules/Translation.cpp index 6f4ca2b04..90f8bbb81 100644 --- a/library/modules/Translation.cpp +++ b/library/modules/Translation.cpp @@ -115,6 +115,9 @@ void Translation::setNickname(df::language_name *name, std::string nick) if (!name->has_name) { + if (nick.empty()) + return; + *name = df::language_name(); name->language = 0; @@ -122,6 +125,18 @@ void Translation::setNickname(df::language_name *name, std::string nick) } name->nickname = nick; + + // If the nick is empty, check if this made the whole name empty + if (name->nickname.empty() && name->first_name.empty()) + { + bool has_words = false; + for (int i = 0; i < 7; i++) + if (name->words[i] >= 0) + has_words = true; + + if (!has_words) + name->has_name = false; + } } string Translation::TranslateName(const df::language_name * name, bool inEnglish, bool onlyLastPart) diff --git a/library/modules/Vegetation.cpp b/library/modules/Vegetation.cpp deleted file mode 100644 index f7c4c9b0c..000000000 --- a/library/modules/Vegetation.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/* -https://github.com/peterix/dfhack -Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any -damages arising from the use of this software. - -Permission is granted to anyone to use this software for any -purpose, including commercial applications, and to alter it and -redistribute it freely, subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must -not claim that you wrote the original software. If you use this -software in a product, an acknowledgment in the product documentation -would be appreciated but is not required. - -2. Altered source versions must be plainly marked as such, and -must not be misrepresented as being the original software. - -3. This notice may not be removed or altered from any source -distribution. -*/ - - -#include "Internal.h" - -#include -#include -#include -using namespace std; - -#include "VersionInfo.h" -#include "MemAccess.h" -#include "Types.h" -#include "Core.h" -using namespace DFHack; - -#include "modules/Vegetation.h" -#include "df/world.h" - -using namespace DFHack; -using df::global::world; - -bool Vegetation::isValid() -{ - return (world != NULL); -} - -uint32_t Vegetation::getCount() -{ - return world->plants.all.size(); -} - -df::plant * Vegetation::getPlant(const int32_t index) -{ - if (uint32_t(index) >= getCount()) - return NULL; - return world->plants.all[index]; -} - -bool Vegetation::copyPlant(const int32_t index, t_plant &out) -{ - if (uint32_t(index) >= getCount()) - return false; - - out.origin = world->plants.all[index]; - - out.name = out.origin->name; - out.flags = out.origin->flags; - out.material = out.origin->material; - out.pos = out.origin->pos; - out.grow_counter = out.origin->grow_counter; - out.temperature_1 = out.origin->temperature.whole; - out.temperature_2 = out.origin->temperature.fraction; - out.is_burning = out.origin->is_burning; - out.hitpoints = out.origin->hitpoints; - out.update_order = out.origin->update_order; - //out.unk1 = out.origin->anon_1; - //out.unk2 = out.origin->anon_2; - //out.temperature_3 = out.origin->temperature_unk; - //out.temperature_4 = out.origin->min_safe_temp; - //out.temperature_5 = out.origin->max_safe_temp; - return true; -} diff --git a/plugins/automaterial.cpp b/plugins/automaterial.cpp index 9f383b935..6f613cf0e 100644 --- a/plugins/automaterial.cpp +++ b/plugins/automaterial.cpp @@ -148,8 +148,8 @@ static MaterialDescriptor get_material_in_list(size_t i) } else if (VIRTUAL_CAST_VAR(spec, df::build_req_choice_specst, ui_build_selector->choices[i])) { - result.item_type = gen->item_type; - result.item_subtype = gen->item_subtype; + result.item_type = spec->candidate->getType(); + result.item_subtype = spec->candidate->getSubtype(); result.type = spec->candidate->getActualMaterial(); result.index = spec->candidate->getActualMaterialIndex(); result.valid = true; @@ -294,7 +294,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest { if (in_material_choice_stage()) { - if (!last_used_moved) + if (!last_used_moved && ui_build_selector->is_grouped) { if (auto_choose_materials && get_curr_constr_prefs().size() > 0) { @@ -304,7 +304,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest return; } } - else if (ui_build_selector->is_grouped) + else { last_used_moved = true; move_material_to_top(get_last_used_material()); diff --git a/plugins/cleaners.cpp b/plugins/cleaners.cpp index 319b83c1f..1a52f8a17 100644 --- a/plugins/cleaners.cpp +++ b/plugins/cleaners.cpp @@ -12,6 +12,7 @@ #include "df/global_objects.h" #include "df/builtin_mats.h" #include "df/contaminant.h" +#include "df/plant.h" using std::vector; using std::string; diff --git a/plugins/devel/tiles.cpp b/plugins/devel/tiles.cpp index 1d30ca953..972b7fd0d 100644 --- a/plugins/devel/tiles.cpp +++ b/plugins/devel/tiles.cpp @@ -11,7 +11,6 @@ using std::string; #include #include #include -#include #include #include #include diff --git a/plugins/fix-armory.cpp b/plugins/fix-armory.cpp index efa9350ff..5a4821b4b 100644 --- a/plugins/fix-armory.cpp +++ b/plugins/fix-armory.cpp @@ -110,7 +110,8 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) * 1. Combat ammo and ammo without any allowed use can be stored * in BOXes marked for Squad Equipment, either directly or via * containing room. No-allowed-use ammo is assumed to be reserved - * for emergency combat use, or something like that. + * for emergency combat use, or something like that; however if + * it is already stored in a training chest, it won't be moved. * 1a. If assigned to a squad position, that box can be used _only_ * for ammo assigned to that specific _squad_. Otherwise, if * multiple squads can use this room, they will store their @@ -158,8 +159,8 @@ static bool is_squad_ammo(df::item *item, df::squad *squad, bool combat, bool tr bool cs = spec->flags.bits.use_combat; bool ts = spec->flags.bits.use_training; - // no-use ammo assumed to be combat - if (((cs || !ts) && combat) || (ts && train)) + // no-use ammo assumed to fit any category + if (((cs || !ts) && combat) || ((ts || !cs) && train)) { if (binsearch_index(spec->assigned, item->id) >= 0) return true; diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index 56c8457cc..eaa8077f2 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -11,8 +11,8 @@ #include "df/map_block.h" #include "df/tile_dig_designation.h" #include "df/plant_raw.h" +#include "df/plant.h" -#include "modules/Vegetation.h" #include using std::string; diff --git a/plugins/liquids.cpp b/plugins/liquids.cpp index 6df530a92..15ae84c9b 100644 --- a/plugins/liquids.cpp +++ b/plugins/liquids.cpp @@ -37,7 +37,6 @@ using std::set; #include "Console.h" #include "Export.h" #include "PluginManager.h" -#include "modules/Vegetation.h" #include "modules/Maps.h" #include "modules/Gui.h" #include "TileTypes.h" diff --git a/plugins/lua/workflow.lua b/plugins/lua/workflow.lua index 19ca0a84a..563e83297 100644 --- a/plugins/lua/workflow.lua +++ b/plugins/lua/workflow.lua @@ -8,10 +8,11 @@ local utils = require 'utils' * isEnabled() * setEnabled(enable) - * listConstraints([job]) -> {...} + * listConstraints([job[,with_history] ]) -> {{...},...} * findConstraint(token) -> {...} or nil * setConstraint(token[, by_count, goal, gap]) -> {...} * deleteConstraint(token) -> true/false + * getCountHistory(token) -> {{...},...} or nil --]] diff --git a/plugins/mapexport/mapexport.cpp b/plugins/mapexport/mapexport.cpp index 6bc2d6fb2..9d1ba1c1d 100644 --- a/plugins/mapexport/mapexport.cpp +++ b/plugins/mapexport/mapexport.cpp @@ -13,6 +13,7 @@ using namespace google::protobuf::io; #include "DataDefs.h" #include "df/world.h" +#include "df/plant.h" #include "modules/Constructions.h" #include "proto/Map.pb.h" diff --git a/plugins/plants.cpp b/plugins/plants.cpp index 5ab09868f..89a3257fa 100644 --- a/plugins/plants.cpp +++ b/plugins/plants.cpp @@ -9,17 +9,19 @@ #include "Console.h" #include "Export.h" #include "PluginManager.h" -#include "modules/Vegetation.h" #include "modules/Maps.h" #include "modules/Gui.h" #include "TileTypes.h" #include "modules/MapCache.h" +#include "df/plant.h" using std::vector; using std::string; using namespace DFHack; using df::global::world; +const uint32_t sapling_to_tree_threshold = 120 * 28 * 12 * 3; // 3 years + command_result df_grow (color_ostream &out, vector & parameters); command_result df_immolate (color_ostream &out, vector & parameters); command_result df_extirpate (color_ostream &out, vector & parameters); @@ -113,7 +115,7 @@ static command_result immolations (color_ostream &out, do_what what, bool shrubs if(shrubs && p->flags.bits.is_shrub || trees && !p->flags.bits.is_shrub) { if (what == do_immolate) - p->is_burning = true; + p->damage_flags.bits.is_burning = true; p->hitpoints = 0; destroyed ++; } @@ -136,7 +138,7 @@ static command_result immolations (color_ostream &out, do_what what, bool shrubs if(tree->pos.x == x && tree->pos.y == y && tree->pos.z == z) { if(what == do_immolate) - tree->is_burning = true; + tree->damage_flags.bits.is_burning = true; tree->hitpoints = 0; didit = true; break; @@ -219,7 +221,7 @@ command_result df_grow (color_ostream &out, vector & parameters) if(tileShape(map.tiletypeAt(DFCoord(x,y,z))) == tiletype_shape::SAPLING && tileSpecial(map.tiletypeAt(DFCoord(x,y,z))) != tiletype_special::DEAD) { - tree->grow_counter = Vegetation::sapling_to_tree_threshold; + tree->grow_counter = sapling_to_tree_threshold; } break; } @@ -235,7 +237,7 @@ command_result df_grow (color_ostream &out, vector & parameters) df::tiletype ttype = map.tiletypeAt(df::coord(p->pos.x,p->pos.y,p->pos.z)); if(!p->flags.bits.is_shrub && tileShape(ttype) == tiletype_shape::SAPLING && tileSpecial(ttype) != tiletype_special::DEAD) { - p->grow_counter = Vegetation::sapling_to_tree_threshold; + p->grow_counter = sapling_to_tree_threshold; } } } diff --git a/plugins/prospector.cpp b/plugins/prospector.cpp index 5eab897c0..efd457dfd 100644 --- a/plugins/prospector.cpp +++ b/plugins/prospector.cpp @@ -33,6 +33,7 @@ using namespace std; #include "df/region_map_entry.h" #include "df/inclusion_type.h" #include "df/viewscreen_choose_start_sitest.h" +#include "df/plant.h" using namespace DFHack; using namespace df::enums; diff --git a/plugins/ruby/codegen.pl b/plugins/ruby/codegen.pl index ff69853af..03017a0f5 100755 --- a/plugins/ruby/codegen.pl +++ b/plugins/ruby/codegen.pl @@ -698,6 +698,8 @@ sub sizeof { return 12; } elsif ($subtype eq 'df-flagarray') { return 8; + } elsif ($subtype eq 'df-static-flagarray') { + return $field->getAttribute('count'); } elsif ($subtype eq 'df-array') { return 8; # XXX 6 ? } else { @@ -849,6 +851,9 @@ sub render_item_number { } elsif ($subtype eq 's-float') { push @lines_rb, 'float'; return; + } elsif ($subtype eq 'd-float') { + push @lines_rb, 'double'; + return; } else { print "no render number $subtype\n"; return; @@ -910,6 +915,7 @@ sub render_item_container { my $rbmethod = join('_', split('-', $subtype)); my $tg = $item->findnodes('child::ld:item')->[0]; my $indexenum = $item->getAttribute('index-enum'); + my $count = $item->getAttribute('count'); if ($tg) { if ($rbmethod eq 'df_linked_list') { @@ -926,11 +932,19 @@ sub render_item_container { elsif ($indexenum) { $indexenum = rb_ucase($indexenum); - push @lines_rb, "$rbmethod($indexenum)"; + if ($count) { + push @lines_rb, "$rbmethod($count, $indexenum)"; + } else { + push @lines_rb, "$rbmethod($indexenum)"; + } } else { - push @lines_rb, "$rbmethod"; + if ($count) { + push @lines_rb, "$rbmethod($count)"; + } else { + push @lines_rb, "$rbmethod"; + } } } diff --git a/plugins/ruby/ruby-autogen-defs.rb b/plugins/ruby/ruby-autogen-defs.rb index 4148659a6..ffd68bf1e 100644 --- a/plugins/ruby/ruby-autogen-defs.rb +++ b/plugins/ruby/ruby-autogen-defs.rb @@ -35,6 +35,9 @@ module DFHack def float Float.new end + def double + Double.new + end def bit(shift, enum=nil) BitField.new(shift, 1, enum) end @@ -75,6 +78,9 @@ module DFHack def df_flagarray(indexenum=nil) DfFlagarray.new(indexenum) end + def df_static_flagarray(len, indexenum=nil) + DfStaticFlagarray.new(len, indexenum) + end def df_array(tglen) DfArray.new(tglen, yield) end @@ -237,6 +243,19 @@ module DFHack _set(0.0) end end + class Double < MemStruct + def _get + DFHack.memory_read_double(@_memaddr) + end + + def _set(v) + DFHack.memory_write_double(@_memaddr, v) + end + + def _cpp_init + _set(0.0) + end + end class BitField < MemStruct attr_accessor :_shift, :_len, :_enum def initialize(shift, len, enum=nil) @@ -308,7 +327,7 @@ module DFHack DFHack.memory_write_int32(@_memaddr, v) end when nil; DFHack.memory_write_int32(@_memaddr, 0) - else _get._set(v) + else @_tg._at(_getp)._set(v) end end @@ -664,6 +683,48 @@ module DFHack include Enumerable end + class DfStaticFlagarray < MemStruct + attr_accessor :_indexenum + def initialize(len, indexenum) + @len = len*8 + @_indexenum = indexenum + end + def length + @len + end + def size ; length ; end + def [](idx) + idx = _indexenum.int(idx) if _indexenum + idx += length if idx < 0 + return if idx < 0 or idx >= length + byte = DFHack.memory_read_int8(@_memaddr + idx/8) + (byte & (1 << (idx%8))) > 0 + end + def []=(idx, v) + idx = _indexenum.int(idx) if _indexenum + idx += length if idx < 0 + if idx >= length or idx < 0 + raise 'index out of bounds' + else + byte = DFHack.memory_read_int8(@_memaddr + idx/8) + if (v == nil or v == false or v == 0) + byte &= 0xff ^ (1 << (idx%8)) + else + byte |= (1 << (idx%8)) + end + DFHack.memory_write_int8(@_memaddr + idx/8, byte) + end + end + def inspect + out = "#' + end + + include Enumerable + end class DfArray < Compound attr_accessor :_tglen, :_tg def initialize(tglen, tg) diff --git a/plugins/ruby/ruby.cpp b/plugins/ruby/ruby.cpp index db94ad650..d75fa2402 100644 --- a/plugins/ruby/ruby.cpp +++ b/plugins/ruby/ruby.cpp @@ -578,6 +578,11 @@ static VALUE rb_dfmemory_read_float(VALUE self, VALUE addr) return rb_float_new(*(float*)rb_num2ulong(addr)); } +static VALUE rb_dfmemory_read_double(VALUE self, VALUE addr) +{ + return rb_float_new(*(double*)rb_num2ulong(addr)); +} + // memory writing (buffer) static VALUE rb_dfmemory_write(VALUE self, VALUE addr, VALUE raw) @@ -613,6 +618,12 @@ static VALUE rb_dfmemory_write_float(VALUE self, VALUE addr, VALUE val) return Qtrue; } +static VALUE rb_dfmemory_write_double(VALUE self, VALUE addr, VALUE val) +{ + *(double*)rb_num2ulong(addr) = rb_num2dbl(val); + return Qtrue; +} + // return memory permissions at address (eg "rx", nil if unmapped) static VALUE rb_dfmemory_check(VALUE self, VALUE addr) { @@ -968,12 +979,14 @@ static void ruby_bind_dfhack(void) { rb_define_singleton_method(rb_cDFHack, "memory_read_int16", RUBY_METHOD_FUNC(rb_dfmemory_read_int16), 1); rb_define_singleton_method(rb_cDFHack, "memory_read_int32", RUBY_METHOD_FUNC(rb_dfmemory_read_int32), 1); rb_define_singleton_method(rb_cDFHack, "memory_read_float", RUBY_METHOD_FUNC(rb_dfmemory_read_float), 1); + rb_define_singleton_method(rb_cDFHack, "memory_read_double", RUBY_METHOD_FUNC(rb_dfmemory_read_double), 1); rb_define_singleton_method(rb_cDFHack, "memory_write", RUBY_METHOD_FUNC(rb_dfmemory_write), 2); rb_define_singleton_method(rb_cDFHack, "memory_write_int8", RUBY_METHOD_FUNC(rb_dfmemory_write_int8), 2); rb_define_singleton_method(rb_cDFHack, "memory_write_int16", RUBY_METHOD_FUNC(rb_dfmemory_write_int16), 2); rb_define_singleton_method(rb_cDFHack, "memory_write_int32", RUBY_METHOD_FUNC(rb_dfmemory_write_int32), 2); rb_define_singleton_method(rb_cDFHack, "memory_write_float", RUBY_METHOD_FUNC(rb_dfmemory_write_float), 2); + rb_define_singleton_method(rb_cDFHack, "memory_write_double", RUBY_METHOD_FUNC(rb_dfmemory_write_double), 2); rb_define_singleton_method(rb_cDFHack, "memory_check", RUBY_METHOD_FUNC(rb_dfmemory_check), 1); rb_define_singleton_method(rb_cDFHack, "memory_patch", RUBY_METHOD_FUNC(rb_dfmemory_patch), 2); diff --git a/plugins/ruby/unit.rb b/plugins/ruby/unit.rb index 4c638b1a9..13c3711b0 100644 --- a/plugins/ruby/unit.rb +++ b/plugins/ruby/unit.rb @@ -63,12 +63,54 @@ module DFHack } end + def unit_testflagcurse(u, flag) + return false if u.curse.rem_tags1.send(flag) + return true if u.curse.add_tags1.send(flag) + return false if u.caste < 0 + u.race_tg.caste[u.caste].flags[flag] + end + + def unit_isfortmember(u) + # RE from viewscreen_unitlistst ctor + return false if df.gamemode != :DWARF or + u.mood == :Berserk or + unit_testflagcurse(u, :CRAZED) or + unit_testflagcurse(u, :OPPOSED_TO_LIFE) or + u.unknown8.unk2 or + u.flags3.ghostly or + u.flags1.marauder or u.flags1.active_invader or u.flags1.invader_origin or + u.flags1.forest or + u.flags1.merchant or u.flags1.diplomat + return true if u.flags1.tame + return false if u.flags2.underworld or u.flags2.resident or + u.flags2.visitor_uninvited or u.flags2.visitor or + u.civ_id == -1 or + u.civ_id != df.ui.civ_id + true + end + + # return the page in viewscreen_unitlist where the unit would appear + def unit_category(u) + return if u.flags1.left or u.flags1.incoming + # return if hostile & unit_invisible(u) (hidden_in_ambush or caged+mapblock.hidden or caged+holder.ambush + return :Dead if u.flags1.dead + return :Dead if u.flags3.ghostly # hostile ? + return :Others if !unit_isfortmember(u) + casteflags = u.race_tg.caste[u.caste].flags if u.caste >= 0 + return :Livestock if casteflags and (casteflags[:PET] or casteflags[:PET_EXOTIC]) + return :Citizens if unit_testflagcurse(u, :CAN_SPEAK) + :Livestock + # some other stuff with ui.race_id ? (jobs only?) + end + def unit_iscitizen(u) - u.race == ui.race_id and u.civ_id == ui.civ_id and !u.flags1.dead and !u.flags1.merchant and !u.flags1.forest and - !u.flags1.diplomat and !u.flags2.resident and !u.flags3.ghostly and - !u.curse.add_tags1.OPPOSED_TO_LIFE and !u.curse.add_tags1.CRAZED and - u.mood != :Berserk - # TODO check curse ; currently this should keep vampires, but may include werebeasts + unit_category(u) == :Citizens + end + + def unit_ishostile(u) + unit_category(u) == :Others and + # TODO + true end # list workers (citizen, not crazy / child / inmood / noble) diff --git a/plugins/search.cpp b/plugins/search.cpp index a14397fba..0f296e2d8 100644 --- a/plugins/search.cpp +++ b/plugins/search.cpp @@ -7,10 +7,12 @@ //#include "df/viewscreen_petst.h" #include "df/viewscreen_storesst.h" +#include "df/viewscreen_layer_stockpilest.h" #include "df/viewscreen_tradegoodsst.h" #include "df/viewscreen_unitlistst.h" #include "df/interface_key.h" #include "df/interfacest.h" +#include "df/layer_object_listst.h" using std::set; using std::vector; @@ -79,6 +81,11 @@ public: return true; } + bool is_valid() + { + return valid; + } + // A new keystroke is received in a searchable screen virtual bool process_input(set *input) { @@ -164,9 +171,9 @@ protected: const S *viewscreen; vector saved_list1, reference_list; vector saved_list2; + vector *sort_list2; vector saved_indexes; - bool valid; bool redo_search; bool track_secondary_values; string search_string; @@ -240,12 +247,17 @@ protected: } } - saved_list2[saved_indexes[i]] = (*sort_list2)[adjusted_item_index]; + update_saved_secondary_list_item(saved_indexes[i], adjusted_item_index); } saved_indexes.clear(); } } + virtual void update_saved_secondary_list_item(size_t i, size_t j) + { + saved_list2[i] = (*sort_list2)[j]; + } + // Store a copy of filtered list, used later to work out if filtered list has been sorted after filtering void store_reference_values() { @@ -254,7 +266,7 @@ protected: } // Shortcut to clear the search immediately - void clear_search() + virtual void clear_search() { if (saved_list1.size() > 0) { @@ -273,7 +285,7 @@ protected: } // The actual sort - void do_search() + virtual void do_search() { if (search_string.length() == 0) { @@ -318,7 +330,8 @@ protected: store_reference_values(); //Keep a copy, in case user sorts new list - *cursor_pos = 0; + if (cursor_pos) + *cursor_pos = 0; } virtual bool should_check_input(set *input) @@ -346,10 +359,9 @@ protected: private: vector *sort_list1; - vector *sort_list2; int *cursor_pos; char select_key; - + bool valid; bool entry_mode; df::interface_key select_token; @@ -359,7 +371,8 @@ private: }; template search_parent *search_parent ::lock = NULL; -// Parent struct for the hooks +// Parent struct for the hooks, use optional param D to generate multiple classes with same T & V +// but different static modules template struct search_hook : T { @@ -437,7 +450,7 @@ public: if (screen != viewscreen && !reset_on_change()) return false; - if (!valid) + if (!is_valid()) { viewscreen = screen; search_parent::init(&screen->item_cursor, &screen->items); @@ -501,7 +514,7 @@ public: if (screen != viewscreen && !reset_on_change()) return false; - if (!valid) + if (!is_valid()) { viewscreen = screen; search_parent::init(&screen->cursor_pos[viewscreen->page], &screen->units[viewscreen->page], &screen->jobs[viewscreen->page]); @@ -608,7 +621,7 @@ public: if (screen != viewscreen && !reset_on_change()) return false; - if (!valid) + if (!is_valid()) { viewscreen = screen; search_parent::init(&screen->trader_cursor, &screen->trader_items, &screen->trader_selected, 'q'); @@ -637,7 +650,7 @@ public: if (screen != viewscreen && !reset_on_change()) return false; - if (!valid) + if (!is_valid()) { viewscreen = screen; search_parent::init(&screen->broker_cursor, &screen->broker_items, &screen->broker_selected, 'w'); @@ -657,6 +670,84 @@ template<> IMPLEMENT_VMETHOD_INTERPOSE(trade_search_fort_hook, render); // +// +// START: Stockpile screen search +// + +class stockpile_search : public search_parent +{ +public: + void update_saved_secondary_list_item(size_t i, size_t j) + { + *saved_list2[i] = *(*sort_list2)[j]; + } + + string get_element_description(string *element) const + { + return *element; + } + + void render() const + { + print_search_option(51, 23); + } + + static df::layer_object_listst *getLayerList(const df::viewscreen_layer *layer, int idx) + { + return virtual_cast(vector_get(layer->layer_objects,idx)); + } + + bool init(df::viewscreen_layer_stockpilest *screen) + { + if (screen != viewscreen && !reset_on_change()) + return false; + + auto list3 = getLayerList(screen, 2); + if (!list3->active) + { + if (is_valid()) + { + clear_search(); + reset_all(); + } + + return false; + } + + if (!is_valid()) + { + viewscreen = screen; + search_parent::init(&list3->cursor, &screen->item_names, &screen->item_status); + track_secondary_values = true; + } + + return true; + } + + void do_search() + { + search_parent::do_search(); + auto list3 = getLayerList(viewscreen, 2); + list3->num_entries = viewscreen->item_names.size(); + } + + void clear_search() + { + search_parent::clear_search(); + auto list3 = getLayerList(viewscreen, 2); + list3->num_entries = viewscreen->item_names.size(); + } +}; + +typedef search_hook stockpile_search_hook; +template<> IMPLEMENT_VMETHOD_INTERPOSE(stockpile_search_hook, feed); +template<> IMPLEMENT_VMETHOD_INTERPOSE(stockpile_search_hook, render); + +// +// END: Stockpile screen search +// + + DFHACK_PLUGIN("search"); @@ -670,7 +761,9 @@ DFhackCExport command_result plugin_init ( color_ostream &out, vector , bool> TMaterialCache; +static const size_t MAX_HISTORY_SIZE = 28; + +enum HistoryItem { + HIST_COUNT = 0, + HIST_AMOUNT, + HIST_INUSE_COUNT, + HIST_INUSE_AMOUNT +}; + struct ItemConstraint { PersistentDataItem config; PersistentDataItem history; @@ -313,6 +322,7 @@ struct ItemConstraint { bool request_suspend, request_resume; bool is_active, cant_resume_reported; + int low_stock_reported; TMaterialCache material_cache; @@ -320,7 +330,7 @@ public: ItemConstraint() : is_craft(false), min_quality(item_quality::Ordinary), is_local(false), weight(0), item_amount(0), item_count(0), item_inuse_amount(0), item_inuse_count(0), - is_active(false), cant_resume_reported(false) + is_active(false), cant_resume_reported(false), low_stock_reported(-1) {} int goalCount() { return config.ival(0); } @@ -340,6 +350,8 @@ public: config.ival(2) &= ~1; } + int curItemStock() { return goalByCount() ? item_count : item_amount; } + void init(const std::string &str) { config.val() = str; @@ -349,7 +361,7 @@ public: void computeRequest() { - int size = goalByCount() ? item_count : item_amount; + int size = curItemStock(); request_resume = (size <= goalCount()-goalGap()); request_suspend = (size >= goalCount()); } @@ -360,36 +372,29 @@ public: size_t history_size() { return history.data_size() / hist_entry_size; } - size_t history_base(int idx) { + int history_value(int idx, HistoryItem item) { size_t hsize = history_size(); - return ((history.ival(0)+hsize-idx) % hsize) * hist_entry_size; - } - int history_count(int idx) { - return history.get_int28(history_base(idx) + 0*int28_size); - } - int history_amount(int idx) { - return history.get_int28(history_base(idx) + 1*int28_size); - } - int history_inuse_count(int idx) { - return history.get_int28(history_base(idx) + 2*int28_size); - } - int history_inuse_amount(int idx) { - return history.get_int28(history_base(idx) + 3*int28_size); + size_t base = ((history.ival(0)+1+idx) % hsize) * hist_entry_size; + return history.get_int28(base + item*int28_size); } + int history_count(int idx) { return history_value(idx, HIST_COUNT); } + int history_amount(int idx) { return history_value(idx, HIST_AMOUNT); } + int history_inuse_count(int idx) { return history_value(idx, HIST_INUSE_COUNT); } + int history_inuse_amount(int idx) { return history_value(idx, HIST_INUSE_AMOUNT); } void updateHistory() { size_t buffer_size = history_size(); - if (buffer_size < 28) - history.ensure_data(hist_entry_size*buffer_size++, hist_entry_size); + if (buffer_size < MAX_HISTORY_SIZE && size_t(history.ival(0)+1) == buffer_size) + history.ensure_data(hist_entry_size*++buffer_size); history.ival(0) = (history.ival(0)+1) % buffer_size; size_t base = history.ival(0) * hist_entry_size; - history.set_int28(base + 0*int28_size, item_count); - history.set_int28(base + 1*int28_size, item_amount); - history.set_int28(base + 2*int28_size, item_inuse_count); - history.set_int28(base + 3*int28_size, item_inuse_amount); + history.set_int28(base + HIST_COUNT*int28_size, item_count); + history.set_int28(base + HIST_AMOUNT*int28_size, item_amount); + history.set_int28(base + HIST_INUSE_COUNT*int28_size, item_inuse_count); + history.set_int28(base + HIST_INUSE_AMOUNT*int28_size, item_inuse_amount); } }; @@ -1321,6 +1326,20 @@ static void update_jobs_by_constraints(color_ostream &out) else if (ct->mat_mask.whole) info = bitfield_to_string(ct->mat_mask) + " " + info; + if (ct->low_stock_reported != DF_GLOBAL_VALUE(cur_season,-1)) + { + int count = ct->goalCount(), gap = ct->goalGap(); + + if (count >= gap*3 && ct->curItemStock() < std::min(gap*2, (count-gap)/2)) + { + ct->low_stock_reported = DF_GLOBAL_VALUE(cur_season,-1); + + Gui::showAnnouncement("Stock level is low: " + info, COLOR_BROWN, true); + } + else + ct->low_stock_reported = -1; + } + if (is_running != ct->is_active) { if (is_running && ct->request_resume) @@ -1384,6 +1403,25 @@ static void setEnabled(color_ostream &out, bool enable) } } +static void push_count_history(lua_State *L, ItemConstraint *icv) +{ + size_t hsize = icv->history_size(); + + lua_createtable(L, hsize, 0); + + for (size_t i = 0; i < hsize; i++) + { + lua_createtable(L, 0, 4); + + Lua::SetField(L, icv->history_amount(i), -1, "cur_amount"); + Lua::SetField(L, icv->history_count(i), -1, "cur_count"); + Lua::SetField(L, icv->history_inuse_amount(i), -1, "cur_in_use_amount"); + Lua::SetField(L, icv->history_inuse_count(i), -1, "cur_in_use_count"); + + lua_rawseti(L, -2, i+1); + } +} + static void push_constraint(lua_State *L, ItemConstraint *cv) { lua_newtable(L); @@ -1430,19 +1468,31 @@ static void push_constraint(lua_State *L, ItemConstraint *cv) lua_newtable(L); + bool resumed = false, want_resumed = false; + for (size_t i = 0, j = 0; i < cv->jobs.size(); i++) { if (!cv->jobs[i]->isLive()) continue; Lua::PushDFObject(L, cv->jobs[i]->actual_job); lua_rawseti(L, -2, ++j); + + if (cv->jobs[i]->want_resumed) { + want_resumed = true; + resumed = resumed || cv->jobs[i]->isActuallyResumed(); + } } lua_setfield(L, ctable, "jobs"); + + if (want_resumed && !resumed) + Lua::SetField(L, true, ctable, "is_delayed"); } static int listConstraints(lua_State *L) { + lua_settop(L, 2); auto job = Lua::CheckDFObject(L, 1); + bool with_history = lua_toboolean(L, 2); lua_pushnil(L); @@ -1467,6 +1517,13 @@ static int listConstraints(lua_State *L) for (size_t i = 0; i < vec.size(); i++) { push_constraint(L, vec[i]); + + if (with_history) + { + push_count_history(L, vec[i]); + lua_setfield(L, -2, "history"); + } + lua_rawseti(L, -2, i+1); } @@ -1525,23 +1582,7 @@ static int getCountHistory(lua_State *L) ItemConstraint *icv = get_constraint(out, token, NULL, false); if (icv) - { - size_t hsize = icv->history_size(); - - lua_createtable(L, hsize, 0); - - for (int i = hsize-1; i >= 0; i--) - { - lua_createtable(L, 0, 4); - - Lua::SetField(L, icv->history_amount(i), -1, "cur_amount"); - Lua::SetField(L, icv->history_count(i), -1, "cur_count"); - Lua::SetField(L, icv->history_inuse_amount(i), -1, "cur_in_use_amount"); - Lua::SetField(L, icv->history_inuse_count(i), -1, "cur_in_use_count"); - - lua_rawseti(L, -2, hsize-i); // reverse order - } - } + push_count_history(L, icv); else lua_pushnil(L); diff --git a/scripts/autofarm.rb b/scripts/autofarm.rb index cd381089e..6a7635b90 100644 --- a/scripts/autofarm.rb +++ b/scripts/autofarm.rb @@ -17,18 +17,30 @@ class AutoFarm @thresholds.default = v.to_i end - def is_plantable(plant) + def is_plantable (plant) + has_seed = plant.flags[:SEED] season = df.cur_season harvest = df.cur_season_tick + plant.growdur * 10 will_finish = harvest < 10080 - can_plant = plant.flags[season] + can_plant = has_seed && plant.flags[season] can_plant = can_plant && (will_finish || plant.flags[(season+1)%4]) can_plant end def find_plantable_plants plantable = {} - for i in 0..df.ui.tasks.known_plants.length-1 + counts = Hash.new(0) + + df.world.items.other[:SEEDS].each { |i| + if (!i.flags.dump && !i.flags.forbid && !i.flags.garbage_collect && + !i.flags.hostile && !i.flags.on_fire && !i.flags.rotten && + !i.flags.trader && !i.flags.in_building && !i.flags.construction && + !i.flags.artifact) + counts[i.mat_index] = counts[i.mat_index] + i.stack_size + end + } + + counts.keys.each { |i| if df.ui.tasks.known_plants[i] plant = df.world.raws.plants.all[i] if is_plantable(plant) @@ -36,7 +48,8 @@ class AutoFarm plantable[i] = :Underground if (plant.underground_depth_min > 0 || plant.underground_depth_max > 0) end end - end + } + return plantable end diff --git a/scripts/deathcause.rb b/scripts/deathcause.rb index 0ed54d81a..73e29c890 100644 --- a/scripts/deathcause.rb +++ b/scripts/deathcause.rb @@ -11,23 +11,37 @@ def display_death_event(e) end item = df.item_find(:selected) +unit = df.unit_find(:selected) if !item or !item.kind_of?(DFHack::ItemBodyComponent) item = df.world.items.other[:ANY_CORPSE].find { |i| df.at_cursor?(i) } end -if !item or !item.kind_of?(DFHack::ItemBodyComponent) - puts "Please select a corpse in the loo'k' menu" -else +if item and item.kind_of?(DFHack::ItemBodyComponent) hf = item.hist_figure_id - if hf == -1 - # TODO try to retrieve info from the unit (u = item.unit_tg) - puts "Not a historical figure, cannot death find info" +elsif unit + hf = unit.hist_figure_id +end + +if not hf + puts "Please select a corpse in the loo'k' menu, or an unit in the 'u'nitlist screen" + +elsif hf == -1 + # TODO try to retrieve info from the unit (u = item.unit_tg) + puts "Not a historical figure, cannot death find info" + +else + histfig = df.world.history.figures.binsearch(hf) + unit = histfig ? df.unit_find(histfig.unit_id) : nil + if unit and not unit.flags1.dead and not unit.flags3.ghostly + puts "#{unit.name} is not dead yet !" + else events = df.world.history.events (0...events.length).reverse_each { |i| - if events[i].kind_of?(DFHack::HistoryEventHistFigureDiedst) and events[i].victim_hf == hf - display_death_event(events[i]) + e = events[i] + if e.kind_of?(DFHack::HistoryEventHistFigureDiedst) and e.victim_hf == hf + display_death_event(e) break end } diff --git a/scripts/devel/inspect-screen.lua b/scripts/devel/inspect-screen.lua new file mode 100644 index 000000000..ae8334ad7 --- /dev/null +++ b/scripts/devel/inspect-screen.lua @@ -0,0 +1,103 @@ +-- Read the tiles from the screen and display info about them. + +local utils = require 'utils' +local gui = require 'gui' + +InspectScreen = defclass(InspectScreen, gui.Screen) + +function InspectScreen:init(args) + local w,h = dfhack.screen.getWindowSize() + self.cursor_x = math.floor(w/2) + self.cursor_y = math.floor(h/2) +end + +function InspectScreen:computeFrame(parent_rect) + local sw, sh = parent_rect.width, parent_rect.height + self.cursor_x = math.max(0, math.min(self.cursor_x, sw-1)) + self.cursor_y = math.max(0, math.min(self.cursor_y, sh-1)) + + local frame = { w = 14, r = 1, h = 10, t = 1 } + if self.cursor_x > sw/2 then + frame = { w = 14, l = 1, h = 10, t = 1 } + end + + return gui.compute_frame_body(sw, sh, frame, 1, 0, false) +end + +function InspectScreen:onRenderFrame(dc, rect) + self:renderParent() + self.cursor_pen = dfhack.screen.readTile(self.cursor_x, self.cursor_y) + if gui.blink_visible(100) then + dfhack.screen.paintTile({ch='X',fg=COLOR_LIGHTGREEN}, self.cursor_x, self.cursor_y) + end + dc:fill(rect, {ch=' ',fg=COLOR_WHITE,bg=COLOR_CYAN}) +end + +local FG_PEN = {fg=COLOR_WHITE,bg=COLOR_BLACK,tile_color=true} +local BG_PEN = {fg=COLOR_BLACK,bg=COLOR_WHITE,tile_color=true} +local TXT_PEN = {fg=COLOR_WHITE} + +function InspectScreen:onRenderBody(dc) + dc:pen(COLOR_WHITE, COLOR_CYAN) + if self.cursor_pen then + local info = self.cursor_pen + dc:string('CH: '):char(info.ch, FG_PEN):char(info.ch, BG_PEN):string(' '):string(''..info.ch,TXT_PEN):newline() + local fgcolor = info.fg + local fgstr = info.fg + if info.bold then + fgcolor = (fgcolor+8)%16 + fgstr = fgstr..'+8' + end + dc:string('FG: '):string('NN',{fg=fgcolor}):string(' '):string(''..fgstr,TXT_PEN) + dc:seek(dc.width-1):char(info.ch,{fg=info.fg,bold=info.bold}):newline() + dc:string('BG: '):string('NN',{fg=info.bg}):string(' '):string(''..info.bg,TXT_PEN) + dc:seek(dc.width-1):char(info.ch,{fg=COLOR_BLACK,bg=info.bg}):newline() + local bstring = 'false' + if info.bold then bstring = 'true' end + dc:string('Bold: '..bstring):newline():newline() + + if info.tile and gui.USE_GRAPHICS then + dc:string('TL: '):tile(' ', info.tile, FG_PEN):tile(' ', info.tile, BG_PEN):string(' '..info.tile):newline() + if info.tile_color then + dc:string('Color: true') + elseif info.tile_fg then + dc:string('FG: '):string('NN',{fg=info.tile_fg}):string(' '):string(''..info.tile_fg,TXT_PEN):newline() + dc:string('BG: '):string('NN',{fg=info.tile_bg}):string(' '):string(''..info.tile_bg,TXT_PEN):newline() + end + end + else + dc:string('Invalid', COLOR_LIGHTRED) + end +end + +local MOVEMENT_KEYS = { + CURSOR_UP = { 0, -1, 0 }, CURSOR_DOWN = { 0, 1, 0 }, + CURSOR_LEFT = { -1, 0, 0 }, CURSOR_RIGHT = { 1, 0, 0 }, + CURSOR_UPLEFT = { -1, -1, 0 }, CURSOR_UPRIGHT = { 1, -1, 0 }, + CURSOR_DOWNLEFT = { -1, 1, 0 }, CURSOR_DOWNRIGHT = { 1, 1, 0 }, + CURSOR_UP_FAST = { 0, -1, 0, true }, CURSOR_DOWN_FAST = { 0, 1, 0, true }, + CURSOR_LEFT_FAST = { -1, 0, 0, true }, CURSOR_RIGHT_FAST = { 1, 0, 0, true }, + CURSOR_UPLEFT_FAST = { -1, -1, 0, true }, CURSOR_UPRIGHT_FAST = { 1, -1, 0, true }, + CURSOR_DOWNLEFT_FAST = { -1, 1, 0, true }, CURSOR_DOWNRIGHT_FAST = { 1, 1, 0, true }, +} + +function InspectScreen:onInput(keys) + if keys.LEAVESCREEN then + self:dismiss() + else + for k,v in pairs(MOVEMENT_KEYS) do + if keys[k] then + local delta = 1 + if v[4] then + delta = 10 + end + self.cursor_x = self.cursor_x + delta*v[1] + self.cursor_y = self.cursor_y + delta*v[2] + self:updateLayout() + return + end + end + end +end + +InspectScreen{}:show() diff --git a/scripts/gui/workflow.lua b/scripts/gui/workflow.lua index 80c05d296..77a87c9ce 100644 --- a/scripts/gui/workflow.lua +++ b/scripts/gui/workflow.lua @@ -66,32 +66,84 @@ function is_caste_mat(iobj) end function describe_material(iobj) - local matline = 'any material' + local matflags = utils.list_bitfield_flags(iobj.mat_mask) + if #matflags > 0 then + matflags = 'any '..table.concat(matflags, '/') + else + matflags = nil + end + if is_caste_mat(iobj) then - matline = 'no material' + return 'no material' elseif (iobj.mat_type or -1) >= 0 then local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index) + local matline if info then matline = info:toString() else matline = iobj.mat_type..':'..iobj.mat_index end + return matline, matflags + else + return matflags or 'any material' + end +end + +function current_stock(iobj) + if iobj.goal_by_count then + return iobj.cur_count + else + return iobj.cur_amount + end +end + +function if_by_count(iobj,bc,ba) + if iobj.goal_by_count then + return bc + else + return ba end - return matline end +function compute_trend(history,field) + local count = #history + if count == 0 then + return 0 + end + local sumX,sumY,sumXY,sumXX = 0,0,0,0 + for i,v in ipairs(history) do + sumX = sumX + i + sumY = sumY + v[field] + sumXY = sumXY + i*v[field] + sumXX = sumXX + i*i + end + return (count * sumXY - sumX * sumY) / (count * sumXX - sumX * sumX) +end + +------------------------ +-- RANGE EDITOR GROUP -- +------------------------ + local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false } RangeEditor = defclass(RangeEditor, widgets.Label) RangeEditor.ATTRS { get_cb = DEFAULT_NIL, - save_cb = DEFAULT_NIL + save_cb = DEFAULT_NIL, + keys = { + count = 'CUSTOM_SHIFT_I', + modify = 'CUSTOM_SHIFT_R', + min_dec = 'BUILDING_TRIGGER_MIN_SIZE_DOWN', + min_inc = 'BUILDING_TRIGGER_MIN_SIZE_UP', + max_dec = 'BUILDING_TRIGGER_MAX_SIZE_DOWN', + max_inc = 'BUILDING_TRIGGER_MAX_SIZE_UP', + } } function RangeEditor:init(args) self:setText{ - { key = 'BUILDING_TRIGGER_ENABLE_CREATURE', + { key = self.keys.count, text = function() local cons = self.get_cb() or null_cons if cons.goal_by_count then @@ -101,21 +153,21 @@ function RangeEditor:init(args) end end, on_activate = self:callback('onChangeUnit') }, - { key = 'BUILDING_TRIGGER_ENABLE_MAGMA', text = ': Modify', + { key = self.keys.modify, text = ': Range', on_activate = self:callback('onEditRange') }, NEWLINE, ' ', - { key = 'BUILDING_TRIGGER_MIN_SIZE_DOWN', - on_activate = self:callback('onIncRange', 'goal_gap', 5) }, - { key = 'BUILDING_TRIGGER_MIN_SIZE_UP', + { key = self.keys.min_dec, + on_activate = self:callback('onIncRange', 'goal_gap', 2) }, + { key = self.keys.min_inc, on_activate = self:callback('onIncRange', 'goal_gap', -1) }, { text = function() local cons = self.get_cb() or null_cons return string.format(': Min %-4d ', cons.goal_value - cons.goal_gap) end }, - { key = 'BUILDING_TRIGGER_MAX_SIZE_DOWN', + { key = self.keys.max_dec, on_activate = self:callback('onIncRange', 'goal_value', -1) }, - { key = 'BUILDING_TRIGGER_MAX_SIZE_UP', - on_activate = self:callback('onIncRange', 'goal_value', 5) }, + { key = self.keys.max_inc, + on_activate = self:callback('onIncRange', 'goal_value', 2) }, { text = function() local cons = self.get_cb() or null_cons return string.format(': Max %-4d', cons.goal_value) @@ -156,12 +208,16 @@ end function RangeEditor:onIncRange(field, delta) local cons = self.get_cb() if not cons.goal_by_count then - delta = delta * 5 + delta = delta * 2 end - cons[field] = math.max(1, cons[field] + delta) + cons[field] = math.max(1, cons[field] + delta*5) self.save_cb(cons) end +--------------------------- +-- NEW CONSTRAINT DIALOG -- +--------------------------- + NewConstraint = defclass(NewConstraint, gui.FramedScreen) NewConstraint.focus_path = 'workflow/new' @@ -177,7 +233,7 @@ NewConstraint.ATTRS { } function NewConstraint:init(args) - self.constraint = args.constraint or {} + self.constraint = args.constraint or { item_type = -1 } rawset_default(self.constraint, { goal_value = 10, goal_gap = 5, goal_by_count = false }) local matlist = {} @@ -202,8 +258,16 @@ function NewConstraint:init(args) frame = { l = 1, t = 2, w = 26 }, text = { 'Type: ', - { pen = COLOR_LIGHTCYAN, - text = function() return describe_item_type(self.constraint) end }, + { pen = function() + if self:isValid() then return COLOR_LIGHTCYAN else return COLOR_LIGHTRED end + end, + text = function() + if self:isValid() then + return describe_item_type(self.constraint) + else + return 'item not set' + end + end }, NEWLINE, ' ', { key = 'CUSTOM_T', text = ': Select, ', on_activate = self:callback('chooseType') }, @@ -239,7 +303,7 @@ function NewConstraint:init(args) } }, widgets.Label{ - frame = { l = 0, t = 13 }, + frame = { l = 0, t = 14 }, text = { 'Desired range: ', { pen = COLOR_LIGHTCYAN, @@ -255,7 +319,7 @@ function NewConstraint:init(args) } }, RangeEditor{ - frame = { l = 1, t = 15 }, + frame = { l = 1, t = 16 }, get_cb = self:cb_getfield('constraint'), save_cb = self:callback('onRangeChange'), }, @@ -277,6 +341,7 @@ function NewConstraint:init(args) { key = 'LEAVESCREEN', text = ': Cancel, ', on_activate = self:callback('dismiss') }, { key = 'MENU_CONFIRM', key_sep = ': ', + enabled = self:callback('isValid'), text = function() if self.is_existing then return 'Update' else return 'Create new' end end, @@ -295,9 +360,17 @@ function NewConstraint:postinit() self:onChange() end +function NewConstraint:isValid() + return self.constraint.item_type >= 0 or self.constraint.is_craft +end + function NewConstraint:onChange() local token = workflow.constraintToToken(self.constraint) - local out = workflow.findConstraint(token) + local out + + if self:isValid() then + out = workflow.findConstraint(token) + end if out then self.constraint = out @@ -390,6 +463,364 @@ function NewConstraint:onRangeChange() cons.goal_gap = math.max(1, math.min(cons.goal_gap, cons.goal_value-1)) end +------------------------------ +-- CONSTRAINT HISTORY GRAPH -- +------------------------------ + +HistoryGraph = defclass(HistoryGraph, widgets.Widget) + +HistoryGraph.ATTRS { + frame_inset = 1, + history_pen = COLOR_CYAN, +} + +function HistoryGraph:init(info) +end + +function HistoryGraph:setData(history, bars) + self.history = history or {} + self.bars = bars or {} + + local maxval = 1 + for i,v in ipairs(self.history) do + maxval = math.max(maxval, v) + end + for i,v in ipairs(self.bars) do + maxval = math.max(maxval, v.value) + end + self.max_value = maxval +end + +function HistoryGraph:onRenderFrame(dc,rect) + dc:fill(rect.x1,rect.y1,rect.x1,rect.y2,{ch='\xb3', fg=COLOR_BROWN}) + dc:fill(rect.x1,rect.y2,rect.x2,rect.y2,{ch='\xc4', fg=COLOR_BROWN}) + dc:seek(rect.x1,rect.y1):char('\x1e', COLOR_BROWN) + dc:seek(rect.x1,rect.y2):char('\xc5', COLOR_BROWN) + dc:seek(rect.x2,rect.y2):char('\x10', COLOR_BROWN) + dc:seek(rect.x1,rect.y2-1):char('0', COLOR_BROWN) +end + +function HistoryGraph:onRenderBody(dc) + local coeff = (dc.height-1)/self.max_value + + for i,v in ipairs(self.bars) do + local y = dc.height-1-math.floor(0.5 + coeff*v.value) + dc:fill(0,y,dc.width-1,y,v.pen or {ch='-', fg=COLOR_GREEN}) + end + + local xbase = dc.width-1-#self.history + for i,v in ipairs(self.history) do + local x = xbase + i + local y = dc.height-1-math.floor(0.5 + coeff*v) + dc:seek(x,y):char('*', self.history_pen) + end +end + +------------------------------ +-- GLOBAL CONSTRAINT SCREEN -- +------------------------------ + +ConstraintList = defclass(ConstraintList, gui.FramedScreen) + +ConstraintList.focus_path = 'workflow/list' + +ConstraintList.ATTRS { + frame_title = 'Workflow Status', + frame_inset = 0, + frame_background = COLOR_BLACK, + frame_style = gui.BOUNDARY_FRAME, +} + +function ConstraintList:init(args) + local fwidth_cb = self:cb_getfield('fwidth') + + self.fwidth = 20 + self.sort_by_severity = false + + self:addviews{ + widgets.Panel{ + frame = { l = 0, r = 31 }, + frame_inset = 1, + on_layout = function(body) + self.fwidth = body.width - (12+1+1+7+1+1+1+7) + end, + subviews = { + widgets.Label{ + frame = { l = 0, t = 0 }, + text_pen = COLOR_CYAN, + text = { + { text = 'Item', width = 12 }, ' ', + { text = 'Material etc', width = fwidth_cb }, ' ', + { text = 'Stock / Limit' }, + } + }, + widgets.FilteredList{ + view_id = 'list', + frame = { t = 2, b = 2 }, + edit_below = true, + not_found_label = 'No matching constraints', + edit_pen = COLOR_LIGHTCYAN, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + cursor_pen = { fg = COLOR_WHITE, bg = COLOR_GREEN }, + on_select = self:callback('onSelectConstraint'), + }, + widgets.Label{ + frame = { b = 0, h = 1 }, + text = { + { key = 'CUSTOM_SHIFT_A', text = ': Add', + on_activate = self:callback('onNewConstraint') }, ', ', + { key = 'CUSTOM_SHIFT_X', text = ': Delete', + on_activate = self:callback('onDeleteConstraint') }, ', ', + { key = 'CUSTOM_SHIFT_O', text = ': Severity Order', + on_activate = self:callback('onSwitchSort'), + pen = function() + if self.sort_by_severity then + return COLOR_LIGHTCYAN + else + return COLOR_WHITE + end + end }, + } + } + } + }, + widgets.Panel{ + frame = { w = 30, r = 0, h = 6, t = 0 }, + frame_inset = 1, + subviews = { + widgets.Label{ + frame = { l = 0, t = 0 }, + enabled = self:callback('isAnySelected'), + text = { + { text = function() + local cur = self:getCurConstraint() + if cur then + return string.format( + 'Currently %d (%d in use)', + current_stock(cur), + if_by_count(cur, cur.cur_in_use_count, cur.cur_in_use_amount) + ) + else + return 'No constraint selected' + end + end } + } + }, + RangeEditor{ + frame = { l = 0, t = 2 }, + enabled = self:callback('isAnySelected'), + get_cb = self:callback('getCurConstraint'), + save_cb = self:callback('saveConstraint'), + keys = { + count = 'CUSTOM_SHIFT_I', + modify = 'CUSTOM_SHIFT_R', + min_dec = 'SECONDSCROLL_PAGEUP', + min_inc = 'SECONDSCROLL_PAGEDOWN', + max_dec = 'SECONDSCROLL_UP', + max_inc = 'SECONDSCROLL_DOWN', + } + }, + } + }, + widgets.Widget{ + active = false, + frame = { w = 1, r = 30 }, + frame_background = gui.BOUNDARY_FRAME.frame_pen, + }, + widgets.Widget{ + active = false, + frame = { w = 30, r = 0, h = 1, t = 6 }, + frame_background = gui.BOUNDARY_FRAME.frame_pen, + }, + HistoryGraph{ + view_id = 'graph', + frame = { w = 30, r = 0, t = 7, b = 0 }, + } + } + + self:initListChoices(nil, args.select_token) +end + +function stock_trend_color(cons) + local stock = current_stock(cons) + if stock >= cons.goal_value - cons.goal_gap then + return COLOR_LIGHTGREEN, 0 + elseif stock <= cons.goal_gap then + return COLOR_LIGHTRED, 4 + elseif stock >= cons.goal_value - 2*cons.goal_gap then + return COLOR_GREEN, 1 + elseif stock <= 2*cons.goal_gap then + return COLOR_RED, 3 + else + local trend = if_by_count(cons, cons.trend_count, cons.trend_amount) + if trend > 0.3 then + return COLOR_GREEN, 1 + elseif trend < -0.3 then + return COLOR_RED, 3 + else + return COLOR_GREY, 2 + end + end +end + +function ConstraintList:initListChoices(clist, sel_token) + clist = clist or workflow.listConstraints(nil, true) + + local fwidth_cb = self:cb_getfield('fwidth') + local choices = {} + + for i,cons in ipairs(clist) do + cons.trend_count = compute_trend(cons.history, 'cur_count') + cons.trend_amount = compute_trend(cons.history, 'cur_amount') + + local itemstr = describe_item_type(cons) + local matstr,matflagstr = describe_material(cons) + if matflagstr then + matstr = matflagstr .. ' ' .. matstr + end + + if cons.min_quality > 0 or cons.is_local then + local lst = {} + if cons.is_local then + table.insert(lst, 'local') + end + if cons.min_quality > 0 then + table.insert(lst, string.lower(df.item_quality[cons.min_quality])) + end + matstr = matstr .. ' ('..table.concat(lst,',')..')' + end + + local goal_color = COLOR_GREY + if #cons.jobs == 0 then + goal_color = COLOR_RED + elseif cons.is_delayed then + goal_color = COLOR_YELLOW + end + + table.insert(choices, { + text = { + { text = itemstr, width = 12, pad_char = ' ' }, ' ', + { text = matstr, width = fwidth_cb, pad_char = ' ' }, ' ', + { text = curry(current_stock,cons), width = 7, rjustify = true, + pen = function() return { fg = stock_trend_color(cons) } end }, + { text = curry(if_by_count,cons,'S','I'), gap = 1, + pen = { fg = COLOR_GREY } }, + { text = function() return cons.goal_value end, gap = 1, + pen = { fg = goal_color } } + }, + severity = select(2, stock_trend_color(cons)), + search_key = itemstr .. ' | ' .. matstr, + token = cons.token, + obj = cons + }) + end + + self:setChoices(choices, sel_token) +end + +function ConstraintList:isAnySelected() + return self.subviews.list:getSelected() ~= nil +end + +function ConstraintList:getCurConstraint() + local selidx,selobj = self.subviews.list:getSelected() + if selobj then return selobj.obj end +end + +function ConstraintList:onSwitchSort() + self.sort_by_severity = not self.sort_by_severity + self:setChoices(self.subviews.list:getChoices()) +end + +function ConstraintList:setChoices(choices, sel_token) + if self.sort_by_severity then + table.sort(choices, function(a,b) + return a.severity > b.severity + or (a.severity == b.severity and + current_stock(a.obj)/a.obj.goal_value < current_stock(b.obj)/b.obj.goal_value) + end) + else + table.sort(choices, function(a,b) return a.search_key < b.search_key end) + end + + local selidx = nil + if sel_token then + selidx = utils.linear_index(choices, sel_token, 'token') + end + + local list = self.subviews.list + local filter = list:getFilter() + + list:setChoices(choices, selidx) + + if filter ~= '' then + list:setFilter(filter, selidx) + + if selidx and list:getSelected() ~= selidx then + list:setFilter('', selidx) + end + end +end + +function ConstraintList:onInput(keys) + if keys.LEAVESCREEN then + self:dismiss() + else + ConstraintList.super.onInput(self, keys) + end +end + +function ConstraintList:onNewConstraint() + NewConstraint{ + on_submit = self:callback('saveConstraint') + }:show() +end + +function ConstraintList:saveConstraint(cons) + local out = workflow.setConstraint(cons.token, cons.goal_by_count, cons.goal_value, cons.goal_gap) + self:initListChoices(nil, out.token) +end + +function ConstraintList:onDeleteConstraint() + local cons = self:getCurConstraint() + dlg.showYesNoPrompt( + 'Delete Constraint', + 'Really delete the current constraint?', + COLOR_YELLOW, + function() + workflow.deleteConstraint(cons.token) + self:initListChoices() + end + ) +end + +function ConstraintList:onSelectConstraint(idx,item) + local history, bars + + if item then + local cons = item.obj + local vfield = if_by_count(cons, 'cur_count', 'cur_amount') + + bars = { + { value = cons.goal_value - cons.goal_gap, pen = {ch='-', fg=COLOR_GREEN} }, + { value = cons.goal_value, pen = {ch='-', fg=COLOR_LIGHTGREEN} }, + } + + history = {} + for i,v in ipairs(cons.history or {}) do + table.insert(history, v[vfield]) + end + + table.insert(history, cons[vfield]) + end + + self.subviews.graph:setData(history, bars) +end + +------------------------------- +-- WORKSHOP JOB INFO OVERLAY -- +------------------------------- + JobConstraints = defclass(JobConstraints, guidm.MenuOverlay) JobConstraints.focus_path = 'workflow/job' @@ -425,14 +856,20 @@ function JobConstraints:init(args) widgets.Label{ frame = { l = 0, b = 0 }, text = { - { key = 'CUSTOM_N', text = ': New limit, ', + { key = 'CUSTOM_SHIFT_A', text = ': Add limit, ', on_activate = self:callback('onNewConstraint') }, - { key = 'CUSTOM_X', text = ': Delete', + { key = 'CUSTOM_SHIFT_X', text = ': Delete', enabled = self:callback('isAnySelected'), on_activate = self:callback('onDeleteConstraint') }, NEWLINE, NEWLINE, { key = 'LEAVESCREEN', text = ': Back', - on_activate = self:callback('dismiss') } + on_activate = self:callback('dismiss') }, + ' ', + { key = 'CUSTOM_SHIFT_S', text = ': Status', + on_activate = function() + local sel = self:getCurConstraint() + ConstraintList{ select_token = (sel or {}).token }:show() + end } } }, } @@ -480,24 +917,12 @@ function JobConstraints:initListChoices(clist, sel_token) end itemstr = itemstr .. ' ('..table.concat(lst,',')..')' end - local matstr = describe_material(cons) - local matflagstr = '' - local matflags = utils.list_bitfield_flags(cons.mat_mask) - if #matflags > 0 then - matflags[1] = 'any '..matflags[1] - if matstr == 'any material' then - matstr = table.concat(matflags, ', ') - matflags = {} - end - end - if #matflags > 0 then - matflagstr = table.concat(matflags, ', ') - end + local matstr,matflagstr = describe_material(cons) table.insert(choices, { text = { goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE, - ' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', matflagstr + ' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', (matflagstr or '') }, token = cons.token, obj = cons @@ -538,32 +963,30 @@ function JobConstraints:onNewConstraint() local choices = {} for i,cons in ipairs(variants) do local itemstr = describe_item_type(cons) - local matstr = describe_material(cons) - local matflags = utils.list_bitfield_flags(cons.mat_mask) - if #matflags > 0 then - local fstr = table.concat(matflags, '/') - if matstr == 'any material' then - matstr = 'any '..fstr - else - matstr = 'any '..fstr..' '..matstr - end + local matstr,matflags = describe_material(cons) + if matflags then + matstr = matflags..' '..matstr end table.insert(choices, { text = itemstr..' of '..matstr, obj = cons }) end - dlg.showListPrompt( - 'New limit', - 'Select one of the possible outputs:', - COLOR_WHITE, - choices, - function(idx,item) + dlg.ListBox{ + frame_title = 'Add limit', + text = 'Select one of the possible outputs:', + text_pen = COLOR_WHITE, + choices = choices, + on_select = function(idx,item) + self:saveConstraint(item.obj) + end, + select2_hint = 'Advanced', + on_select2 = function(idx,item) NewConstraint{ constraint = item.obj, on_submit = self:callback('saveConstraint') }:show() - end - ) + end, + }:show() end function JobConstraints:onDeleteConstraint() @@ -589,20 +1012,25 @@ function JobConstraints:onInput(keys) end end -if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then - qerror("This script requires a workshop job selected in the 'q' mode") -end +local args = {...} -local job = dfhack.gui.getSelectedJob() +if args[1] == 'status' then + check_enabled(function() ConstraintList{}:show() end) +else + if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then + qerror("This script requires a workshop job selected in the 'q' mode") + end -check_enabled(function() - check_repeat(job, function() - local clist = workflow.listConstraints(job) - if not clist then - dlg.showMessage('Not Supported', 'This type of job is not supported by workflow.', COLOR_LIGHTRED) - return - end - JobConstraints{ job = job, clist = clist }:show() - end) -end) + local job = dfhack.gui.getSelectedJob() + check_enabled(function() + check_repeat(job, function() + local clist = workflow.listConstraints(job) + if not clist then + dlg.showMessage('Not Supported', 'This type of job is not supported by workflow.', COLOR_LIGHTRED) + return + end + JobConstraints{ job = job, clist = clist }:show() + end) + end) +end diff --git a/scripts/lever.rb b/scripts/lever.rb index 59196f7d2..43aa29b04 100644 --- a/scripts/lever.rb +++ b/scripts/lever.rb @@ -47,9 +47,12 @@ def lever_descr(bld, idx=nil) }.flatten.each { |r| # linked building description tg = r.building_tg - state = tg.gate_flags.closed ? 'closed' : 'opened' - state << ', closing' if tg.gate_flags.closing - state << ', opening' if tg.gate_flags.opening + state = '' + if tg.respond_to?(:gate_flags) + state << (tg.gate_flags.closed ? 'closed' : 'opened') + state << ", closing (#{tg.timer})" if tg.gate_flags.closing + state << ", opening (#{tg.timer})" if tg.gate_flags.opening + end ret << (descr + " linked to #{tg._rtti_classname} ##{tg.id} @[#{tg.centerx}, #{tg.centery}, #{tg.z}] #{state}") diff --git a/scripts/soundsense-season.lua b/scripts/soundsense-season.lua new file mode 100644 index 000000000..6b7d43cfa --- /dev/null +++ b/scripts/soundsense-season.lua @@ -0,0 +1,26 @@ +-- On map load writes the current season to gamelog.txt + +local seasons = { + [0] = 'Spring', + [1] = 'Summer', + [2] = 'Autumn', + [3] = 'Winter', +} + +local args = {...} + +local function write_gamelog(msg) + local log = io.open('gamelog.txt', 'a') + log:write(msg.."\n") + log:close() +end + +if args[1] == 'disable' then + dfhack.onStateChange[_ENV] = nil +else + dfhack.onStateChange[_ENV] = function(op) + if op == SC_WORLD_LOADED then + write_gamelog(seasons[df.global.cur_season]..' has arrived on the calendar.') + end + end +end