Merge pull request #534 from NQNStudios:fix-479

Fixing text buffer texture/font corruption (#479)

* #479 demonstrates that the contents of the text buffer are NOT irrelevant for reproducing bugs. So I set up recording/replay for the burma shave easter egg. This also makes an easy way to mess with the buffer state when debugging (just mash &/\*/&/\*/&/\* n times)
* When a replay throws an error, it puts up a showError() dialog. If the next action is a control_click, the system will try to click that control on the error dialog--which is totally divergent from the replay's intended behavior. So we should just stop replaying when an error happens.
* If you have a long replay and want to run it very fast, but then slow down when you get to the sequence that reproduces your bug, now you can add a `<change_fps>` to your replay to achieve that.
* Fixes for the 2 legacy replay errors that I opened recently

Fix #479 
Fix #532 
Fix #533
This commit is contained in:
2025-01-20 09:10:17 -05:00
committed by GitHub
22 changed files with 165 additions and 43 deletions

View File

@@ -2232,6 +2232,24 @@ void cancel_item_target(bool& did_something, bool& need_redraw, bool& need_repri
did_something = need_redraw = need_reprint = true;
}
// I'm finally adding the easter egg to the replay system
// because it allows forcing the text buffer into a specific state
// which I'm debugging.
std::vector<std::string> easter_egg_messages = {
"If Valorim ...",
"You want to save ...",
"Back up your save files ...",
"Burma Shave."
};
void easter_egg(int idx) {
if(recording){
record_action("easter_egg", boost::lexical_cast<std::string>(idx));
}
add_string_to_buf(easter_egg_messages[idx]);
print_buf();
}
bool handle_keystroke(const sf::Event& event, cFramerateLimiter& fps_limiter){
bool are_done = false;
location pass_point; // TODO: This isn't needed
@@ -2387,20 +2405,16 @@ bool handle_keystroke(const sf::Event& event, cFramerateLimiter& fps_limiter){
switch(chr) {
case '&':
add_string_to_buf("If Valorim ...");
print_buf();
easter_egg(0);
break;
case '*':
add_string_to_buf("You want to save ...");
print_buf();
easter_egg(1);
break;
case '(':
add_string_to_buf("Back up your save files ...");
print_buf();
easter_egg(2);
break;
case ')':
add_string_to_buf("Burma Shave.");
print_buf();
easter_egg(3);
break;
case '?':
@@ -3749,11 +3763,11 @@ bool is_sign(ter_num_t ter) {
return false;
}
bool check_for_interrupt(){
bool check_for_interrupt(std::string confirm_dialog){
using kb = sf::Keyboard;
bool interrupt = false;
sf::Event evt;
if(replaying && has_next_action() && next_action_type() == "handle_interrupt"){
if(replaying && confirm_dialog == "confirm-interrupt-special" && has_next_action() && next_action_type() == "handle_interrupt"){
pop_next_action();
interrupt = true;
}
@@ -3770,8 +3784,16 @@ bool check_for_interrupt(){
if(recording){
record_action("handle_interrupt", "");
}
cChoiceDlog confirm("confirm-interrupt", {"quit","cancel"});
if(confirm.show() == "quit") return true;
cChoiceDlog confirm(confirm_dialog, {"quit","cancel"});
bool was_replaying = replaying;
if(confirm_dialog == "confirm-interrupt-replay"){
// There's a slight chance the next action could be snatched up by the replay system to respond
// to the yes/no prompt, so suspend the replay loop
replaying = false;
}
std::string result = confirm.show();
replaying = was_replaying;
if(result == "quit") return true;
}
return false;
}

View File

@@ -40,7 +40,7 @@ void setup_outdoors(location where);
short get_outdoor_num();
short count_walls(location loc);
bool is_sign(ter_num_t ter);
bool check_for_interrupt();
bool check_for_interrupt(std::string confirm_dialog = "confirm-interrupt-special");
void handle_startup_button_click(eStartButton btn, eKeyMod mods);
void handle_switch_pc(short which_pc, bool& need_redraw, bool& need_reprint);
@@ -102,5 +102,6 @@ void show_item_info(short item_hit);
void close_map(bool record = false);
void cancel_item_target(bool& did_something, bool& need_redraw, bool& need_reprint);
void update_item_stats_area(bool& need_reprint);
void easter_egg(int idx);
#endif

View File

@@ -1599,7 +1599,8 @@ void do_combat_cast(location target) {
if(ashes_loc.x > 0){
// If ashes are going to appear, there'd better be a visible blast on the spot.
if(!hit_ashes_loc){
add_explosion(ashes_loc,0,0,get_boom_type(eDamageType::FIRE),1,0);
// the last argument is true so this doesn't break RNG of older replays:
add_explosion(ashes_loc,0,0,get_boom_type(eDamageType::FIRE),1,0,true);
}
univ.town.set_ash(ashes_loc.x,ashes_loc.y,true);

View File

@@ -10,6 +10,7 @@
#define BOE_GAME_CONSTS_H
#include <set>
#include <map>
/* overall mode; some seem to be missing */
enum eGameMode {
@@ -84,14 +85,9 @@ enum eTrapType {
// Startup buttons
enum eStartButton {
// Left Column
STARTBTN_TUTORIAL = 0,
STARTBTN_LOAD = 1,
STARTBTN_PREFS = 2,
// Right Column
STARTBTN_NEW = 3,
STARTBTN_JOIN = 4,
STARTBTN_SCROLL = 5,
STARTBTN_TUTORIAL = 0, STARTBTN_NEW = 3,
STARTBTN_LOAD = 1, STARTBTN_JOIN = 4,
STARTBTN_PREFS = 2, STARTBTN_SCROLL = 5,
// Keep last:
MAX_eStartButton = 6
};

View File

@@ -6,6 +6,7 @@
#include <vector>
#include <string>
#include <sstream>
#include <map>
#include "boe.consts.hpp"
#define ASB add_string_to_buf
@@ -26,4 +27,8 @@ struct effect_pat_type {
unsigned short pattern[9][9];
};
extern std::map<std::string, int> startup_button_indices;
extern std::map<int, std::string> startup_button_names;
extern std::map<int, std::string> startup_button_names_v1;
#endif

View File

@@ -414,12 +414,9 @@ void draw_startup_stats() {
void draw_start_button(eStartButton which_position,short which_button) {
rectangle from_rect,to_rect;
const char *button_labels[MAX_eStartButton];
button_labels[STARTBTN_TUTORIAL] = "Tutorial";
button_labels[STARTBTN_LOAD] = "Load Game";
button_labels[STARTBTN_PREFS] = "Preferences";
button_labels[STARTBTN_NEW] = "Make New Party";
button_labels[STARTBTN_JOIN] = "Start Scenario";
button_labels[STARTBTN_SCROLL] = "";
for(int i = 0; i < MAX_eStartButton; ++i){
button_labels[i] = startup_button_names[i].c_str();
}
// The 0..65535 version of the blue component was 14472; the commented version was 43144431
sf::Color base_color = {0,0,57};

View File

@@ -333,6 +333,9 @@ static void process_args(int argc, char* argv[]) {
cli.writeToStream(std::cout);
exit(0);
}
// This obsolete preference should always be true unless running an old replay
// (which will set it false after this line if it needs to)
set_pref("DrawTerrainFrills", true);
if(replay){
if(record_to){
std::cout << "Warning: flag --record conflicts with --replay and will be ignored." << std::endl;
@@ -502,6 +505,29 @@ static void handle_scenario_args() {
}
}
std::map<std::string, int> startup_button_indices = {
// Button layout since 11/30/24
{"Tutorial", 0}, {"Make New Party", 3},
{"Load Game", 1}, {"Start Scenario", 4},
{"Preferences", 2},
// Buttons that don't exist anymore
{"Custom Scenario", -1},
};
std::map<int, std::string> startup_button_names = {
{0, "Tutorial"}, {3, "Make New Party"},
{1, "Load Game"}, {4, "Start Scenario"},
{2, "Preferences"}, {5, ""},
};
// Map legacy int indices onto new string-mapped layout
std::map<int, std::string> startup_button_names_v1 = {
{0, "Load Game"}, {3, "Start Scenario"},
{1, "Make New Party"}, {4, "Custom Scenario"},
{2, "Preferences"},
};
void replay_action(Element& action) {
bool did_something = false, need_redraw = false, need_reprint = false;
@@ -512,10 +538,30 @@ void replay_action(Element& action) {
// NOTE: Action replay blocks need to return early unless the action advances time
if(overall_mode == MODE_STARTUP && t == "startup_button_click"){
auto info = info_from_action(action);
eStartButton btn = static_cast<eStartButton>(std::stoi(info["btn"]));
int btn_idx = -1;
try{
// Legacy replays use ints to encode startup buttons
btn_idx = std::stoi(info["btn"]);
}catch(std::invalid_argument& err){
// Newer replays use strings to encode startup buttons
btn_idx = startup_button_indices[info["btn"]];
}
// No-op button
if(btn_idx == -1){
return;
}
eStartButton btn = static_cast<eStartButton>(btn_idx);
eKeyMod mods = static_cast<eKeyMod>(std::stoi(info["mods"]));
handle_startup_button_click(btn, mods);
return;
}else if(t == "change_fps"){
extern boost::optional<cFramerateLimiter> replay_fps_limit;
// default new fps: slow the replay down substantially
int new_fps = 2;
if(!action.GetText().empty()){
new_fps = boost::lexical_cast<int>(action.GetText());
}
replay_fps_limit.emplace(new_fps);
}else if(t == "load_party"){
decode_file(action.GetText(), tempDir / "temp.exg");
load_party(tempDir / "temp.exg", univ);
@@ -832,12 +878,15 @@ void replay_action(Element& action) {
return;
}else if(t == "cancel_item_target"){
cancel_item_target(did_something, need_redraw, need_reprint);
}else if(t == "easter_egg"){
easter_egg(boost::lexical_cast<int>(action.GetText()));
}else if(t == "advance_time"){
// This is bad regardless of strictness, because visual changes may have occurred which won't get redrawn/reprinted
throw std::string { "Replay system internal error! advance_time() was supposed to be called by the last action, but wasn't: " } + _last_action_type;
}else{
std::ostringstream sstr;
sstr << "Couldn't replay action: " << action;
replaying = false;
throw sstr.str();
}
@@ -1009,6 +1058,10 @@ void handle_events() {
while(!All_Done) {
if(replaying && has_next_action()){
if(check_for_interrupt("confirm-interrupt-replay")){
replaying = false;
continue;
}
replay_next_action();
}else{
#ifdef __APPLE__

View File

@@ -302,7 +302,9 @@ void mondo_boom(location l,short type,short snd) {
end_missile_anim();
}
void add_explosion(location dest,short val_to_place,short place_type,short boom_type,short x_adj,short y_adj) {
void add_explosion(location dest,short val_to_place,short place_type,short boom_type,short x_adj,short y_adj, bool use_unique_ran) {
if(!get_bool_pref("DrawTerrainFrills", true))
return;
if(!boom_anim_active)
return;
// lose redundant explosions
@@ -316,7 +318,7 @@ void add_explosion(location dest,short val_to_place,short place_type,short boom_
for(short i = 0; i < 30; i++)
if(store_booms[i].boom_type < 0) {
have_boom = true;
store_booms[i].offset = (i == 0) ? 0 : -1 * get_ran(1,0,2);
store_booms[i].offset = (i == 0) ? 0 : -1 * get_ran(1,0,2,use_unique_ran);
store_booms[i].dest = dest;
store_booms[i].val_to_place = val_to_place;
store_booms[i].place_type = place_type;

View File

@@ -56,7 +56,7 @@ void run_a_missile(location from,location fire_to,miss_num_t miss_type,short pat
void run_a_boom(location boom_where,short type,short x_adj,short y_adj,short snd = -1);
void mondo_boom(location l,short type,short snd = -1);
void add_missile(location dest,miss_num_t missile_type,short path_type,short x_adj,short y_adj);
void add_explosion(location dest,short val_to_place,short place_type,short boom_type,short x_adj,short y_adj);
void add_explosion(location dest,short val_to_place,short place_type,short boom_type,short x_adj,short y_adj, bool use_unique_ran = false);
void do_missile_anim(short num_steps,location missile_origin,short sound_num) ;
void do_explosion_anim(short sound_num,short expand,short snd = -1);
void click_shop_rect(rectangle area_rect);

View File

@@ -2222,10 +2222,11 @@ bool pick_pc_graphic(short pc_num,short mode,cDialog* parent) {
static bool pc_name_event_filter(cDialog& me, short store_train_pc) {
std::string pcName = me["name"].getText();
if(!isalpha(pcName[0])) {
if(pcName.empty()){
me["error"].setText("Cannot be empty.");
}else if(!isalpha(pcName[0])){
me["error"].setText("Must begin with a letter.");
}
else {
}else{
// TODO: This was originally truncated to 18 characters; is that really necessary?
univ.party[store_train_pc].name = pcName;
me.toast(true);

View File

@@ -42,7 +42,7 @@ enum_map(eStartButton, rectangle) startup_button;
void handle_startup_button_click(eStartButton btn, eKeyMod mods) {
if(recording){
std::map<std::string, std::string> info;
info["btn"] = boost::lexical_cast<std::string>(btn);
info["btn"] = startup_button_names[btn];
info["mods"] = boost::lexical_cast<std::string>(mods);
record_action("startup_button_click", info);
}