Remove parseReportString from API (now utility fn)

Implementations using `word_wrap()` are commented out pending changes to that function.
develop
Ryan Williams 2022-05-24 03:52:33 -07:00 committed by GitHub
parent ce34ac8f33
commit 4b21e7afb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 224 additions and 86 deletions

@ -1095,7 +1095,7 @@ Announcements
and a string and processes them just like DF does. Sometimes this means the announcement won't occur. and a string and processes them just like DF does. Sometimes this means the announcement won't occur.
Can also be built from parameters instead of a ``report_init``. Setting ``is_sparring`` to ``true`` means the report Can also be built from parameters instead of a ``report_init``. Setting ``is_sparring`` to ``true`` means the report
will be added to sparring logs (if applicable) rather than hunting or combat. Text is parsed using ``&`` as an escape character, with ``&r`` will be added to sparring logs (if applicable) rather than hunting or combat. Text is parsed using ``&`` as an escape character, with ``&r``
being a newline, ``&&`` being just ``&``, and any other combination causing neither character to display. adding a blank line (equivalent to ``\n \n``,) ``&&`` being just ``&``, and any other combination causing neither character to display.
Other Other
~~~~~ ~~~~~
@ -3856,6 +3856,7 @@ Subclass of Widget; implements a simple edit field.
Attributes: Attributes:
:label_text: The optional text label displayed before the editable text.
:text: The current contents of the field. :text: The current contents of the field.
:text_pen: The pen to draw the text with. :text_pen: The pen to draw the text with.
:on_char: Input validation callback; used as ``on_char(new_char,text)``. :on_char: Input validation callback; used as ``on_char(new_char,text)``.
@ -3863,6 +3864,8 @@ Attributes:
:on_change: Change notification callback; used as ``on_change(new_text,old_text)``. :on_change: Change notification callback; used as ``on_change(new_text,old_text)``.
:on_submit: Enter key callback; if set the field will handle the key and call ``on_submit(text)``. :on_submit: Enter key callback; if set the field will handle the key and call ``on_submit(text)``.
:key: If specified, the field is disabled until this key is pressed. Must be given as a string. :key: If specified, the field is disabled until this key is pressed. Must be given as a string.
:key_sep: If specified, will be used to customize how the activation key is
displayed. See ``token.key_sep`` in the ``Label`` documentation below.
Label class Label class
----------- -----------
@ -3931,8 +3934,8 @@ containing newlines, or a table with the following possible fields:
* ``token.key_sep = '...'`` * ``token.key_sep = '...'``
Specifies the separator to place between the keybinding label produced Specifies the separator to place between the keybinding label produced
by ``token.key``, and the main text of the token. If the separator is by ``token.key``, and the main text of the token. If the separator starts with
'()', the token is formatted as ``text..' ('..binding..')'``. Otherwise '()', the token is formatted as ``text..' ('..binding..sep:sub(2)``. Otherwise
it is simply ``binding..sep..text``. it is simply ``binding..sep..text``.
* ``token.enabled``, ``token.disabled`` * ``token.enabled``, ``token.disabled``
@ -4023,6 +4026,8 @@ a hotkey.
It has the following attributes: It has the following attributes:
:key: The hotkey keycode to display, e.g. ``'CUSTOM_A'``. :key: The hotkey keycode to display, e.g. ``'CUSTOM_A'``.
:key_sep: If specified, will be used to customize how the activation key is
displayed. See ``token.key_sep`` in the ``Label`` documentation.
:label: The string (or a function that returns a string) to display after the :label: The string (or a function that returns a string) to display after the
hotkey. hotkey.
:on_activate: If specified, it is the callback that will be called whenever :on_activate: If specified, it is the callback that will be called whenever

@ -18,6 +18,27 @@ devel/unforbidall
Replaced by the `unforbid` script. Run ``unforbid all --quiet`` to match the Replaced by the `unforbid` script. Run ``unforbid all --quiet`` to match the
behavior of the original ``devel/unforbidall`` script. behavior of the original ``devel/unforbidall`` script.
.. _deteriorateclothes:
deteriorateclothes
==================
Replaced by the new combined `deteriorate` script. Run
``deteriorate --types=clothes``.
.. _deterioratecorpses:
deterioratecorpses
==================
Replaced by the new combined `deteriorate` script. Run
``deteriorate --types=corpses``.
.. _deterioratefood:
deterioratefood
===============
Replaced by the new combined `deteriorate` script. Run
``deteriorate --types=food``.
.. _digfort: .. _digfort:
digfort digfort

@ -38,16 +38,21 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## New Tweaks ## New Tweaks
## Fixes ## Fixes
- ``widgets.CycleHotkeyLabel``: allow initial option values to be specified as an index instead of an option value
## Misc Improvements ## Misc Improvements
- `confirm`: added a confirmation dialog for removing manager orders
- `dfhack-examples-guide`: refine food preparation orders and fix conditions for making jugs and pots in the ``basic`` manager orders - `dfhack-examples-guide`: refine food preparation orders and fix conditions for making jugs and pots in the ``basic`` manager orders
## Documentation ## Documentation
## API ## API
- add functions reverse-engineered from announcement code: ``Gui::parseReportString``, ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen`` - add functions reverse-engineered from announcement code: ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen``
## Lua ## Lua
- ``widgets.HotkeyLabel``: the ``key_sep`` string is now configurable
- ``widgets.EditField``: the ``key_sep`` string is now configurable
- ``widgets.EditField``: can now display an optional string label in addition to the activation key
# 0.47.05-r5 # 0.47.05-r5

@ -115,7 +115,6 @@ namespace DFHack
// Low-level API that gives full control over announcements and reports // Low-level API that gives full control over announcements and reports
DFHACK_EXPORT void writeToGamelog(std::string message); DFHACK_EXPORT void writeToGamelog(std::string message);
DFHACK_EXPORT bool parseReportString(std::vector<std::string> &out, const std::string &str, size_t line_length = 73);
DFHACK_EXPORT int makeAnnouncement(df::announcement_type type, df::announcement_flags mode, df::coord pos, std::string message, int color = 7, bool bright = true); DFHACK_EXPORT int makeAnnouncement(df::announcement_type type, df::announcement_flags mode, df::coord pos, std::string message, int color = 7, bool bright = true);
DFHACK_EXPORT bool addCombatReport(df::unit *unit, df::unit_report_type slot, int report_index); DFHACK_EXPORT bool addCombatReport(df::unit *unit, df::unit_report_type slot, int report_index);
DFHACK_EXPORT bool addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int report_index); DFHACK_EXPORT bool addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int report_index);
@ -131,7 +130,7 @@ namespace DFHack
// Process an announcement exactly like DF would, which might result in no announcement // Process an announcement exactly like DF would, which might result in no announcement
DFHACK_EXPORT bool autoDFAnnouncement(df::report_init r, std::string message); DFHACK_EXPORT bool autoDFAnnouncement(df::report_init r, std::string message);
DFHACK_EXPORT bool autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, DFHACK_EXPORT bool autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true,
df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool is_sparring = false); df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool is_sparring = false);
/* /*
* Cursor and window coords * Cursor and window coords

@ -178,14 +178,27 @@ end
EditField = defclass(EditField, Widget) EditField = defclass(EditField, Widget)
EditField.ATTRS{ EditField.ATTRS{
label_text = DEFAULT_NIL,
text = '', text = '',
text_pen = DEFAULT_NIL, text_pen = DEFAULT_NIL,
on_char = DEFAULT_NIL, on_char = DEFAULT_NIL,
on_change = DEFAULT_NIL, on_change = DEFAULT_NIL,
on_submit = DEFAULT_NIL, on_submit = DEFAULT_NIL,
key = DEFAULT_NIL, key = DEFAULT_NIL,
key_sep = DEFAULT_NIL,
} }
function EditField:init()
self:addviews{HotkeyLabel{frame={t=0,l=0},
key=self.key,
key_sep=self.key_sep,
label=self.label_text}}
end
function EditField:postUpdateLayout()
self.text_offset = self.subviews[1]:getTextWidth()
end
function EditField:onRenderBody(dc) function EditField:onRenderBody(dc)
dc:pen(self.text_pen or COLOR_LIGHTCYAN):fill(0,0,dc.width-1,0) dc:pen(self.text_pen or COLOR_LIGHTCYAN):fill(0,0,dc.width-1,0)
@ -194,16 +207,11 @@ function EditField:onRenderBody(dc)
cursor = ' ' cursor = ' '
end end
local txt = self.text .. cursor local txt = self.text .. cursor
local dx = dc.x local max_width = dc.width - self.text_offset
if self.key then
dc:key_string(self.key, '')
end
dx = dc.x - dx
local max_width = dc.width - dx
if #txt > max_width then if #txt > max_width then
txt = string.char(27)..string.sub(txt, #txt-max_width+2) txt = string.char(27)..string.sub(txt, #txt-max_width+2)
end end
dc:string(txt) dc:advance(self.text_offset):string(txt)
end end
function EditField:onInput(keys) function EditField:onInput(keys)
@ -359,12 +367,13 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled)
x = x + #keystr x = x + #keystr
if sep == '()' then if sep:startswith('()') then
if dc then if dc then
dc:string(text) dc:string(text)
dc:string(' ('):string(keystr,keypen):string(')') dc:string(' ('):string(keystr,keypen)
dc:string(sep:sub(2))
end end
x = x + 3 x = x + 1 + #sep
else else
if dc then if dc then
dc:string(keystr,keypen):string(sep):string(text) dc:string(keystr,keypen):string(sep):string(text)
@ -605,13 +614,14 @@ HotkeyLabel = defclass(HotkeyLabel, Label)
HotkeyLabel.ATTRS{ HotkeyLabel.ATTRS{
key=DEFAULT_NIL, key=DEFAULT_NIL,
key_sep=': ',
label=DEFAULT_NIL, label=DEFAULT_NIL,
on_activate=DEFAULT_NIL, on_activate=DEFAULT_NIL,
} }
function HotkeyLabel:init() function HotkeyLabel:init()
self:setText{{key=self.key, key_sep=': ', text=self.label, self:setText{{key=self.key, key_sep=self.key_sep, text=self.label,
on_activate=self.on_activate}} on_activate=self.on_activate}}
end end
---------------------- ----------------------
@ -637,6 +647,11 @@ function CycleHotkeyLabel:init()
break break
end end
end end
if not self.option_idx then
if self.options[self.initial_option] then
self.option_idx = self.initial_option
end
end
if not self.option_idx then if not self.option_idx then
error(('cannot find option with value or index: "%s"') error(('cannot find option with value or index: "%s"')
:format(self.initial_option)) :format(self.initial_option))

@ -1368,82 +1368,150 @@ DFHACK_EXPORT void Gui::writeToGamelog(std::string message)
fseed.close(); fseed.close();
} }
bool Gui::parseReportString(std::vector<std::string> &out, const std::string &str, size_t line_length) namespace
{ // out vector will contain strings cut to line_length, avoiding cutting up words { // Utility functions for reports
// Reverse-engineered from DF announcement code, fixes applied /*bool parseReportString(std::vector<std::string> *out, const std::string &str, size_t line_length = 73)
{
if (str.empty() || line_length == 0)
return false;
if (str.empty() || line_length == 0) string parsed = "";
return false; size_t i = 0;
out.clear();
bool ignore_space = false; while (i < str.length())
string current_line = "";
size_t iter = 0;
do
{
if (ignore_space)
{ {
if (str[iter] == ' ') if (str[i] == '&') // escape character
continue; {
ignore_space = false; i++; // ignore the '&' itself
if (i >= str.length())
break;
if (str[i] == 'r') // "&r" adds a blank line
parsed += "\n \n"; // DF adds a line with a space for some reason
else if (str[i] == '&') // "&&" is '&'
parsed += "&";
// else next char is ignored
}
else
{
parsed += str[i];
}
i++;
} }
if (str[iter] == '&') // escape character return word_wrap(out, parsed, line_length, true);
{ }*/
iter++; // ignore the '&' itself /*bool parseReportString(std::vector<std::string> *out, const std::string &str, size_t line_length = 73)
if (iter >= str.length()) {
break; if (str.empty() || line_length == 0)
return false;
string parsed = "";
size_t i = 0;
if (str[iter] == 'r') // "&r" starts new line while (i < str.length())
{
if (str[i] == '&') // escape character
{ {
if (!current_line.empty()) i++; // ignore the '&' itself
if (i >= str.length())
break;
if (str[i] == 'r') // "&r" adds a blank line
{ {
out.push_back(string(current_line)); word_wrap(out, parsed, line_length, true);
current_line = ""; out->push_back(" "); // DF adds a line with a space for some reason
parsed = "";
} }
out.push_back(" "); else if (str[i] == '&') // "&&" is '&'
continue; // don't add 'r' to current_line parsed += "&";
// else next char is ignored
} }
else if (str[iter] != '&') else
{ // not "&&", don't add character to current_line {
continue; parsed += str[i];
} }
i++;
} }
current_line += str[iter]; if (parsed != "")
if (current_line.length() > line_length) word_wrap(out, parsed, line_length, true);
{
size_t i = current_line.length(); // start of current word return true;
size_t j; // end of previous word }*/
while (--i > 0 && current_line[i] != ' '); // find start of current word bool parseReportString(std::vector<std::string> *out, const std::string &str, size_t line_length)
{ // out vector will contain strings cut to line_length, avoiding cutting up words
// Reverse-engineered from DF announcement code, fixes applied
if (str.empty() || line_length == 0)
return false;
if (i == 0) bool ignore_space = false;
{ // need to push at least one char string current_line = "";
j = i = line_length; // last char ends up on next line size_t iter = 0;
do
{
if (ignore_space)
{
if (str[iter] == ' ')
continue;
ignore_space = false;
} }
else
if (str[iter] == '&') // escape character
{ {
j = i; iter++; // ignore the '&' itself
while (j > 1 && current_line[j - 1] == ' ') if (iter >= str.length())
j--; // consume excess spaces at the split point break;
if (str[iter] == 'r') // "&r" adds a blank line
{
if (!current_line.empty())
{
out->push_back(string(current_line));
current_line = "";
}
out->push_back(" "); // DF adds a line with a space for some reason
continue; // don't add 'r' to current_line
}
else if (str[iter] != '&')
{ // not "&&", don't add character to current_line
continue;
}
} }
out.push_back(current_line.substr(0, j)); // push string before j
if (current_line[i] == ' ') current_line += str[iter];
i++; // don't keep this space if (current_line.length() > line_length)
current_line.erase(0, i); // current_line now starts at last word or is empty {
ignore_space = current_line.empty(); // ignore leading spaces on new line size_t i = current_line.length(); // start of current word
} size_t j; // end of previous word
} while (++iter < str.length()); while (--i > 0 && current_line[i] != ' '); // find start of current word
if (!current_line.empty()) if (i == 0)
out.push_back(current_line); { // need to push at least one char
j = i = line_length; // last char ends up on next line
}
else
{
j = i;
while (j > 1 && current_line[j - 1] == ' ')
j--; // consume excess spaces at the split point
}
out->push_back(current_line.substr(0, j)); // push string before j
return true; if (current_line[i] == ' ')
} i++; // don't keep this space
current_line.erase(0, i); // current_line now starts at last word or is empty
ignore_space = current_line.empty(); // ignore leading spaces on new line
}
} while (++iter < str.length());
if (!current_line.empty())
out->push_back(current_line);
return true;
}
namespace
{ // Utility functions for reports
bool recent_report(df::unit *unit, df::unit_report_type slot) bool recent_report(df::unit *unit, df::unit_report_type slot)
{ {
if (unit && !unit->reports.log[slot].empty() && if (unit && !unit->reports.log[slot].empty() &&
@ -1755,7 +1823,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message)
} }
} }
else else
{ // Dwarf mode (or arena?) { // Dwarf mode
if ((r.unit1 != NULL || r.unit2 != NULL) && (r.unit1 == NULL || Units::isHidden(r.unit1)) && (r.unit2 == NULL || Units::isHidden(r.unit2))) if ((r.unit1 != NULL || r.unit2 != NULL) && (r.unit1 == NULL || Units::isHidden(r.unit1)) && (r.unit2 == NULL || Units::isHidden(r.unit2)))
{ {
DEBUG(gui).print("Dwarf mode announcement not heard:\n%s\n", message.c_str()); DEBUG(gui).print("Dwarf mode announcement not heard:\n%s\n", message.c_str());
@ -1796,16 +1864,16 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message)
vector<string> results; vector<string> results;
size_t line_length = (r.speaker_id == -1) ? (init->display.grid_x - 7) : (init->display.grid_x - 10); size_t line_length = (r.speaker_id == -1) ? (init->display.grid_x - 7) : (init->display.grid_x - 10);
parseReportString(results, message, line_length); parseReportString(&results, message, line_length);
if (results.empty()) if (results.empty()) // DF doesn't do this check
{ {
DEBUG(gui).print("Skipped announcement because it was empty after parsing:\n%s\n", message.c_str()); DEBUG(gui).print("Skipped announcement because it was empty after parsing:\n%s\n", message.c_str());
return false; return false;
} }
// Check for repeat report // Check for repeat report
int32_t repeat_count = check_repeat_report(results); int32_t repeat_count = check_repeat_report(results); // Does nothing outside dwarf mode
if (repeat_count > 0) if (repeat_count > 0)
{ {
if (a_flags.bits.D_DISPLAY) if (a_flags.bits.D_DISPLAY)
@ -1818,7 +1886,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message)
} }
bool success = false; // only print to gamelog if report was used bool success = false; // only print to gamelog if report was used
size_t new_report_index = world->status.reports.size(); size_t new_report_index = world->status.reports.size(); // we need this for addCombatReport
for (size_t i = 0; i < results.size(); i++) for (size_t i = 0; i < results.size(); i++)
{ // Generate report entries for each line { // Generate report entries for each line
auto new_report = new df::report(); auto new_report = new df::report();
@ -1854,7 +1922,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message)
} }
} }
if (*gamemode == game_mode::DWARF) if (*gamemode == game_mode::DWARF) // DF does this inside the previous loop, but we're using addCombatReport instead
{ {
if (a_flags.bits.UNIT_COMBAT_REPORT) if (a_flags.bits.UNIT_COMBAT_REPORT)
{ {
@ -1902,7 +1970,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message)
{ {
DEBUG(gui).print("Announcement succeeded but skipped printing to gamelog.txt because debug_gamelog is false:\n%s\n", message.c_str()); DEBUG(gui).print("Announcement succeeded but skipped printing to gamelog.txt because debug_gamelog is false:\n%s\n", message.c_str());
}*/ }*/
else else // not sure if this can actually happen; our results.empty() check handles the one edge case I can think of that would get this far
{ {
DEBUG(gui).print("Announcement succeeded internally but didn't qualify to be displayed anywhere:\n%s\n", message.c_str()); DEBUG(gui).print("Announcement succeeded internally but didn't qualify to be displayed anywhere:\n%s\n", message.c_str());
} }
@ -1911,7 +1979,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message)
} }
bool Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color, bool Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color,
bool bright, df::unit *unit1, df::unit *unit2, bool is_sparring) bool bright, df::unit *unit1, df::unit *unit2, bool is_sparring)
{ {
auto r = df::report_init(); auto r = df::report_init();
r.type = type; r.type = type;
@ -1922,9 +1990,6 @@ bool Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::str
r.unit2 = unit2; r.unit2 = unit2;
r.flags.bits.hostile_combat = !is_sparring; r.flags.bits.hostile_combat = !is_sparring;
if (Maps::isValidTilePos(pos))
r.zoom_type = report_zoom_type::Unit;
return autoDFAnnouncement(r, message); return autoDFAnnouncement(r, message);
} }

@ -1 +1 @@
Subproject commit 59075f42bbc77c354b5f815c5c1cce5bf48e76a5 Subproject commit a59495e8f72115909772e6df20a7b9dec272f14c

@ -18,6 +18,7 @@
#include "df/general_ref.h" #include "df/general_ref.h"
#include "df/general_ref_contained_in_itemst.h" #include "df/general_ref_contained_in_itemst.h"
#include "df/viewscreen_dwarfmodest.h" #include "df/viewscreen_dwarfmodest.h"
#include "df/viewscreen_jobmanagementst.h"
#include "df/viewscreen_justicest.h" #include "df/viewscreen_justicest.h"
#include "df/viewscreen_layer_militaryst.h" #include "df/viewscreen_layer_militaryst.h"
#include "df/viewscreen_locationsst.h" #include "df/viewscreen_locationsst.h"
@ -481,6 +482,7 @@ DEFINE_CONFIRMATION(note_delete, viewscreen_dwarfmodest);
DEFINE_CONFIRMATION(route_delete, viewscreen_dwarfmodest); DEFINE_CONFIRMATION(route_delete, viewscreen_dwarfmodest);
DEFINE_CONFIRMATION(location_retire, viewscreen_locationsst); DEFINE_CONFIRMATION(location_retire, viewscreen_locationsst);
DEFINE_CONFIRMATION(convict, viewscreen_justicest); DEFINE_CONFIRMATION(convict, viewscreen_justicest);
DEFINE_CONFIRMATION(order_remove, viewscreen_jobmanagementst);
DFhackCExport command_result plugin_init (color_ostream &out, vector <PluginCommand> &commands) DFhackCExport command_result plugin_init (color_ostream &out, vector <PluginCommand> &commands)
{ {

@ -219,6 +219,14 @@ function convict.get_message()
"This action is irreversible." "This action is irreversible."
end end
order_remove = defconf('order-remove')
function order_remove.intercept_key(key)
return key == keys.MANAGER_REMOVE and
not screen.in_max_workshops
end
order_remove.title = "Remove manager order"
order_remove.message = "Are you sure you want to remove this order?"
-- End of confirmation definitions -- End of confirmation definitions
function check() function check()

@ -1 +1 @@
Subproject commit e062237ee03a22d0d8b88b725ba3712e649f1bf6 Subproject commit 05d46b32a3aff4f5f98534fdccfbf9ae88dd31a3

@ -0,0 +1,18 @@
local widgets = require('gui.widgets')
function test.togglehotkeylabel()
local toggle = widgets.ToggleHotkeyLabel{}
expect.true_(toggle:getOptionValue())
toggle:cycle()
expect.false_(toggle:getOptionValue())
toggle:cycle()
expect.true_(toggle:getOptionValue())
end
function test.togglehotkeylabel_default_value()
local toggle = widgets.ToggleHotkeyLabel{initial_option=2}
expect.false_(toggle:getOptionValue())
toggle = widgets.ToggleHotkeyLabel{initial_option=false}
expect.false_(toggle:getOptionValue())
end