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:
parent
aa83b52a9e
commit
17edbcb90a
4 changed files with 202 additions and 0 deletions
106
data/test/scenarios/unknown_scenario_warning.cfg
Normal file
106
data/test/scenarios/unknown_scenario_warning.cfg
Normal 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
|
|
@ -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 campaign’s author:\n$unknown_list|",
|
||||
"Please report the following missing scenarios to the campaign’s 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
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue