Implement a feature flags system.

* Scenarios contain a string map of feature flags. The flag names are the keys, and flag versions are the values, so a typical value might be "fixed" for bug fixes or for evolving features, "V1", "V2", etc.
* The game has a map of flags to lists of supported versions. The game can therefore signal that it supports a legacy behavior for a given feature flag. The last version in the list is considered to be this build version's default behavior.
* When launching a scenario, we check to make sure the game supports the scenario's required versions of its feature flags.
* When launching a replay, we make sure the game supports the feature flags that the version of the game that made the recording did.

Fix #555
Close #591
This commit is contained in:
2025-02-08 19:27:27 -06:00
committed by Celtic Minstrel
parent f0662902cb
commit f80f8a932a
11 changed files with 135 additions and 1 deletions

View File

@@ -89,6 +89,15 @@ boost::optional<location> scen_arg_out_sec, scen_arg_loc;
extern std::string last_load_file;
std::string help_text_rsrc = "help";
/*
// Example feature flags:
{
// A build which supports both V2 and V3 of the updated graphics sheet:
{"graphics-sheet", {"V2", "V3"}}
}
*/
std::map<std::string,std::vector<std::string>> feature_flags = {};
struct cParseEntrance {
boost::optional<short>& opt;
cParseEntrance(boost::optional<short>& opt) : opt(opt) {}
@@ -925,6 +934,50 @@ static void replay_next_action() {
replay_action(pop_next_action());
}
static void record_feature_flags() {
Element next_action("feature_flags");
for(auto& p : feature_flags){
Element next_flag(p.first);
std::vector<std::string> supported_versions = p.second;
for(std::string version : supported_versions){
Element next_version("version");
Text version_text(version);
next_version.InsertEndChild(version_text);
next_flag.InsertEndChild(next_version);
}
next_action.InsertEndChild(next_flag);
}
record_action(next_action);
}
static void replay_feature_flags() {
std::map<std::string,std::vector<std::string>> recorded_flags = {};
if(has_next_action("feature_flags")){
Element action = pop_next_action();
Element* next_flag = action.FirstChildElement(false);
while(next_flag){
std::string flag = next_flag->Value();
std::vector<std::string> supported_versions;
Element* next_version = next_flag->FirstChildElement(false);
while(next_version){
std::string version = next_version->GetText();
// The game build needs to support the feature version that the replay had
if(!has_feature_flag(flag, version)){
std::string error = "This replay requires a feature that is not supported in your version of Blades of Exile: " + flag + " should support '" + version + "'";
throw error;
}
supported_versions.push_back(version);
next_version = next_version->NextSiblingElement(false);
}
recorded_flags[flag] = supported_versions;
next_flag = next_flag->NextSiblingElement(false);
}
}
feature_flags = recorded_flags;
}
void init_boe(int argc, char* argv[]) {
set_up_apple_events();
init_directories(argv[0]);
@@ -947,6 +1000,12 @@ void init_boe(int argc, char* argv[]) {
set_cursor(watch_curs);
init_buf();
if(recording){
record_feature_flags();
}else if(replaying){
replay_feature_flags();
}
// Seed the RNG
if(replaying) {
Element& srand_element = pop_next_action("srand");