Update Gui.cpp

Add reverse engineered functions: parseReportString, autoDFAnnouncement, recenterViewscreen, and pauseRecenter.
Add versions of autoDFAnnouncement that don't take a report_init struct and that log unprinted announcements.
Add utility functions: recent_report, recent_report_any, delete_old_reports, and check_repeat_report.
develop
Ryan Williams 2022-04-24 22:45:26 -07:00 committed by GitHub
parent 89ed9950c7
commit c7be54dac0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 431 additions and 0 deletions

@ -44,6 +44,7 @@ using namespace DFHack;
#include "modules/Job.h" #include "modules/Job.h"
#include "modules/Screen.h" #include "modules/Screen.h"
#include "modules/Maps.h" #include "modules/Maps.h"
#include "modules/Units.h"
#include "DataDefs.h" #include "DataDefs.h"
@ -70,6 +71,7 @@ using namespace DFHack;
#include "df/plant.h" #include "df/plant.h"
#include "df/popup_message.h" #include "df/popup_message.h"
#include "df/report.h" #include "df/report.h"
#include "df/report_zoom_type.h"
#include "df/route_stockpile_link.h" #include "df/route_stockpile_link.h"
#include "df/stop_depart_condition.h" #include "df/stop_depart_condition.h"
#include "df/ui_advmode.h" #include "df/ui_advmode.h"
@ -1360,6 +1362,137 @@ 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)
{ // 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;
out.clear();
bool ignore_space = false;
string current_line = "";
size_t iter = 0;
do
{
if (ignore_space)
{
if (str[iter] == ' ')
continue;
ignore_space = false;
}
if (str[iter] == '&') // escape character
{
iter++; // ignore the '&' itself
if (iter >= str.length())
break;
if (str[iter] == 'r') // "&r" starts new line
{
if (!current_line.empty())
{
out.push_back(string(current_line));
current_line = "";
}
out.push_back(" ");
continue; // don't add 'r' to current_line
}
else if (str[iter] != '&')
{ // not "&&", don't add character to current_line
continue;
}
}
current_line += str[iter];
if (current_line.length() > line_length)
{
size_t i = current_line.length(); // start of current word
size_t j; // end of previous word
while (--i > 0 && current_line[i] != ' '); // find start of current word
if (i == 0)
{ // 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
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)
{
if (unit && !unit->reports.log[slot].empty() &&
*df::global::cur_year == unit->reports.last_year[slot] &&
(*df::global::cur_year_tick - unit->reports.last_year_tick[slot]) <= 500)
{
return true;
}
return false;
}
bool recent_report_any(df::unit *unit)
{
FOR_ENUM_ITEMS(unit_report_type, slot)
{
if (recent_report(unit, slot))
return true;
}
return false;
}
void delete_old_reports()
{
auto &reports = world->status.reports;
while (reports.size() > 3000)
{
if (reports[0] != NULL)
{
if (reports[0]->flags.bits.announcement)
erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id);
delete reports[0];
}
reports.erase(reports.begin());
}
}
int32_t check_repeat_report(vector<string> &results)
{
if (*gamemode == game_mode::DWARF && !results.empty() && world->status.reports.size() >= results.size())
{
auto &reports = world->status.reports;
size_t base = reports.size() - results.size(); // index where a repeat would start
size_t offset = 0;
while (reports[base + offset]->text == results[offset] && ++offset < results.size()); // match each report
if (offset == results.size()) // all lines matched
{
reports[base]->duration = 100;
return ++(reports[base]->repeat_count);
}
}
return 0;
}
}
DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announcement_flags flags, df::coord pos, std::string message, int color, bool bright) DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announcement_flags flags, df::coord pos, std::string message, int color, bool bright)
{ {
using df::global::world; using df::global::world;
@ -1574,6 +1707,239 @@ void Gui::showAutoAnnouncement(
addCombatReportAuto(unit2, flags, id); addCombatReportAuto(unit2, flags, id);
} }
int Gui::autoDFAnnouncement(df::report_init r, string message)
{ // Reverse-engineered from DF announcement code
if (!world->unk_26a9a8) // TODO: world->show_announcements
return 1;
df::announcement_flags a_flags;
if (is_valid_enum_item(r.type))
a_flags = df::global::d_init->announcements.flags[r.type];
else
return 2;
if (message.empty())
{
Core::printerr("Empty announcement %u\n", r.type); // DF would print this to errorlog.txt
return 3;
}
// Check if the announcement will actually be announced
if (*gamemode == game_mode::ADVENTURE)
{
if (r.pos.x != -30000 &&
r.type != announcement_type::CREATURE_SOUND &&
r.type != announcement_type::REGULAR_CONVERSATION &&
r.type != announcement_type::CONFLICT_CONVERSATION &&
r.type != announcement_type::MECHANISM_SOUND)
{ // If not sound, make sure we can see pos
if ((world->units.active.empty() || (r.unit1 != world->units.active[0] && r.unit2 != world->units.active[0])) &&
((Maps::getTileDesignation(r.pos)->whole & 0x10) == 0x0)) // Adventure mode uses this bit to determine current visibility
{
return 4;
}
}
}
else
{ // Dwarf mode (or arena?)
if ((r.unit1 != NULL || r.unit2 != NULL) && (r.unit1 == NULL || Units::isHidden(r.unit1)) && (r.unit2 == NULL || Units::isHidden(r.unit2)))
return 5;
if (!a_flags.bits.D_DISPLAY)
{
if (a_flags.bits.UNIT_COMBAT_REPORT)
{
if (r.unit1 == NULL && r.unit2 == NULL)
return 6;
}
else
{
if (!a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE)
return 7;
if (!recent_report_any(r.unit1) && !recent_report_any(r.unit2))
return 8;
}
}
}
if (a_flags.bits.PAUSE || a_flags.bits.RECENTER)
pauseRecenter((a_flags.bits.RECENTER ? r.pos : df::coord()), a_flags.bits.PAUSE); // Does nothing outside dwarf mode
if (a_flags.bits.DO_MEGA && (*gamemode != game_mode::ADVENTURE || world->units.active.empty() || world->units.active[0]->counters.unconscious <= 0))
showPopupAnnouncement(message, r.color, r.bright);
vector<string> results;
size_t line_length = (r.speaker_id == -1) ? (init->display.grid_x - 7) : (init->display.grid_x - 10);
parseReportString(results, message, line_length);
if (results.empty())
return 9;
// Check for repeat report
int32_t repeat_count = check_repeat_report(results);
if (repeat_count > 0)
{
if (a_flags.bits.D_DISPLAY)
{
world->status.display_timer = r.display_timer;
Gui::writeToGamelog("x" + (repeat_count + 1));
}
return 0;
}
bool success = false; // only print to gamelog if report was used
size_t new_report_index = world->status.reports.size();
for (size_t i = 0; i < results.size(); i++)
{ // Generate report entries for each line
auto new_report = new df::report();
new_report->type = r.type;
new_report->text = results[i];
new_report->color = r.color;
new_report->bright = r.bright;
new_report->flags.whole = 0x0;
new_report->zoom_type = r.zoom_type;
new_report->pos = r.pos;
new_report->zoom_type2 = r.zoom_type2;
new_report->pos2 = r.pos2;
new_report->id = world->status.next_report_id++;
new_report->year = *df::global::cur_year;
new_report->time = *df::global::cur_year_tick;
new_report->unk_v40_1 = r.unk_v40_1;
new_report->unk_v40_2 = r.unk_v40_2;
new_report->speaker_id = r.speaker_id;
world->status.reports.push_back(new_report);
if (i > 0)
new_report->flags.bits.continuation = true;
if (*gamemode == game_mode::ADVENTURE && !world->units.active.empty() && world->units.active[0]->counters.unconscious > 0)
new_report->flags.bits.unconscious = true;
if ((*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || (*gamemode == game_mode::DWARF && a_flags.bits.D_DISPLAY))
{
insert_into_vector(world->status.announcements, &df::report::id, new_report);
new_report->flags.bits.announcement = true;
world->status.display_timer = r.display_timer;
success = true;
}
}
if (*gamemode == game_mode::DWARF)
{
if (a_flags.bits.UNIT_COMBAT_REPORT)
{
if (r.unit1 != NULL)
{
if (r.flags.bits.sparring) // TODO: flags.sparring is inverted
success |= addCombatReport(r.unit1, unit_report_type::Combat, new_report_index);
else if (r.unit1->job.current_job != NULL && r.unit1->job.current_job->job_type == job_type::Hunt)
success |= addCombatReport(r.unit1, unit_report_type::Hunting, new_report_index);
else
success |= addCombatReport(r.unit1, unit_report_type::Sparring, new_report_index);
}
if (r.unit2 != NULL)
{
if (r.flags.bits.sparring) // TODO: flags.sparring is inverted
success |= addCombatReport(r.unit2, unit_report_type::Combat, new_report_index);
else if (r.unit2->job.current_job != NULL && r.unit2->job.current_job->job_type == job_type::Hunt)
success |= addCombatReport(r.unit2, unit_report_type::Hunting, new_report_index);
else
success |= addCombatReport(r.unit2, unit_report_type::Sparring, new_report_index);
}
}
if (a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE)
{
FOR_ENUM_ITEMS(unit_report_type, slot)
{
if (recent_report(r.unit1, slot))
success |= addCombatReport(r.unit1, slot, new_report_index);
if (recent_report(r.unit2, slot))
success |= addCombatReport(r.unit2, slot, new_report_index);
}
}
}
delete_old_reports();
if (/*debug_gamelog &&*/ success)
Gui::writeToGamelog(message);
else if (success)
return 10;
else
return 11;
return 0;
}
int Gui::autoDFAnnouncement(df::report_init r, string message, bool log_failures)
{ // Prints info about failed announcements to DFHack console if log_failures is true
int rv = autoDFAnnouncement(r, message);
if (log_failures)
{
switch (rv)
{
case 0:
break; // success
case 1:
Core::print("Skipped an announcement because world->show_announcements is false:\n%s\n", message.c_str());
break;
case 2:
Core::printerr("Invalid announcement type!\n");
break;
case 3:
break; // empty announcement, already handled
case 4:
Core::print("An adventure announcement occured, but nobody heard:\n%s\n", message.c_str());
break;
case 5:
Core::print("An announcement occured, but nobody heard:\n%s\n", message.c_str());
break;
case 6:
Core::print("Skipped a UNIT_COMBAT_REPORT because it has no units:\n%s\n", message.c_str());
break;
case 7:
Core::print("Skipped an announcement not enabled for this game mode:\n%s\n", message.c_str());
break;
case 8:
Core::print("Skipped an announcement because there's no active report:\n%s\n", message.c_str());
break;
case 9:
Core::print("Skipped an announcement because it was empty after parsing:\n%s\n", message.c_str());
break;
case 10:
Core::print("Report added but skipped printing to gamelog.txt because debug_gamelog is false.\n");
break;
case 11:
Core::print("Report added but didn't qualify to be displayed anywhere:\n%s\n", message.c_str());
break;
default:
Core::printerr("autoDFAnnouncement: Unexpected return value!\n");
}
}
return rv;
}
int Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color, bool bright, df::unit *unit1, df::unit *unit2, bool sparring, bool log_failures)
{
auto r = df::report_init();
r.type = type;
r.color = color;
r.bright = bright;
r.pos = pos;
r.unit1 = unit1;
r.unit2 = unit2;
r.flags.bits.sparring = !sparring; // TODO: inverted
if (Maps::isValidTilePos(pos))
r.zoom_type = report_zoom_type::Unit;
return autoDFAnnouncement(r, message, log_failures);
}
df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed) df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed)
{ {
if (!gview) if (!gview)
@ -1624,6 +1990,71 @@ df::coord Gui::getCursorPos()
return df::coord(cursor->x, cursor->y, cursor->z); return df::coord(cursor->x, cursor->y, cursor->z);
} }
void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_type zoom)
{
// Reverse-engineered from DF announcement code, also used when scrolling
auto dims = getDwarfmodeViewDims();
int32_t w = dims.map_x2 - dims.map_x1 + 1;
int32_t h = dims.map_y2 - dims.map_y1 + 1;
int32_t new_win_x, new_win_y, new_win_z;
getViewCoords(new_win_x, new_win_y, new_win_z);
if (zoom != report_zoom_type::Generic && x != -30000)
{
if (zoom == report_zoom_type::Unit)
{
new_win_x = x - w / 2;
new_win_y = y - h / 2;
}
else // report_zoom_type::Item
{
if (new_win_x > (x - 5))
new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10;
if (new_win_y > (y - 5))
new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10;
if (new_win_x < (x + 5 - w))
new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10;
if (new_win_y < (y + 5 - h))
new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10;
}
if (new_win_z != z)
ui_sidebar_menus->minimap.need_scan = true;
new_win_z = z;
}
*df::global::window_x = clip_range(new_win_x, 0, (world->map.x_count - w));
*df::global::window_y = clip_range(new_win_y, 0, (world->map.y_count - h));
*df::global::window_z = clip_range(new_win_z, 0, (world->map.z_count - 1));
return;
}
void Gui::pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause)
{
// Reverse-engineered from DF announcement code
if (*gamemode != game_mode::DWARF)
return;
resetDwarfmodeView(pause);
if (x != -30000)
{
recenterViewscreen(x, y, z, report_zoom_type::Item);
ui_sidebar_menus->minimap.need_render = true;
ui_sidebar_menus->minimap.need_scan = true;
}
if (init->input.pause_zoom_no_interface_ms > 0)
{
gview->shutdown_interface_tickcount = Core::getInstance().p->getTickCount();
gview->shutdown_interface_for_ms = init->input.pause_zoom_no_interface_ms;
}
return;
}
Gui::DwarfmodeDims getDwarfmodeViewDims_default() Gui::DwarfmodeDims getDwarfmodeViewDims_default()
{ {
Gui::DwarfmodeDims dims; Gui::DwarfmodeDims dims;