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

@@ -8,6 +8,7 @@
#include <sstream>
#include <map>
#include "boe.consts.hpp"
#include "universe/universe.hpp"
#define ASB add_string_to_buf
#define PSD univ.party.stuff_done
@@ -31,4 +32,28 @@ 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;
extern cUniverse univ;
extern std::map<std::string, std::vector<std::string>> feature_flags;
inline bool has_feature_flag(std::string flag, std::string version) {
auto iter = feature_flags.find(flag);
if(iter == feature_flags.end()) return false;
std::vector<std::string> versions = iter->second;
return std::find(versions.begin(), versions.end(), version) != versions.end();
}
// Return the version of a feature that SHOULD BE USED in the currently running game.
inline std::string get_feature_version(std::string flag) {
// If a scenario is loaded and specifies the flag, use that version.
if(!univ.party.scen_name.empty()){
std::string scenario_flag = univ.scenario.get_feature_flag(flag);
if(!scenario_flag.empty()) return scenario_flag;
}
// Otherwise, use the most recent version of the feature supported by this build,
// or by the build that recorded the current replay.
auto iter = feature_flags.find(flag);
if(iter == feature_flags.end()) return "";
return iter->second.back();
}
#endif

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");

View File

@@ -171,6 +171,14 @@ void put_party_in_scen(std::string scen_name, bool force) {
show_get_items("Choose stored items to keep:", saved_item_refs, pc, true);
}
// Make sure the game build supports all the scenario's features
for(auto pair : univ.scenario.feature_flags){
if(!has_feature_flag(pair.first, pair.second)){
showError("This scenario requires a feature that is not supported in your version of Blades of Exile: " + pair.first + " should support '" + pair.second + "'");
return;
}
}
univ.enter_scenario(scen_name);
// if at this point, startup must be over, so make this call to make sure we're ready,