When starting or loading a game, warn if the next scenario is unknown

Check that the scenario's next_scenario= exists, and display a warning
if it would lead to an "Unknown Scenario" error. This also checks any
[endlevel] tags in events.

The error message includes the id of the missing scenario, but is mainly
a recommendation to check the forums and report a bug in the campaign.

This version has a short, one-size-fits-all, text for the error message.
This commit is contained in:
Steve Cotton 2021-06-07 11:36:28 +02:00 committed by Steve Cotton
parent aa83b52a9e
commit 17edbcb90a
4 changed files with 202 additions and 0 deletions

View file

@ -0,0 +1,106 @@
# wmllint: no translatables
# Check that the "this will lead to an unknown scenario" warning doesn't get triggered.
{GENERIC_UNIT_TEST "unknown_scenario_false_positives" (
# Note: the C++ code under test runs after all name=start events have run,
# so putting the {SUCCEED} in a start event will skip the test.
[event]
name = side 1 turn 1
{SUCCEED}
[/event]
# This event doesn't get triggered, but its contents will be checked.
[event]
name = turn 2
# A scenario that exists (has to be a [test], as the current scenario's tagname is used).
[endlevel]
next_scenario = "empty_test"
[/endlevel]
# Variable interpolation (false negatives are acceptable, but not false positives).
{VARIABLE chosen_branch empty_test}
[endlevel]
next_scenario = "$chosen_branch"
[/endlevel]
# Using the scenario's next_scenario instead of overriding it
[endlevel]
result=victory
[/endlevel]
#ifndef SCHEMA_SHOULD_SKIP_THIS
# Should only check [endlevel] tags, not similarly named attributes in other tags
[dummy]
next_scenario = "non_existent_scenario"
[/dummy]
#endif
[/event]
)}
# Not a branching scenario, but the only route uses a variable
{GENERIC_UNIT_TEST "unknown_scenario_interpolated" (
# Note: the C++ code under test runs after all name=start events have run,
# so putting the {SUCCEED} in a start event will skip the test.
[event]
name = side 1 turn 1
{SUCCEED}
[/event]
next_scenario = "$chosen_branch"
)}
# The tests below should all trigger the dialog, thus returning result BROKE_STRICT.
#
# There are variants to trigger all the alternative wordings of the dialog for interactive testing.
#
# For the automated testing, there's value in running exactly one of these, because any of them will
# check that triggering the dialog results in BROKE_STRICT, making the tests above fail.
#
# This is effectively a varargs macro. There's always a branch to "non_existent_scenario", and since
# the code under test combines non-unique ids then any next_scenario that points at the same id
# won't change the warning message.
#define TEST_UNKNOWN_SCENARIO NAME
#arg SCEN2
"non_existent_scenario"
#endarg
#arg SCEN3
"non_existent_scenario"
#endarg
#arg SCEN4
"non_existent_scenario"
#endarg
{GENERIC_UNIT_TEST {NAME} (
[event]
name = side 1 turn 1
{SUCCEED}
[/event]
next_scenario = "non_existent_scenario"
[event]
name = turn 2
[endlevel]
next_scenario = {SCEN2}
[/endlevel]
[endlevel]
next_scenario = {SCEN3}
[/endlevel]
[endlevel]
next_scenario = {SCEN4}
[/endlevel]
[/event]
)}
#enddef
# The numbers in the names are (number of broken branches) (number of ok branches) (last scenario)
{TEST_UNKNOWN_SCENARIO "unknown_scenario_1_0"}
{TEST_UNKNOWN_SCENARIO "unknown_scenario_1_0_last" SCEN2=""}
{TEST_UNKNOWN_SCENARIO "unknown_scenario_1_1" SCEN2="test_return"}
{TEST_UNKNOWN_SCENARIO "unknown_scenario_2_0" SCEN2="non_existent_scenario_2"}
# This should give the same message as unknown_scenario_1_1_last, because "" and "null" are equivalent
{TEST_UNKNOWN_SCENARIO "unknown_scenario_1_1_last_null" SCEN2="test_return" SCEN3="" SCEN4="null"}
#undef TEST_UNKNOWN_SCENARIO

View file

@ -36,6 +36,7 @@
#include "game_state.hpp"
#include "gettext.hpp"
#include "gui/dialogs/loading_screen.hpp"
#include "gui/dialogs/message.hpp" // for show_error_message
#include "gui/dialogs/transient_message.hpp"
#include "hotkey/command_executor.hpp"
#include "hotkey/hotkey_handler.hpp"
@ -64,6 +65,7 @@
#include "units/id.hpp"
#include "units/types.hpp"
#include "units/unit.hpp"
#include "utils/general.hpp"
#include "whiteboard/manager.hpp"
#include <functional>
@ -1196,6 +1198,91 @@ void play_controller::start_game()
gamestate().gamedata_.set_phase(game_data::PLAY);
gui_->recalculate_minimap();
}
check_next_scenario_is_known();
}
/**
* Find all [endlevel]next_scenario= attributes, and add them to @a result.
*/
static void find_next_scenarios(const config& parent, std::set<std::string>& result) {
for(const auto& endlevel : parent.child_range("endlevel")) {
if(endlevel.has_attribute("next_scenario")) {
result.insert(endlevel["next_scenario"]);
}
}
for(const auto& cfg : parent.all_children_range()) {
find_next_scenarios(cfg.cfg, result);
}
};
void play_controller::check_next_scenario_is_known() {
// Which scenarios are reachable from the current one?
std::set<std::string> possible_next_scenarios;
possible_next_scenarios.insert(gamestate().gamedata_.next_scenario());
// Find all "endlevel" tags that could be triggered in events
config events;
gamestate().events_manager_->write_events(events);
find_next_scenarios(events, possible_next_scenarios);
// Are we looking for [scenario]id=, [multiplayer]id= or [test]id=?
const auto tagname = saved_game_.classification().get_tagname();
// Of the possible routes, work out which exist.
bool possible_this_is_the_last_scenario = false;
std::vector<std::string> known;
std::vector<std::string> unknown;
for(const auto& x : possible_next_scenarios) {
if(x.empty() || x == "null") {
possible_this_is_the_last_scenario = true;
LOG_NG << "This can be the last scenario\n";
} else if(utils::contains(x, '$')) {
// Assume a WML variable will be set to a correct value before the end of the scenario
known.push_back(x);
LOG_NG << "Variable value for next scenario '" << x << "'\n";
} else if(game_config_.find_child(tagname, "id", x)) {
known.push_back(x);
LOG_NG << "Known next scenario '" << x << "'\n";
} else {
unknown.push_back(x);
ERR_NG << "Unknown next scenario '" << x << "'\n";
}
}
if(unknown.empty()) {
// everything's good
return;
}
std::string title = _("Warning: broken campaign branches");
std::stringstream message;
message << _n(
// TRANSLATORS: This is an error that will hopefully only be seen by UMC authors and by players who have already
// said "okay" to a "loading saves from an old version might not work" dialog.
"The next scenario is missing, you will not be able to finish this campaign.",
// TRANSLATORS: This is an error that will hopefully only be seen by UMC authors and by players who have already
// said "okay" to a "loading saves from an old version might not work" dialog.
"Some of the possible next scenarios are missing, you might not be able to finish this campaign.",
unknown.size() + known.size() + (possible_this_is_the_last_scenario ? 1 : 0));
message << "\n\n";
message << _n(
"Please report the following missing scenario to the campaigns author:\n$unknown_list|",
"Please report the following missing scenarios to the campaigns author:\n$unknown_list|",
unknown.size());
message << "\n";
message << _("Once this is fixed, you will need to restart this scenario.");
std::stringstream unknown_list;
for(const auto& x : unknown) {
unknown_list << font::unicode_bullet << " " << x << "\n";
}
utils::string_map symbols;
symbols["unknown_list"] = unknown_list.str();
auto message_str = utils::interpolate_variables_into_string(message.str(), &symbols);
ERR_NG << message_str << "\n";
gui2::show_message(title, message_str, gui2::dialogs::message::close_button);
}
bool play_controller::can_use_synced_wml_menu() const

View file

@ -406,6 +406,11 @@ private:
void init(const config& level);
/**
* This shows a warning dialog if either [scenario]next_scenario or any [endlevel]next_scenario would lead to an "Unknown Scenario" dialog.
*/
void check_next_scenario_is_known();
bool victory_when_enemies_defeated_;
bool remove_from_carryover_on_defeat_;
std::vector<std::string> victory_music_;

View file

@ -329,3 +329,7 @@
0 event_name_variable_substitution
# Game mechanics
0 heal
# Warnings about WML
0 unknown_scenario_false_positives
0 unknown_scenario_interpolated
5 unknown_scenario_1_0